blob: da2bfb6e5a8888db1d52a46e663868c3737d0668 [file]
// Copyright 2026 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.concurrent;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.util.concurrent.Futures.immediateFuture;
import static com.google.devtools.build.lib.concurrent.safeexecutor.SafeExecutor.safeDirectExecutor;
import static java.util.concurrent.ForkJoinPool.commonPool;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.google.devtools.build.lib.concurrent.safeexecutor.RejectionHandlingRunnable;
import com.google.devtools.build.lib.concurrent.safeexecutor.SafeExecutor;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nullable;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public final class AccumulatingQuiescingFutureTest {
@Test
public void accumulateFutureResult_success_foldsResultAndDecrementsCount() throws Exception {
var future = new AccumulatingStringFuture();
var subtask1 = SettableFuture.<String>create();
var subtask2 = SettableFuture.<String>create();
future.addFuture(subtask1, safeDirectExecutor());
future.addFuture(subtask2, safeDirectExecutor());
future.finishRegistration();
assertThat(future.isDone()).isFalse();
subtask1.set("hello");
assertThat(future.isDone()).isFalse();
subtask2.set("world");
assertThat(future.isDone()).isTrue();
assertThat(future.get()).containsExactly("hello", "world").inOrder();
}
@Test
public void accumulateFutureResult_throwsException_recordsExceptionAndDecrementsCount()
throws Exception {
AtomicBoolean doneWithErrorCalled = new AtomicBoolean(false);
var expectedError = new RuntimeException("failed during accumulateFutureResult");
var future =
new AccumulatingQuiescingFuture<String, String>(safeDirectExecutor()) {
@Override
protected void accumulateFutureResult(String result) {
throw expectedError;
}
@Override
protected String getValue() {
return "unused";
}
@Override
protected void doneWithError(
@Nullable Throwable primaryCause, ImmutableList<Throwable> secondaryCauses) {
doneWithErrorCalled.set(true);
}
};
var subtask = SettableFuture.<String>create();
future.addFuture(subtask, safeDirectExecutor());
future.finishRegistration();
subtask.set("trigger");
assertThat(future.isDone()).isTrue();
assertThat(doneWithErrorCalled.get()).isTrue();
var thrown = assertThrows(ExecutionException.class, future::get);
assertThat(thrown).hasCauseThat().isSameInstanceAs(expectedError);
}
@Test
public void onFailure_recordsExceptionAndDecrementsCount() throws Exception {
AtomicBoolean doneWithErrorCalled = new AtomicBoolean(false);
var expectedError = new RuntimeException("subtask failed");
var future =
new AccumulatingQuiescingFuture<String, String>(safeDirectExecutor()) {
@Override
protected void accumulateFutureResult(String result) {}
@Override
protected String getValue() {
return "unused";
}
@Override
protected void doneWithError(
@Nullable Throwable primaryCause, ImmutableList<Throwable> secondaryCauses) {
doneWithErrorCalled.set(true);
}
};
var subtask = SettableFuture.<String>create();
future.addFuture(subtask, safeDirectExecutor());
future.finishRegistration();
subtask.setException(expectedError);
assertThat(future.isDone()).isTrue();
assertThat(doneWithErrorCalled.get()).isTrue();
var thrown = assertThrows(ExecutionException.class, future::get);
assertThat(thrown).hasCauseThat().isSameInstanceAs(expectedError);
}
@Test
public void addFuture_rejection_rollsBackCountAndCompletesExceptionally() throws Exception {
AtomicBoolean doneWithErrorCalled = new AtomicBoolean(false);
var rejectionException = new RejectedExecutionException("callback rejected");
SafeExecutor rejectingExecutor =
new SafeExecutor() {
@Override
public void execute(RejectionHandlingRunnable task) {
task.handleRejection(rejectionException);
}
@Override
public <T> void addCallback(
ListenableFuture<T> future, FutureCallback<? super T> callback) {
throw rejectionException;
}
@Override
public Executor getInternalUnsafeExecutor() {
return null;
}
};
var future =
new AccumulatingQuiescingFuture<String, String>(safeDirectExecutor()) {
@Override
protected void accumulateFutureResult(String result) {}
@Override
protected String getValue() {
return "result";
}
@Override
protected void doneWithError(
@Nullable Throwable primaryCause, ImmutableList<Throwable> secondaryCauses) {
doneWithErrorCalled.set(true);
}
};
var subtask = SettableFuture.<String>create();
future.addFuture(subtask, rejectingExecutor);
future.finishRegistration();
assertThat(future.isDone()).isTrue();
assertThat(doneWithErrorCalled.get()).isTrue();
var thrown = assertThrows(ExecutionException.class, future::get);
assertThat(thrown).hasCauseThat().isSameInstanceAs(rejectionException);
}
@SuppressWarnings("AssertThrowsMinimizer") // safeDirectExecutor() does not throw exceptions
@Test
public void addFuture_nullFuture_throwsNullPointerException() {
var future = new AccumulatingStringFuture();
assertThrows(NullPointerException.class, () -> future.addFuture(null, safeDirectExecutor()));
}
@Test
public void addFuture_nullExecutor_throwsNullPointerException() {
var future = new AccumulatingStringFuture();
var subtask = SettableFuture.<String>create();
assertThrows(NullPointerException.class, () -> future.addFuture(subtask, null));
}
@Test
public void multipleSubtasks_concurrentCompletion() throws Exception {
var future = new AccumulatingStringFuture();
int count = 50;
List<SettableFuture<String>> subtasks = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
var subtask = SettableFuture.<String>create();
subtasks.add(subtask);
future.addFuture(subtask, safeDirectExecutor());
}
future.finishRegistration();
var latch = new CountDownLatch(count);
for (int i = 0; i < count; i++) {
final int index = i;
commonPool()
.execute(
() -> {
subtasks.get(index).set("item-" + index);
latch.countDown();
});
}
latch.await();
assertThat(future.isDone()).isTrue();
assertThat(future.get()).hasSize(count);
}
@Test
public void multipleSubtasks_multipleFailures_preservesSecondaryExceptions() throws Exception {
AtomicReference<Throwable> capturedPrimary = new AtomicReference<>();
AtomicReference<ImmutableList<Throwable>> capturedSecondaries = new AtomicReference<>();
var future =
new AccumulatingQuiescingFuture<String, String>(safeDirectExecutor()) {
@Override
protected void accumulateFutureResult(String result) {}
@Override
protected String getValue() {
return "unused";
}
@Override
protected void doneWithError(
@Nullable Throwable primaryCause, ImmutableList<Throwable> secondaryCauses) {
capturedPrimary.set(primaryCause);
capturedSecondaries.set(secondaryCauses);
}
};
var error1 = new RuntimeException("error1");
var error2 = new RuntimeException("error2");
var subtask1 = SettableFuture.<String>create();
var subtask2 = SettableFuture.<String>create();
future.addFuture(subtask1, safeDirectExecutor());
future.addFuture(subtask2, safeDirectExecutor());
future.finishRegistration();
subtask1.setException(error1);
subtask2.setException(error2);
assertThat(future.isDone()).isTrue();
assertThat(capturedPrimary.get()).isSameInstanceAs(error1);
assertThat(capturedSecondaries.get()).containsExactly(error2);
var thrown = assertThrows(ExecutionException.class, future::get);
assertThat(thrown).hasCauseThat().isSameInstanceAs(error1);
}
@Test
public void immediateFutures_resolveImmediately() throws Exception {
var future = new AccumulatingStringFuture();
future.addFuture(immediateFuture("a"), safeDirectExecutor());
future.addFuture(immediateFuture("b"), safeDirectExecutor());
future.finishRegistration();
assertThat(future.isDone()).isTrue();
assertThat(future.get()).containsExactly("a", "b").inOrder();
}
@Test
public void accumulateFuture_cancelledSubtask_cancelsAccumulatingFuture() throws Exception {
var future = new AccumulatingStringFuture();
var subtask1 = SettableFuture.<String>create();
var subtask2 = SettableFuture.<String>create();
future.addFuture(subtask1, safeDirectExecutor());
future.addFuture(subtask2, safeDirectExecutor());
future.finishRegistration();
assertThat(future.isDone()).isFalse();
subtask1.cancel(/* mayInterruptIfRunning= */ false);
assertThat(future.isDone()).isTrue();
assertThat(future.isCancelled()).isTrue();
assertThrows(CancellationException.class, future::get);
}
private static final class AccumulatingStringFuture
extends AccumulatingQuiescingFuture<ImmutableList<String>, String> {
private final ConcurrentLinkedQueue<String> collected = new ConcurrentLinkedQueue<>();
private AccumulatingStringFuture() {
super(safeDirectExecutor());
}
@Override
protected void accumulateFutureResult(String result) {
collected.add(result);
}
@Override
protected ImmutableList<String> getValue() {
return ImmutableList.copyOf(collected);
}
}
}