diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlockWaitingModule.java b/src/main/java/com/google/devtools/build/lib/runtime/BlockWaitingModule.java
index 726ef3715caacd..a628a54a3b0f68 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BlockWaitingModule.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlockWaitingModule.java
@@ -19,28 +19,62 @@
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.devtools.build.lib.concurrent.ExecutorUtil;
import com.google.devtools.build.lib.util.AbruptExitException;
+import java.util.ArrayList;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
import javax.annotation.Nullable;
/** A {@link BlazeModule} that waits for submitted tasks to terminate after every command. */
public class BlockWaitingModule extends BlazeModule {
+
+ /** A task to be submitted. */
+ public interface Task {
+ void call() throws AbruptExitException;
+ }
+
+ /**
+ * Wraps an AbruptExitException thrown by a task.
+ *
+ *
This is needed because a task that can throw a checked exception cannot be submitted to
+ * {@link ExecutorService}.
+ */
+ private static class TaskException extends RuntimeException {
+ TaskException(AbruptExitException cause) {
+ super(cause);
+ }
+ }
+
@Nullable private ExecutorService executorService;
+ @Nullable private ArrayList> submittedTasks;
@Override
public void beforeCommand(CommandEnvironment env) throws AbruptExitException {
checkState(executorService == null, "executorService must be null");
+ checkState(submittedTasks == null, "submittedTasks must be null");
executorService =
Executors.newCachedThreadPool(
new ThreadFactoryBuilder().setNameFormat("block-waiting-%d").build());
+
+ submittedTasks = new ArrayList<>();
}
@SuppressWarnings("FutureReturnValueIgnored")
- public void submit(Runnable task) {
+ public void submit(Task task) {
checkNotNull(executorService, "executorService must not be null");
+ checkNotNull(submittedTasks, "submittedTasks must be null");
- executorService.submit(task);
+ submittedTasks.add(
+ executorService.submit(
+ () -> {
+ try {
+ task.call();
+ } catch (AbruptExitException e) {
+ throw new TaskException(e);
+ }
+ }));
}
@Override
@@ -51,6 +85,22 @@ public void afterCommand() throws AbruptExitException {
Thread.currentThread().interrupt();
}
+ for (Future> f : submittedTasks) {
+ try {
+ f.get(); // guaranteed to have completed.
+ } catch (InterruptedException e) {
+ throw new AssertionError("task should not have been interrupted");
+ } catch (ExecutionException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof TaskException) {
+ checkState(cause.getCause() instanceof AbruptExitException);
+ throw (AbruptExitException) cause.getCause();
+ }
+ throw new RuntimeException(e);
+ }
+ }
+
executorService = null;
+ submittedTasks = null;
}
}
diff --git a/src/test/java/com/google/devtools/build/lib/runtime/BlockWaitingModuleTest.java b/src/test/java/com/google/devtools/build/lib/runtime/BlockWaitingModuleTest.java
new file mode 100644
index 00000000000000..a68dfb063d94e9
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/runtime/BlockWaitingModuleTest.java
@@ -0,0 +1,128 @@
+// Copyright 2022 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.runtime;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import com.google.devtools.build.lib.runtime.BlockWaitingModule.Task;
+import com.google.devtools.build.lib.server.FailureDetails.Crash;
+import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.DetailedExitCode;
+import java.util.concurrent.ExecutionException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+
+/** Tests for {@link BlockWaitingModule}. */
+@RunWith(JUnit4.class)
+public final class BlockWaitingModuleTest {
+
+ private static final DetailedExitCode CRASH =
+ DetailedExitCode.of(
+ FailureDetail.newBuilder()
+ .setMessage("crash")
+ .setCrash(Crash.newBuilder().setCode(Crash.Code.CRASH_UNKNOWN))
+ .build());
+
+ BlockWaitingModule module;
+ @Mock CommandEnvironment env;
+
+ @Test
+ public void testSubmitZeroTasks() throws Exception {
+ // arrange
+ BlockWaitingModule m = new BlockWaitingModule();
+
+ // act
+ m.beforeCommand(env);
+ m.afterCommand();
+
+ // nothing to assert
+ }
+
+ @Test
+ public void testSubmitOneTask() throws Exception {
+ // arrange
+ BlockWaitingModule m = new BlockWaitingModule();
+ Task t = mock(Task.class);
+
+ // act
+ m.beforeCommand(env);
+ m.submit(t);
+ m.afterCommand();
+
+ // assert
+ verify(t).call();
+ }
+
+ @Test
+ public void testSubmitMultipleTasks() throws Exception {
+ // arrange
+ BlockWaitingModule m = new BlockWaitingModule();
+ Task t1 = mock(Task.class);
+ Task t2 = mock(Task.class);
+ Task t3 = mock(Task.class);
+
+ // act
+ m.beforeCommand(env);
+ m.submit(t1);
+ m.submit(t2);
+ m.submit(t3);
+ m.afterCommand();
+
+ // assert
+ verify(t1).call();
+ verify(t2).call();
+ verify(t3).call();
+ }
+
+ @Test
+ public void testTaskThrowsAbruptExitException() throws Exception {
+ // arrange
+ BlockWaitingModule m = new BlockWaitingModule();
+ Task t = mock(Task.class);
+ doThrow(new AbruptExitException(CRASH)).when(t).call();
+
+ // act
+ m.beforeCommand(env);
+ m.submit(t);
+
+ // assert
+ Throwable e = assertThrows(AbruptExitException.class, m::afterCommand);
+ assertThat(((AbruptExitException) e).getDetailedExitCode()).isEqualTo(CRASH);
+ }
+
+ @Test
+ public void testTaskThrowsUnrecognizedException() throws Exception {
+ // arrange
+ BlockWaitingModule m = new BlockWaitingModule();
+ Task t = mock(Task.class);
+ doThrow(new IllegalStateException("illegal state")).when(t).call();
+
+ // act
+ m.beforeCommand(env);
+ m.submit(t);
+
+ // assert
+ Throwable e = assertThrows(RuntimeException.class, m::afterCommand);
+ assertThat(e).hasCauseThat().isInstanceOf(ExecutionException.class);
+ assertThat(e).hasCauseThat().hasCauseThat().isInstanceOf(IllegalStateException.class);
+ assertThat(e).hasCauseThat().hasCauseThat().hasMessageThat().contains("illegal state");
+ }
+}