Context propagation mechanism for Java applications that enables carrying scoped values across API boundaries and between threads
—
Cancellable contexts with hierarchical cancellation propagation, listener notification, and cause tracking for managing the lifecycle of operations.
Create contexts that can be independently cancelled while also cascading cancellation from their parent. Callers must ensure that cancel() or close() are called to allow garbage collection.
/**
* Create a new context which is independently cancellable and cascades cancellation from its parent.
* @return CancellableContext that must be cancelled at some point
*/
public Context.CancellableContext withCancellation();Usage Example:
Context.CancellableContext cancellableContext = Context.current().withCancellation();
try {
cancellableContext.run(() -> {
Context current = Context.current();
while (!current.isCancelled()) {
// Keep working until cancelled
doWork();
}
});
} finally {
cancellableContext.cancel(null); // Always cancel to prevent memory leaks
}Check if a context has been cancelled and retrieve the cancellation cause.
/**
* Check if this context is cancelled.
* @return true if context is cancelled, false otherwise
*/
public boolean isCancelled();
/**
* Get the cause of cancellation if the context is cancelled.
* @return Throwable cause of cancellation, or null if not cancelled or no cause provided
*/
public Throwable cancellationCause();Usage Example:
Context.CancellableContext cancellableContext = Context.current().withCancellation();
// Check cancellation status
if (cancellableContext.isCancelled()) {
Throwable cause = cancellableContext.cancellationCause();
if (cause != null) {
System.out.println("Cancelled due to: " + cause.getMessage());
}
return; // Exit early if cancelled
}Cancel a context with an optional cause. It is safe to call cancel() multiple times - only the first call has any effect.
public static final class CancellableContext extends Context implements Closeable {
/**
* Cancel this context and optionally provide a cause for the cancellation.
* @param cause Optional cause for the cancellation (can be null)
* @return true if this call cancelled the context, false if already cancelled
*/
public boolean cancel(Throwable cause);
/**
* Cleans up this object by calling cancel(null). Equivalent to cancel(null).
*/
public void close();
/**
* Cancel this context and detach it as the current context.
* @param toAttach Context to make current after detaching
* @param cause Optional cause for cancellation
*/
public void detachAndCancel(Context toAttach, Throwable cause);
}Usage Examples:
// Basic cancellation
Context.CancellableContext ctx = Context.current().withCancellation();
boolean wasCancelled = ctx.cancel(new RuntimeException("Operation timeout"));
// Using try-with-resources (automatically calls close())
try (Context.CancellableContext ctx = Context.current().withCancellation()) {
Context toRestore = ctx.attach();
try {
performOperation();
} finally {
ctx.detach(toRestore);
}
} // Automatically cancelled on close
// Cancel and detach in one operation
Context.CancellableContext ctx = Context.current().withCancellation();
Context previous = ctx.attach();
try {
performOperation();
} finally {
ctx.detachAndCancel(previous, null);
}Add and remove listeners that will be notified when the context becomes cancelled. Listeners are executed on the provided executor.
/**
* Add a listener that will be notified when the context becomes cancelled.
* @param cancellationListener The listener to notify on cancellation
* @param executor The executor to run the listener on
*/
public void addListener(Context.CancellationListener cancellationListener, Executor executor);
/**
* Remove a previously added CancellationListener.
* @param cancellationListener The listener to remove
*/
public void removeListener(Context.CancellationListener cancellationListener);
/**
* A listener notified on context cancellation.
*/
public interface CancellationListener {
/**
* Notifies that a context was cancelled.
* @param context The newly cancelled context
*/
void cancelled(Context context);
}Usage Example:
Context.CancellableContext cancellableContext = Context.current().withCancellation();
Executor executor = Executors.newSingleThreadExecutor();
// Create a cancellation listener
Context.CancellationListener listener = new Context.CancellationListener() {
@Override
public void cancelled(Context context) {
System.out.println("Context was cancelled!");
Throwable cause = context.cancellationCause();
if (cause != null) {
System.out.println("Cancellation cause: " + cause.getMessage());
}
// Perform cleanup operations
cleanupResources();
}
};
// Add the listener
cancellableContext.addListener(listener, executor);
try {
// Perform work
cancellableContext.run(() -> {
doLongRunningWork();
});
} finally {
// Remove listener and cancel context
cancellableContext.removeListener(listener);
cancellableContext.cancel(null);
}Child contexts automatically inherit cancellation from their parents. When a parent context is cancelled, all descendant contexts are also cancelled.
Usage Example:
// Create parent cancellable context
Context.CancellableContext parentContext = Context.current().withCancellation();
parentContext.run(() -> {
// Create child context within parent scope
Context.CancellableContext childContext = Context.current().withCancellation();
childContext.run(() -> {
// This context will be cancelled when either childContext or parentContext is cancelled
Context current = Context.current();
while (!current.isCancelled()) {
doWork();
}
// Check which context was cancelled
if (childContext.isCancelled()) {
System.out.println("Child context was cancelled directly");
} else if (parentContext.isCancelled()) {
System.out.println("Parent context cancellation cascaded to child");
}
});
childContext.cancel(null);
});
parentContext.cancel(null);CancellableContext uses a surrogate for attachment to prevent external code from accessing cancellation methods.
/**
* Attach this cancellable context. Uses an internal surrogate to prevent
* external access to cancellation methods through Context.current().
* @return The previously current context
*/
@Override
public Context attach();
/**
* Detach this cancellable context, restoring the previous context.
* @param toAttach The context returned by the corresponding attach() call
*/
@Override
public void detach(Context toAttach);Usage Example:
Context.CancellableContext cancellableContext = Context.current().withCancellation();
Context previous = cancellableContext.attach();
try {
// Context.current() returns a non-cancellable view
Context current = Context.current();
assert !(current instanceof Context.CancellableContext);
// But cancellation status is still accessible
boolean isCancelled = current.isCancelled();
} finally {
cancellableContext.detach(previous);
}Install with Tessl CLI
npx tessl i tessl/maven-io-grpc--grpc-context