| // 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.devtools.build.lib.testutil.TestUtils.WAIT_TIMEOUT_MILLISECONDS; | |
| import static com.google.devtools.build.lib.testutil.TestUtils.WAIT_TIMEOUT_SECONDS; | |
| import static java.util.concurrent.TimeUnit.MILLISECONDS; | |
| import static java.util.concurrent.TimeUnit.SECONDS; | |
| import static org.junit.Assert.assertThrows; | |
| import com.google.devtools.build.lib.testutil.TestThread; | |
| import com.google.testing.junit.testparameterinjector.TestParameter; | |
| import com.google.testing.junit.testparameterinjector.TestParameterInjector; | |
| import java.io.IOException; | |
| import java.util.ArrayList; | |
| import java.util.concurrent.CyclicBarrier; | |
| import java.util.concurrent.Semaphore; | |
| import java.util.concurrent.atomic.AtomicBoolean; | |
| import java.util.concurrent.atomic.AtomicInteger; | |
| import java.util.concurrent.atomic.AtomicReference; | |
| import org.junit.Test; | |
| import org.junit.runner.RunWith; | |
| /** Tests for {@link CancellableTask}. */ | |
| @RunWith(TestParameterInjector.class) | |
| public final class CancellableTaskTest { | |
| /** The cancellation entry point under test, which must not affect the cancellation semantics. */ | |
| private enum CancelVariant { | |
| INTERRUPTIBLE { | |
| @Override | |
| void cancel(CancellableTask<?> task) throws InterruptedException { | |
| task.cancelAndAwait(); | |
| } | |
| }, | |
| REQUEST_THEN_AWAIT { | |
| @Override | |
| void cancel(CancellableTask<?> task) throws InterruptedException { | |
| task.requestCancellation(); | |
| task.awaitCompletion(); | |
| } | |
| }; | |
| abstract void cancel(CancellableTask<?> task) throws InterruptedException; | |
| } | |
| @Test | |
| public void runTwice_throws() throws Exception { | |
| var task = new CancellableTask<>(() -> {}); | |
| assertThat(task.runIfNotCancelled()).isTrue(); | |
| assertThrows(IllegalStateException.class, task::runIfNotCancelled); | |
| } | |
| @Test | |
| public void cancelBeforeRun_preventsTaskFromRunning(@TestParameter CancelVariant variant) | |
| throws Exception { | |
| var ran = new AtomicBoolean(); | |
| var task = new CancellableTask<>(() -> ran.set(true)); | |
| variant.cancel(task); | |
| assertThat(task.runIfNotCancelled()).isFalse(); | |
| assertThat(ran.get()).isFalse(); | |
| } | |
| @Test | |
| public void cancelTwiceBeforeRun_returnsWithoutBlocking(@TestParameter CancelVariant variant) | |
| throws Exception { | |
| var task = new CancellableTask<>(() -> {}); | |
| variant.cancel(task); | |
| variant.cancel(task); | |
| } | |
| @Test | |
| public void cancelDuringRun_interruptsAndAwaitsTask(@TestParameter CancelVariant variant) | |
| throws Exception { | |
| var taskStarted = new Semaphore(0); | |
| var taskInterrupted = new Semaphore(0); | |
| var taskMayFinish = new Semaphore(0); | |
| var cancellationReturned = new Semaphore(0); | |
| var task = | |
| new CancellableTask<>( | |
| () -> { | |
| taskStarted.release(); | |
| try { | |
| taskMayFinish.acquire(); | |
| } catch (InterruptedException e) { | |
| taskInterrupted.release(); | |
| taskMayFinish.acquireUninterruptibly(); | |
| } | |
| }); | |
| var runner = new TestThread(() -> assertThat(task.runIfNotCancelled()).isTrue()); | |
| runner.start(); | |
| assertThat(taskStarted.tryAcquire(WAIT_TIMEOUT_SECONDS, SECONDS)).isTrue(); | |
| var canceller = | |
| new TestThread( | |
| () -> { | |
| variant.cancel(task); | |
| cancellationReturned.release(); | |
| }); | |
| canceller.start(); | |
| try { | |
| assertThat(cancellationReturned.tryAcquire(100, MILLISECONDS)).isFalse(); | |
| assertThat(taskInterrupted.tryAcquire(WAIT_TIMEOUT_SECONDS, SECONDS)).isTrue(); | |
| } finally { | |
| taskMayFinish.release(); | |
| } | |
| assertThat(cancellationReturned.tryAcquire(WAIT_TIMEOUT_SECONDS, SECONDS)).isTrue(); | |
| runner.joinAndAssertState(WAIT_TIMEOUT_MILLISECONDS); | |
| canceller.joinAndAssertState(WAIT_TIMEOUT_MILLISECONDS); | |
| } | |
| @Test | |
| public void cancelAfterRun_returnsWithoutBlocking(@TestParameter CancelVariant variant) | |
| throws Exception { | |
| var task = new CancellableTask<>(() -> {}); | |
| assertThat(task.runIfNotCancelled()).isTrue(); | |
| variant.cancel(task); | |
| // Repeated cancellation is allowed and must not block either. | |
| variant.cancel(task); | |
| } | |
| @Test | |
| public void completion_runsOnceAfterBody() throws Exception { | |
| var events = new ArrayList<String>(); | |
| var task = new CancellableTask<>(() -> events.add("body"), () -> events.add("completion")); | |
| assertThat(task.runIfNotCancelled()).isTrue(); | |
| assertThat(events).containsExactly("body", "completion").inOrder(); | |
| // Cancellation after completion must not run the completion action again. | |
| task.cancelAndAwait(); | |
| assertThat(events).containsExactly("body", "completion").inOrder(); | |
| } | |
| @Test | |
| public void completion_runsWhenBodyThrows() { | |
| var completionRuns = new AtomicInteger(); | |
| var task = | |
| new CancellableTask<>( | |
| () -> { | |
| throw new IOException("task failed"); | |
| }, | |
| completionRuns::incrementAndGet); | |
| assertThrows(IOException.class, task::runIfNotCancelled); | |
| assertThat(completionRuns.get()).isEqualTo(1); | |
| } | |
| @Test | |
| public void cancelBeforeRun_runsCompletionExactlyOnce(@TestParameter CancelVariant variant) | |
| throws Exception { | |
| var completionRuns = new AtomicInteger(); | |
| var task = new CancellableTask<>(() -> {}, completionRuns::incrementAndGet); | |
| variant.cancel(task); | |
| assertThat(completionRuns.get()).isEqualTo(1); | |
| // Neither a repeated cancellation nor the prevented run runs the completion action again. | |
| variant.cancel(task); | |
| assertThat(task.runIfNotCancelled()).isFalse(); | |
| assertThat(completionRuns.get()).isEqualTo(1); | |
| } | |
| @Test | |
| public void cancelBeforeRun_concurrentCancellerAwaitsCompletion( | |
| @TestParameter CancelVariant variant) throws Exception { | |
| var completionStarted = new Semaphore(0); | |
| var completionMayFinish = new Semaphore(0); | |
| var task = | |
| new CancellableTask<>( | |
| () -> {}, | |
| () -> { | |
| completionStarted.release(); | |
| completionMayFinish.acquireUninterruptibly(); | |
| }); | |
| var winner = new TestThread(() -> variant.cancel(task)); | |
| winner.start(); | |
| assertThat(completionStarted.tryAcquire(WAIT_TIMEOUT_SECONDS, SECONDS)).isTrue(); | |
| var loserReturned = new Semaphore(0); | |
| var loser = | |
| new TestThread( | |
| () -> { | |
| variant.cancel(task); | |
| loserReturned.release(); | |
| }); | |
| loser.start(); | |
| try { | |
| // The losing canceler must not return while the winner's completion action is running. | |
| assertThat(loserReturned.tryAcquire(100, MILLISECONDS)).isFalse(); | |
| } finally { | |
| completionMayFinish.release(); | |
| } | |
| assertThat(loserReturned.tryAcquire(WAIT_TIMEOUT_SECONDS, SECONDS)).isTrue(); | |
| winner.joinAndAssertState(WAIT_TIMEOUT_MILLISECONDS); | |
| loser.joinAndAssertState(WAIT_TIMEOUT_MILLISECONDS); | |
| } | |
| @Test | |
| public void cancelDuringCompletion_awaitsWithoutInterruptingCompletion( | |
| @TestParameter CancelVariant variant) throws Exception { | |
| var completionStarted = new Semaphore(0); | |
| var completionMayFinish = new Semaphore(0); | |
| var completionWasInterrupted = new AtomicBoolean(); | |
| var task = | |
| new CancellableTask<>( | |
| () -> {}, | |
| () -> { | |
| completionStarted.release(); | |
| try { | |
| completionMayFinish.acquire(); | |
| } catch (InterruptedException e) { | |
| completionWasInterrupted.set(true); | |
| } | |
| }); | |
| var runner = new TestThread(() -> assertThat(task.runIfNotCancelled()).isTrue()); | |
| runner.start(); | |
| assertThat(completionStarted.tryAcquire(WAIT_TIMEOUT_SECONDS, SECONDS)).isTrue(); | |
| var cancellationReturned = new Semaphore(0); | |
| var canceller = | |
| new TestThread( | |
| () -> { | |
| variant.cancel(task); | |
| cancellationReturned.release(); | |
| }); | |
| canceller.start(); | |
| try { | |
| // Completion is part of the terminal lifecycle, but cancellation only interrupts the body. | |
| assertThat(cancellationReturned.tryAcquire(100, MILLISECONDS)).isFalse(); | |
| assertThat(completionWasInterrupted.get()).isFalse(); | |
| } finally { | |
| completionMayFinish.release(); | |
| } | |
| assertThat(cancellationReturned.tryAcquire(WAIT_TIMEOUT_SECONDS, SECONDS)).isTrue(); | |
| runner.joinAndAssertState(WAIT_TIMEOUT_MILLISECONDS); | |
| canceller.joinAndAssertState(WAIT_TIMEOUT_MILLISECONDS); | |
| assertThat(completionWasInterrupted.get()).isFalse(); | |
| } | |
| @Test | |
| public void runLosingToCancelBeforeStart_awaitsCompletion(@TestParameter CancelVariant variant) | |
| throws Exception { | |
| var completionStarted = new Semaphore(0); | |
| var completionMayFinish = new Semaphore(0); | |
| var task = | |
| new CancellableTask<>( | |
| () -> {}, | |
| () -> { | |
| completionStarted.release(); | |
| completionMayFinish.acquireUninterruptibly(); | |
| }); | |
| var canceller = new TestThread(() -> variant.cancel(task)); | |
| canceller.start(); | |
| assertThat(completionStarted.tryAcquire(WAIT_TIMEOUT_SECONDS, SECONDS)).isTrue(); | |
| var runReturned = new Semaphore(0); | |
| var ran = new AtomicBoolean(true); | |
| var runner = | |
| new TestThread( | |
| () -> { | |
| ran.set(task.runIfNotCancelled()); | |
| runReturned.release(); | |
| }); | |
| runner.start(); | |
| try { | |
| assertThat(runReturned.tryAcquire(100, MILLISECONDS)).isFalse(); | |
| } finally { | |
| completionMayFinish.release(); | |
| } | |
| assertThat(runReturned.tryAcquire(WAIT_TIMEOUT_SECONDS, SECONDS)).isTrue(); | |
| runner.joinAndAssertState(WAIT_TIMEOUT_MILLISECONDS); | |
| canceller.joinAndAssertState(WAIT_TIMEOUT_MILLISECONDS); | |
| assertThat(ran.get()).isFalse(); | |
| } | |
| @Test | |
| public void taskThrows_propagatesExceptionAndUnblocksCanceller( | |
| @TestParameter CancelVariant variant) throws Exception { | |
| var taskStarted = new Semaphore(0); | |
| var taskMayThrow = new Semaphore(0); | |
| var task = | |
| new CancellableTask<IOException>( | |
| () -> { | |
| taskStarted.release(); | |
| taskMayThrow.acquireUninterruptibly(); | |
| throw new IOException("task failed"); | |
| }); | |
| var thrown = new AtomicReference<IOException>(); | |
| var runner = | |
| new TestThread(() -> thrown.set(assertThrows(IOException.class, task::runIfNotCancelled))); | |
| runner.start(); | |
| assertThat(taskStarted.tryAcquire(WAIT_TIMEOUT_SECONDS, SECONDS)).isTrue(); | |
| var cancellationReturned = new Semaphore(0); | |
| var canceller = | |
| new TestThread( | |
| () -> { | |
| variant.cancel(task); | |
| cancellationReturned.release(); | |
| }); | |
| canceller.start(); | |
| try { | |
| assertThat(cancellationReturned.tryAcquire(100, MILLISECONDS)).isFalse(); | |
| } finally { | |
| taskMayThrow.release(); | |
| } | |
| // A task that leaves its body by throwing must still unblock cancelers. | |
| assertThat(cancellationReturned.tryAcquire(WAIT_TIMEOUT_SECONDS, SECONDS)).isTrue(); | |
| runner.joinAndAssertState(WAIT_TIMEOUT_MILLISECONDS); | |
| canceller.joinAndAssertState(WAIT_TIMEOUT_MILLISECONDS); | |
| assertThat(thrown.get()).hasMessageThat().isEqualTo("task failed"); | |
| } | |
| @Test | |
| public void cancelFromTaskBody_throws(@TestParameter CancelVariant variant) throws Exception { | |
| var taskRef = new AtomicReference<CancellableTask<Exception>>(); | |
| var task = | |
| new CancellableTask<Exception>( | |
| () -> assertThrows(IllegalStateException.class, () -> variant.cancel(taskRef.get()))); | |
| taskRef.set(task); | |
| assertThat(task.runIfNotCancelled()).isTrue(); | |
| } | |
| @Test | |
| public void cancelFromCompletionAfterBody_throws(@TestParameter CancelVariant variant) | |
| throws Exception { | |
| var taskRef = new AtomicReference<CancellableTask<Exception>>(); | |
| var task = | |
| new CancellableTask<Exception>( | |
| () -> {}, | |
| () -> assertThrows(IllegalStateException.class, () -> variant.cancel(taskRef.get()))); | |
| taskRef.set(task); | |
| var runner = new TestThread(() -> assertThat(task.runIfNotCancelled()).isTrue()); | |
| runner.start(); | |
| runner.joinAndAssertState(WAIT_TIMEOUT_MILLISECONDS); | |
| } | |
| @Test | |
| public void cancelFromCancelBeforeStartCompletion_throws(@TestParameter CancelVariant variant) | |
| throws Exception { | |
| var taskRef = new AtomicReference<CancellableTask<Exception>>(); | |
| var task = | |
| new CancellableTask<Exception>( | |
| () -> {}, | |
| () -> assertThrows(IllegalStateException.class, () -> variant.cancel(taskRef.get()))); | |
| taskRef.set(task); | |
| var canceller = new TestThread(() -> variant.cancel(task)); | |
| canceller.start(); | |
| canceller.joinAndAssertState(WAIT_TIMEOUT_MILLISECONDS); | |
| assertThat(task.runIfNotCancelled()).isFalse(); | |
| } | |
| @Test | |
| public void runFromCancelBeforeStartCompletion_throws() throws Exception { | |
| var taskRef = new AtomicReference<CancellableTask<Exception>>(); | |
| var task = | |
| new CancellableTask<Exception>( | |
| () -> {}, | |
| () -> | |
| assertThrows(IllegalStateException.class, () -> taskRef.get().runIfNotCancelled())); | |
| taskRef.set(task); | |
| var canceller = new TestThread(task::cancelAndAwait); | |
| canceller.start(); | |
| canceller.joinAndAssertState(WAIT_TIMEOUT_MILLISECONDS); | |
| } | |
| @Test | |
| public void completionThrowsAfterBody_stillSignalsTerminalState( | |
| @TestParameter CancelVariant variant) throws Exception { | |
| var task = | |
| new CancellableTask<>( | |
| () -> {}, | |
| () -> { | |
| throw new IllegalStateException("completion failed"); | |
| }); | |
| assertThat(assertThrows(IllegalStateException.class, task::runIfNotCancelled)) | |
| .hasMessageThat() | |
| .isEqualTo("completion failed"); | |
| assertLaterCancellationReturns(task, variant); | |
| } | |
| @Test | |
| public void completionThrowsAfterCancelBeforeStart_stillSignalsTerminalState( | |
| @TestParameter CancelVariant variant) throws Exception { | |
| var task = | |
| new CancellableTask<>( | |
| () -> {}, | |
| () -> { | |
| throw new IllegalStateException("completion failed"); | |
| }); | |
| assertThat(assertThrows(IllegalStateException.class, () -> variant.cancel(task))) | |
| .hasMessageThat() | |
| .isEqualTo("completion failed"); | |
| assertLaterCancellationReturns(task, variant); | |
| assertThat(task.runIfNotCancelled()).isFalse(); | |
| } | |
| private static void assertLaterCancellationReturns(CancellableTask<?> task, CancelVariant variant) | |
| throws Exception { | |
| var cancellationReturned = new Semaphore(0); | |
| var canceller = | |
| new TestThread( | |
| () -> { | |
| variant.cancel(task); | |
| cancellationReturned.release(); | |
| }); | |
| canceller.start(); | |
| assertThat(cancellationReturned.tryAcquire(WAIT_TIMEOUT_SECONDS, SECONDS)).isTrue(); | |
| canceller.joinAndAssertState(WAIT_TIMEOUT_MILLISECONDS); | |
| } | |
| @Test | |
| public void requestCancellation_beforeRun_preventsStartAndRunsCompletion() throws Exception { | |
| var ran = new AtomicBoolean(); | |
| var completionRuns = new AtomicInteger(); | |
| var task = new CancellableTask<>(() -> ran.set(true), completionRuns::incrementAndGet); | |
| task.requestCancellation(); | |
| assertThat(completionRuns.get()).isEqualTo(1); | |
| assertThat(task.runIfNotCancelled()).isFalse(); | |
| assertThat(ran.get()).isFalse(); | |
| assertThat(completionRuns.get()).isEqualTo(1); | |
| } | |
| @Test | |
| public void requestCancellation_duringRun_interruptsWithoutAwaiting() throws Exception { | |
| var taskStarted = new Semaphore(0); | |
| var taskInterrupted = new Semaphore(0); | |
| var taskMayFinish = new Semaphore(0); | |
| var task = | |
| new CancellableTask<>( | |
| () -> { | |
| taskStarted.release(); | |
| try { | |
| taskMayFinish.acquire(); | |
| } catch (InterruptedException e) { | |
| taskInterrupted.release(); | |
| taskMayFinish.acquireUninterruptibly(); | |
| } | |
| }); | |
| var runner = new TestThread(() -> assertThat(task.runIfNotCancelled()).isTrue()); | |
| runner.start(); | |
| assertThat(taskStarted.tryAcquire(WAIT_TIMEOUT_SECONDS, SECONDS)).isTrue(); | |
| // Returns while the body is still running: it only finishes after the release below. | |
| task.requestCancellation(); | |
| assertThat(taskInterrupted.tryAcquire(WAIT_TIMEOUT_SECONDS, SECONDS)).isTrue(); | |
| taskMayFinish.release(); | |
| runner.joinAndAssertState(WAIT_TIMEOUT_MILLISECONDS); | |
| } | |
| @Test | |
| public void requestCancellation_thenAwaitCompletion_doesNotInterruptTaskAgain() throws Exception { | |
| var taskStarted = new Semaphore(0); | |
| var cleanupStarted = new Semaphore(0); | |
| var cleanupMayFinish = new Semaphore(0); | |
| var interruptionCount = new AtomicInteger(); | |
| var task = | |
| new CancellableTask<>( | |
| () -> { | |
| taskStarted.release(); | |
| try { | |
| new Semaphore(0).acquire(); | |
| } catch (InterruptedException e) { | |
| interruptionCount.incrementAndGet(); | |
| } | |
| cleanupStarted.release(); | |
| try { | |
| cleanupMayFinish.acquire(); | |
| } catch (InterruptedException e) { | |
| interruptionCount.incrementAndGet(); | |
| } | |
| }); | |
| var runner = new TestThread(() -> assertThat(task.runIfNotCancelled()).isTrue()); | |
| runner.start(); | |
| assertThat(taskStarted.tryAcquire(WAIT_TIMEOUT_SECONDS, SECONDS)).isTrue(); | |
| task.requestCancellation(); | |
| assertThat(cleanupStarted.tryAcquire(WAIT_TIMEOUT_SECONDS, SECONDS)).isTrue(); | |
| var waiter = new TestThread(task::awaitCompletion); | |
| waiter.start(); | |
| try { | |
| // Awaiting a previously requested cancellation must not send another interrupt that could | |
| // abort the task's cleanup. | |
| assertThat(waiter.isAlive()).isTrue(); | |
| assertThat(interruptionCount.get()).isEqualTo(1); | |
| } finally { | |
| cleanupMayFinish.release(); | |
| } | |
| runner.joinAndAssertState(WAIT_TIMEOUT_MILLISECONDS); | |
| waiter.joinAndAssertState(WAIT_TIMEOUT_MILLISECONDS); | |
| assertThat(interruptionCount.get()).isEqualTo(1); | |
| } | |
| @Test | |
| public void cancelAndAwait_interruptedWhileAwaiting_retryInterruptsAgainAndAwaits() | |
| throws Exception { | |
| var taskStarted = new Semaphore(0); | |
| var taskMayFinish = new Semaphore(0); | |
| var taskInterruptions = new Semaphore(0); | |
| var interruptionCount = new AtomicInteger(); | |
| var task = | |
| new CancellableTask<>( | |
| () -> { | |
| taskStarted.release(); | |
| while (true) { | |
| try { | |
| taskMayFinish.acquire(); | |
| return; | |
| } catch (InterruptedException e) { | |
| interruptionCount.incrementAndGet(); | |
| taskInterruptions.release(); | |
| } | |
| } | |
| }); | |
| var runner = new TestThread(() -> assertThat(task.runIfNotCancelled()).isTrue()); | |
| runner.start(); | |
| assertThat(taskStarted.tryAcquire(WAIT_TIMEOUT_SECONDS, SECONDS)).isTrue(); | |
| var cancellerThrew = new Semaphore(0); | |
| var cancellationReturned = new Semaphore(0); | |
| var canceller = | |
| new TestThread( | |
| () -> { | |
| assertThrows(InterruptedException.class, task::cancelAndAwait); | |
| cancellerThrew.release(); | |
| // A retry interrupts the still-running task again and awaits quiescence. | |
| task.cancelAndAwait(); | |
| cancellationReturned.release(); | |
| }); | |
| canceller.start(); | |
| assertThat(taskInterruptions.tryAcquire(WAIT_TIMEOUT_SECONDS, SECONDS)).isTrue(); | |
| canceller.interrupt(); | |
| assertThat(cancellerThrew.tryAcquire(WAIT_TIMEOUT_SECONDS, SECONDS)).isTrue(); | |
| assertThat(taskInterruptions.tryAcquire(WAIT_TIMEOUT_SECONDS, SECONDS)).isTrue(); | |
| try { | |
| assertThat(cancellationReturned.tryAcquire(100, MILLISECONDS)).isFalse(); | |
| assertThat(interruptionCount.get()).isEqualTo(2); | |
| } finally { | |
| taskMayFinish.release(); | |
| } | |
| assertThat(cancellationReturned.tryAcquire(WAIT_TIMEOUT_SECONDS, SECONDS)).isTrue(); | |
| runner.joinAndAssertState(WAIT_TIMEOUT_MILLISECONDS); | |
| canceller.joinAndAssertState(WAIT_TIMEOUT_MILLISECONDS); | |
| assertThat(interruptionCount.get()).isEqualTo(2); | |
| } | |
| @Test | |
| public void runAfterCancel_whileInterrupted_restoresInterruptBit() throws Exception { | |
| var task = new CancellableTask<>(() -> {}); | |
| task.cancelAndAwait(); | |
| Thread.currentThread().interrupt(); | |
| assertThat(task.runIfNotCancelled()).isFalse(); | |
| assertThat(Thread.interrupted()).isTrue(); | |
| } | |
| @Test | |
| public void concurrentRunAndCancel_exactlyOneClaimWins() throws Exception { | |
| for (int i = 0; i < 1000; i++) { | |
| var bodyRuns = new AtomicInteger(); | |
| var bodyRunning = new AtomicBoolean(); | |
| var completionRuns = new AtomicInteger(); | |
| var task = | |
| new CancellableTask<>( | |
| () -> { | |
| bodyRunning.set(true); | |
| bodyRuns.incrementAndGet(); | |
| bodyRunning.set(false); | |
| }, | |
| completionRuns::incrementAndGet); | |
| var barrier = new CyclicBarrier(3); | |
| var ran = new AtomicBoolean(); | |
| var runner = | |
| new TestThread( | |
| () -> { | |
| barrier.await(); | |
| ran.set(task.runIfNotCancelled()); | |
| }); | |
| var cancellers = new TestThread[2]; | |
| for (int c = 0; c < cancellers.length; c++) { | |
| cancellers[c] = | |
| new TestThread( | |
| () -> { | |
| barrier.await(); | |
| task.cancelAndAwait(); | |
| // A normal return from cancellation guarantees that the body is not running | |
| // and that the completion action has finished. | |
| assertThat(bodyRunning.get()).isFalse(); | |
| assertThat(completionRuns.get()).isEqualTo(1); | |
| }); | |
| } | |
| // Rotate which thread arrives at the barrier last: that thread trips the barrier and | |
| // proceeds without parking while the others are still being woken, so it tends to win the | |
| // claim. Rotation makes both outcomes of the race common. | |
| var startOrder = | |
| switch (i % 3) { | |
| case 0 -> new TestThread[] {runner, cancellers[0], cancellers[1]}; | |
| case 1 -> new TestThread[] {cancellers[0], cancellers[1], runner}; | |
| default -> new TestThread[] {cancellers[0], runner, cancellers[1]}; | |
| }; | |
| for (var thread : startOrder) { | |
| thread.start(); | |
| } | |
| runner.joinAndAssertState(WAIT_TIMEOUT_MILLISECONDS); | |
| for (var canceller : cancellers) { | |
| canceller.joinAndAssertState(WAIT_TIMEOUT_MILLISECONDS); | |
| } | |
| // The claim is atomic: either the task ran to completion exactly once and no canceler | |
| // prevented it, or cancellation prevented it from running. | |
| assertThat(ran.get()).isEqualTo(bodyRuns.get() == 1); | |
| assertThat(bodyRuns.get()).isAtMost(1); | |
| assertThat(completionRuns.get()).isEqualTo(1); | |
| } | |
| } | |
| } |