Only allow worker async finishing when sandboxed.
All workers under dynamic execution are sandboxed, but on user interrupt non-sandboxed workers might keep working even after another invocation has begun, which could cause issues. Instead, we kill off interrupted workers that are neither sandboxed nor under
dynamic execution.
RELNOTES: None.
PiperOrigin-RevId: 373141238
diff --git a/src/main/java/com/google/devtools/build/lib/worker/SandboxedWorker.java b/src/main/java/com/google/devtools/build/lib/worker/SandboxedWorker.java
index bf11c68..9798e96 100644
--- a/src/main/java/com/google/devtools/build/lib/worker/SandboxedWorker.java
+++ b/src/main/java/com/google/devtools/build/lib/worker/SandboxedWorker.java
@@ -33,6 +33,11 @@
}
@Override
+ public boolean isSandboxed() {
+ return true;
+ }
+
+ @Override
public void prepareExecution(
SandboxInputs inputFiles, SandboxOutputs outputs, Set<PathFragment> workerFiles)
throws IOException {
diff --git a/src/main/java/com/google/devtools/build/lib/worker/SingleplexWorker.java b/src/main/java/com/google/devtools/build/lib/worker/SingleplexWorker.java
index 3edce2c..3412dc3 100644
--- a/src/main/java/com/google/devtools/build/lib/worker/SingleplexWorker.java
+++ b/src/main/java/com/google/devtools/build/lib/worker/SingleplexWorker.java
@@ -93,6 +93,11 @@
}
@Override
+ public boolean isSandboxed() {
+ return false;
+ }
+
+ @Override
public void prepareExecution(
SandboxInputs inputFiles, SandboxOutputs outputs, Set<PathFragment> workerFiles)
throws IOException {
diff --git a/src/main/java/com/google/devtools/build/lib/worker/Worker.java b/src/main/java/com/google/devtools/build/lib/worker/Worker.java
index defc5be..ce2447e 100644
--- a/src/main/java/com/google/devtools/build/lib/worker/Worker.java
+++ b/src/main/java/com/google/devtools/build/lib/worker/Worker.java
@@ -66,6 +66,9 @@
return workerKey.getWorkerFilesWithHashes();
}
+ /** Returns true if this worker is sandboxed. */
+ public abstract boolean isSandboxed();
+
/**
* Sets the reporter this {@code Worker} should report anomalous events to, or clears it. We
* expect the reporter to be cleared at end of build.
diff --git a/src/main/java/com/google/devtools/build/lib/worker/WorkerFactory.java b/src/main/java/com/google/devtools/build/lib/worker/WorkerFactory.java
index f05b8fe..1b3d25a 100644
--- a/src/main/java/com/google/devtools/build/lib/worker/WorkerFactory.java
+++ b/src/main/java/com/google/devtools/build/lib/worker/WorkerFactory.java
@@ -99,11 +99,9 @@
return new DefaultPooledObject<>(worker);
}
- /**
- * When a worker process is discarded, destroy its process, too.
- */
+ /** When a worker process is discarded, destroy its process, too. */
@Override
- public void destroyObject(WorkerKey key, PooledObject<Worker> p) throws Exception {
+ public void destroyObject(WorkerKey key, PooledObject<Worker> p) {
if (workerOptions.workerVerbose) {
int workerId = p.getObject().getWorkerId();
reporter.handle(
diff --git a/src/main/java/com/google/devtools/build/lib/worker/WorkerProxy.java b/src/main/java/com/google/devtools/build/lib/worker/WorkerProxy.java
index 7a508f8..0b93239 100644
--- a/src/main/java/com/google/devtools/build/lib/worker/WorkerProxy.java
+++ b/src/main/java/com/google/devtools/build/lib/worker/WorkerProxy.java
@@ -55,6 +55,11 @@
}
@Override
+ public boolean isSandboxed() {
+ return false;
+ }
+
+ @Override
void setReporter(EventHandler reporter) {
// We might have created this multiplexer after setting the reporter for existing multiplexers
workerMultiplexer.setReporter(reporter);
diff --git a/src/main/java/com/google/devtools/build/lib/worker/WorkerSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/worker/WorkerSpawnRunner.java
index 2682aab..ca6a3b0 100644
--- a/src/main/java/com/google/devtools/build/lib/worker/WorkerSpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/worker/WorkerSpawnRunner.java
@@ -457,12 +457,27 @@
try {
response = worker.getResponse(request.getRequestId());
} catch (InterruptedException e) {
- finishWorkAsync(
- key,
- worker,
- request,
- workerOptions.workerCancellation && Spawns.supportsWorkerCancellation(spawn));
- worker = null;
+ if (worker.isSandboxed()) {
+ // Sandboxed workers can safely finish their work async.
+ finishWorkAsync(
+ key,
+ worker,
+ request,
+ workerOptions.workerCancellation && Spawns.supportsWorkerCancellation(spawn));
+ worker = null;
+ } else if (!key.isSpeculative()) {
+ // Non-sandboxed workers interrupted outside of dynamic execution can only mean that
+ // the user interrupted the build, and we don't want to delay finishing. Instead we
+ // kill the worker.
+ // Technically, workers are always sandboxed under dynamic execution, at least for now.
+ try {
+ workers.invalidateObject(key, worker);
+ } catch (IOException e1) {
+ // Nothing useful we can do here, in fact it may not be possible to get here.
+ } finally {
+ worker = null;
+ }
+ }
throw e;
} catch (IOException e) {
restoreInterrupt(e);
diff --git a/src/test/java/com/google/devtools/build/lib/worker/WorkerSpawnRunnerTest.java b/src/test/java/com/google/devtools/build/lib/worker/WorkerSpawnRunnerTest.java
index 8991d27..fcdfea1 100644
--- a/src/test/java/com/google/devtools/build/lib/worker/WorkerSpawnRunnerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/worker/WorkerSpawnRunnerTest.java
@@ -179,8 +179,10 @@
throws ExecException, InterruptedException, IOException {
WorkerOptions workerOptions = new WorkerOptions();
workerOptions.workerCancellation = true;
+ workerOptions.workerSandboxing = true;
when(spawn.getExecutionInfo())
.thenReturn(ImmutableMap.of(ExecutionRequirements.SUPPORTS_WORKER_CANCELLATION, "1"));
+ when(worker.isSandboxed()).thenReturn(true);
WorkerSpawnRunner runner =
new WorkerSpawnRunner(
new SandboxHelpers(false),
@@ -196,6 +198,7 @@
WorkerKey key = createWorkerKey(fs, "mnem", false);
Path logFile = fs.getPath("/worker.log");
Semaphore secondResponseRequested = new Semaphore(0);
+ // Fake that the getting the regular response gets interrupted and we then answer the cancel.
when(worker.getResponse(anyInt()))
.thenThrow(new InterruptedException())
.thenAnswer(
@@ -230,6 +233,53 @@
}
@Test
+ public void testExecInWorker_unsandboxedDiesOnInterrupt()
+ throws InterruptedException, IOException {
+ WorkerOptions workerOptions = new WorkerOptions();
+ workerOptions.workerCancellation = true;
+ workerOptions.workerSandboxing = false;
+ when(spawn.getExecutionInfo())
+ .thenReturn(ImmutableMap.of(ExecutionRequirements.SUPPORTS_WORKER_CANCELLATION, "1"));
+ WorkerSpawnRunner runner =
+ new WorkerSpawnRunner(
+ new SandboxHelpers(false),
+ fs.getPath("/execRoot"),
+ createWorkerPool(),
+ /* multiplex */ false,
+ reporter,
+ localEnvProvider,
+ /* binTools */ null,
+ resourceManager,
+ /* runfilesTreeUpdater=*/ null,
+ workerOptions);
+ WorkerKey key = createWorkerKey(fs, "mnem", false);
+ Path logFile = fs.getPath("/worker.log");
+ when(worker.getResponse(anyInt())).thenThrow(new InterruptedException());
+
+ // Since this worker is not sandboxed, it will just get killed on interrupt.
+ assertThrows(
+ InterruptedException.class,
+ () ->
+ runner.execInWorker(
+ spawn,
+ key,
+ context,
+ new SandboxInputs(ImmutableMap.of(), ImmutableSet.of(), ImmutableMap.of()),
+ SandboxOutputs.create(ImmutableSet.of(), ImmutableSet.of()),
+ ImmutableList.of(),
+ inputFileCache,
+ spawnMetrics));
+
+ assertThat(logFile.exists()).isFalse();
+ verify(context, times(1)).report(ProgressStatus.EXECUTING, "worker");
+ ArgumentCaptor<WorkRequest> argumentCaptor = ArgumentCaptor.forClass(WorkRequest.class);
+ verify(worker, times(1)).putRequest(argumentCaptor.capture());
+ assertThat(argumentCaptor.getAllValues().get(0))
+ .isEqualTo(WorkRequest.newBuilder().setRequestId(0).build());
+ verify(worker, times(1)).destroy();
+ }
+
+ @Test
public void testExecInWorker_noMultiplexWithDynamic()
throws ExecException, InterruptedException, IOException {
WorkerOptions workerOptions = new WorkerOptions();