AsyncGenerator Cancellation
October 2, 2025 ยท View on GitHub
The AsyncGenerator provides a mechanism to cancel an ongoing iteration. This is particularly useful for long-running asynchronous sequences.
IsCancellable Interface
Cancellation is supported by generators that are compliant with the IsCancellable interface. This interface provides the core methods for cancellation:
interface IsCancellable {
boolean isCancelled();
boolean cancel(boolean mayInterruptIfRunning);
}
An AsyncGenerator can be made cancellable, for example, by wrapping it with AsyncGenerator.WithResult or by using a generator that extends AsyncGenerator.BaseCancellable.
Cancellation Behavior
The cancel(boolean mayInterruptIfRunning) method allows for two types of cancellation:
1. Graceful Cancellation
When you invoke cancel(false), the generator sets an internal "cancelled" flag to true. The iteration will not be immediately terminated. Instead, it will stop gracefully before processing the next element. This ensures that the current operation completes, but no new operations are started. This is useful when you want to allow the current asynchronous task to finish its work to avoid leaving the system in an inconsistent state.
2. Immediate Cancellation
Invoking cancel(true) also sets the internal "cancelled" flag. In addition, it attempts to interrupt the underlying thread that is executing the iteration. This is a more forceful cancellation and can be useful when you need to stop a long-running or blocked operation immediately. This will typically result in an InterruptedException being thrown within the task's execution block.
Threading and Iteration
The cancellation behavior is closely tied to how the AsyncGenerator is consumed.
Using forEachAsync(consumer)
When you use forEachAsync(consumer), the iteration is executed on a new, dedicated single-thread executor.
cancel(false)will cause the loop to terminate before the next element is processed.cancel(true)will interrupt the dedicated thread, causing theforEachAsyncCompletableFutureto complete exceptionally (often with anInterruptedException).
Example from asyncGeneratorForEachCancelTest:
final var data = List.of( "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9", "e10" );
final AsyncGenerator<String> it = AsyncGenerator.from(data.iterator());
final var cancellableIt = new AsyncGenerator.WithResult<>(it);
CompletableFuture.runAsync( () -> {
try {
Thread.sleep( 2000 );
cancellableIt.cancel(true); // Interrupt the thread
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
var futureResult = cancellableIt.forEachAsync( value -> {
try {
Thread.sleep( 500 );
forEachResult.add(value);
} catch (InterruptedException e) {
// The thread is interrupted here
Thread.currentThread().interrupt();
throw new CompletionException(e);
}
} ).exceptionally( throwable -> {
assertInstanceOf( InterruptedException.class, throwable.getCause());
return AsyncGenerator.Cancellable.CANCELLED;
});
Using iterator()
When you use the standard for-each loop with an AsyncGenerator (which uses the iterator() method), the iteration runs on the current thread. The iterator() blocks on each call to next() until the CompletableFuture for that element is resolved.
cancel(false)will causehasNext()to returnfalseon the next check, effectively stopping the loop.cancel(true)will also causehasNext()to returnfalse. Since the iteration is running on the calling thread, interrupting should not have any effect on the current thread.
How check if iteration has been interrupted
To check if an iteration has been interrupted, you can use the isCancelled() available on on your IsCancellable generator.
This method will return true if cancel() has been called, regardless of the mayInterruptIfRunning parameter.
if (cancellableGenerator.isCancelled()) {
// Logic to handle the cancellation
}
Summary
In summary, cancel(false) provides a non-disruptive way to signal termination, while cancel(true) offers a more immediate stop by leveraging thread interruption, which is most effective with forEachAsync.
Conclusion
We must understand that the cancellation is a cooperative game and to make it effective we must be aware of this. Anyway the async-generator library provides a base implementation to make this game easier to play.
Next Version - AbortController
n the next version we have planned tp implement an AbortController allowing to provide a way to interrupt asynchronous task that could spawn different threads