diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionMetadata.java b/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionMetadata.java index 0dd03238eb2a4d..eb9cc3d0e68897 100644 --- a/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionMetadata.java +++ b/src/main/java/com/google/devtools/build/lib/actions/ActionExecutionMetadata.java @@ -114,4 +114,14 @@ default String getProgressMessage(RepositoryMapping mainRepositoryMapping) { default boolean mayInsensitivelyPropagateInputs() { return false; } + + /** + * Returns true if the action may modify spawn outputs after the spawn has executed. + * + *
If this returns true, any kind of spawn output caching or reuse needs to happen
+ * synchronously directly after the spawn execution.
+ */
+ default boolean mayModifySpawnOutputsAfterExecution() {
+ return false;
+ }
}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java b/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java
index 70e2259748ac52..752888254e4db8 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java
@@ -309,6 +309,16 @@ private static ImmutableSet Every call to this method must be matched by a call to {@link #unregister()} via
+ * try-finally.
+ *
+ * This always returns true for actions that do not modify their spawns' outputs after
+ * execution.
+ */
+ public boolean registerForOutputReuse() {
+ // We only use a single phase.
+ return spawnResultConsumers.register() == 0;
+ }
+
+ /**
+ * Unregisters a thread waiting for the {@link #spawnResultFuture}, either after successful
+ * reuse of the outputs or upon failure.
+ */
+ public void unregister() {
+ spawnResultConsumers.arriveAndDeregister();
+ }
+
+ /**
+ * Waits for all potential consumers of the {@link #spawnResultFuture} to be done with their
+ * output reuse.
+ */
+ public void awaitAllOutputReuse() {
+ spawnResultConsumers.arriveAndAwaitAdvance();
+ }
+
/**
* Signals to all potential consumers of the {@link #spawnResultFuture} that this execution has
* been cancelled and that the result will not be available.
@@ -1571,7 +1610,8 @@ public void uploadOutputs(RemoteAction action, SpawnResult spawnResult, Runnable
SpawnResult.Status.SUCCESS.equals(spawnResult.status()) && spawnResult.exitCode() == 0,
"shouldn't upload outputs of failed local action");
- if (remoteOptions.remoteCacheAsync) {
+ if (remoteOptions.remoteCacheAsync
+ && !action.getSpawn().getResourceOwner().mayModifySpawnOutputsAfterExecution()) {
Single.using(
remoteCache::retain,
remoteCache ->
diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnCache.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnCache.java
index 3224f56ac23684..ae802b93203935 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnCache.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteSpawnCache.java
@@ -109,89 +109,110 @@ public CacheHandle lookup(Spawn spawn, SpawnExecutionContext context)
// results haven't been uploaded to the cache yet and deduplicate all of them against the
// first one.
LocalExecution previousExecution = null;
- thisExecution = LocalExecution.createIfDeduplicatable(action);
- if (shouldUploadLocalResults && thisExecution != null) {
- previousExecution = inFlightExecutions.putIfAbsent(action.getActionKey(), thisExecution);
- }
- // Metadata will be available in context.current() until we detach.
- // This is done via a thread-local variable.
try {
- RemoteActionResult result;
- try (SilentCloseable c = prof.profile(ProfilerTask.REMOTE_CACHE_CHECK, "check cache hit")) {
- result = remoteExecutionService.lookupCache(action);
- }
- // In case the remote cache returned a failed action (exit code != 0) we treat it as a
- // cache miss
- if (result != null && result.getExitCode() == 0) {
- Stopwatch fetchTime = Stopwatch.createStarted();
- InMemoryOutput inMemoryOutput;
- try (SilentCloseable c = prof.profile(REMOTE_DOWNLOAD, "download outputs")) {
- inMemoryOutput = remoteExecutionService.downloadOutputs(action, result);
- }
- fetchTime.stop();
- totalTime.stop();
- spawnMetrics
- .setFetchTimeInMs((int) fetchTime.elapsed().toMillis())
- .setTotalTimeInMs((int) totalTime.elapsed().toMillis())
- .setNetworkTimeInMs((int) action.getNetworkTime().getDuration().toMillis());
- SpawnResult spawnResult =
- createSpawnResult(
- digestUtil,
+ thisExecution = LocalExecution.createIfDeduplicatable(action);
+ if (shouldUploadLocalResults && thisExecution != null) {
+ LocalExecution previousOrThisExecution =
+ inFlightExecutions.merge(
action.getActionKey(),
- result.getExitCode(),
- /* cacheHit= */ true,
- result.cacheName(),
- inMemoryOutput,
- result.getExecutionMetadata().getExecutionStartTimestamp(),
- result.getExecutionMetadata().getExecutionCompletedTimestamp(),
- spawnMetrics.build(),
- spawn.getMnemonic());
- return SpawnCache.success(spawnResult);
+ thisExecution,
+ (existingExecution, thisExecutionArg) -> {
+ if (existingExecution.registerForOutputReuse()) {
+ return existingExecution;
+ } else {
+ // The existing execution has completed and its results may have already
+ // been modified by its action, so we can't deduplicate against it. Instead,
+ // start a new in-flight execution.
+ return thisExecutionArg;
+ }
+ });
+ previousExecution =
+ previousOrThisExecution == thisExecution ? null : previousOrThisExecution;
}
- } catch (CacheNotFoundException e) {
- // Intentionally left blank
- } catch (IOException e) {
- if (BulkTransferException.allCausedByCacheNotFoundException(e)) {
+ try {
+ RemoteActionResult result;
+ try (SilentCloseable c =
+ prof.profile(ProfilerTask.REMOTE_CACHE_CHECK, "check cache hit")) {
+ result = remoteExecutionService.lookupCache(action);
+ }
+ // In case the remote cache returned a failed action (exit code != 0) we treat it as a
+ // cache miss
+ if (result != null && result.getExitCode() == 0) {
+ Stopwatch fetchTime = Stopwatch.createStarted();
+ InMemoryOutput inMemoryOutput;
+ try (SilentCloseable c = prof.profile(REMOTE_DOWNLOAD, "download outputs")) {
+ inMemoryOutput = remoteExecutionService.downloadOutputs(action, result);
+ }
+ fetchTime.stop();
+ totalTime.stop();
+ spawnMetrics
+ .setFetchTimeInMs((int) fetchTime.elapsed().toMillis())
+ .setTotalTimeInMs((int) totalTime.elapsed().toMillis())
+ .setNetworkTimeInMs((int) action.getNetworkTime().getDuration().toMillis());
+ SpawnResult spawnResult =
+ createSpawnResult(
+ digestUtil,
+ action.getActionKey(),
+ result.getExitCode(),
+ /* cacheHit= */ true,
+ result.cacheName(),
+ inMemoryOutput,
+ result.getExecutionMetadata().getExecutionStartTimestamp(),
+ result.getExecutionMetadata().getExecutionCompletedTimestamp(),
+ spawnMetrics.build(),
+ spawn.getMnemonic());
+ return SpawnCache.success(spawnResult);
+ }
+ } catch (CacheNotFoundException e) {
// Intentionally left blank
- } else {
- String errorMessage = Utils.grpcAwareErrorMessage(e, verboseFailures);
- if (isNullOrEmpty(errorMessage)) {
- errorMessage = e.getClass().getSimpleName();
+ } catch (IOException e) {
+ if (BulkTransferException.allCausedByCacheNotFoundException(e)) {
+ // Intentionally left blank
+ } else {
+ String errorMessage = Utils.grpcAwareErrorMessage(e, verboseFailures);
+ if (isNullOrEmpty(errorMessage)) {
+ errorMessage = e.getClass().getSimpleName();
+ }
+ errorMessage = "Remote Cache: " + errorMessage;
+ remoteExecutionService.report(Event.warn(errorMessage));
}
- errorMessage = "Remote Cache: " + errorMessage;
- remoteExecutionService.report(Event.warn(errorMessage));
}
- }
- if (previousExecution != null) {
- Stopwatch fetchTime = Stopwatch.createStarted();
- SpawnResult previousResult;
- try (SilentCloseable c = prof.profile(REMOTE_DOWNLOAD, "reuse outputs")) {
- previousResult = remoteExecutionService.waitForAndReuseOutputs(action, previousExecution);
- }
- if (previousResult != null) {
- spawnMetrics
- .setFetchTimeInMs((int) fetchTime.elapsed().toMillis())
- .setTotalTimeInMs((int) totalTime.elapsed().toMillis())
- .setNetworkTimeInMs((int) action.getNetworkTime().getDuration().toMillis());
- SpawnMetrics buildMetrics = spawnMetrics.build();
- return SpawnCache.success(
- new SpawnResult.DelegateSpawnResult(previousResult) {
- @Override
- public String getRunnerName() {
- return "deduplicated";
- }
+ if (previousExecution != null) {
+ Stopwatch fetchTime = Stopwatch.createStarted();
+ SpawnResult previousResult;
+ try (SilentCloseable c = prof.profile(REMOTE_DOWNLOAD, "reuse outputs")) {
+ previousResult =
+ remoteExecutionService.waitForAndReuseOutputs(action, previousExecution);
+ }
+ if (previousResult != null) {
+ spawnMetrics
+ .setFetchTimeInMs((int) fetchTime.elapsed().toMillis())
+ .setTotalTimeInMs((int) totalTime.elapsed().toMillis())
+ .setNetworkTimeInMs((int) action.getNetworkTime().getDuration().toMillis());
+ SpawnMetrics buildMetrics = spawnMetrics.build();
+ return SpawnCache.success(
+ new SpawnResult.DelegateSpawnResult(previousResult) {
+ @Override
+ public String getRunnerName() {
+ return "deduplicated";
+ }
- @Override
- public SpawnMetrics getMetrics() {
- return buildMetrics;
- }
- });
+ @Override
+ public SpawnMetrics getMetrics() {
+ return buildMetrics;
+ }
+ });
+ }
+ // If we reach here, the previous execution was not successful (it encountered an
+ // exception or the spawn had an exit code != 0). Since it isn't possible to accurately
+ // recreate the failure without rerunning the action, we fall back to running the action
+ // locally. This means that we have introduced an unnecessary wait, but that can only
+ // happen in the case of a failing build with --keep_going.
+ }
+ } finally {
+ if (previousExecution != null) {
+ previousExecution.unregister();
}
- // If we reach here, the previous execution was not successful (it encountered an exception
- // or the spawn had an exit code != 0). Since it isn't possible to accurately recreate the
- // failure without rerunning the action, we fall back to running the action locally. This
- // means that we have introduced an unnecessary wait, but that can only happen in the case
- // of a failing build with --keep_going.
}
}
@@ -239,6 +260,17 @@ public void store(SpawnResult result) throws ExecException, InterruptedException
// large.
remoteExecutionService.uploadOutputs(
action, result, () -> inFlightExecutions.remove(action.getActionKey()));
+ if (thisExecutionFinal != null
+ && action.getSpawn().getResourceOwner().mayModifySpawnOutputsAfterExecution()) {
+ // In this case outputs have been uploaded synchronously and the callback above has run,
+ // so no new executions will be deduplicated against this one. We can safely await all
+ // existing executions finish the reuse.
+ // Note that while this call itself isn't interruptible, all operations it awaits are
+ // interruptible.
+ try (SilentCloseable c = prof.profile(REMOTE_DOWNLOAD, "await output reuse")) {
+ thisExecutionFinal.awaitAllOutputReuse();
+ }
+ }
}
private void checkForConcurrentModifications()
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompileAction.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompileAction.java
index 06068df7d8c76d..5db91c5f75888b 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompileAction.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompileAction.java
@@ -709,6 +709,14 @@ public NestedSet