Context propagation mechanism for Java applications that enables carrying scoped values across API boundaries and between threads
—
Utilities for wrapping Runnables, Callables, and Executors to automatically propagate context across thread boundaries and asynchronous operations.
Execute code immediately within a specific context scope. These methods handle attach/detach automatically.
/**
* Immediately run a Runnable with this context as the current context.
* @param r Runnable to run
*/
public void run(Runnable r);
/**
* Immediately call a Callable with this context as the current context.
* @param c Callable to call
* @return Result of the callable
* @throws Exception If the callable throws an exception
*/
public <V> V call(Callable<V> c) throws Exception;Usage Examples:
Context.Key<String> USER_KEY = Context.key("user");
Context withUser = Context.current().withValue(USER_KEY, "alice");
// Run a task with the context
withUser.run(() -> {
String user = USER_KEY.get(); // "alice"
processUserRequest(user);
});
// Call a task that returns a value
String result = withUser.call(() -> {
String user = USER_KEY.get(); // "alice"
return fetchUserData(user);
});
// Exception handling with call()
try {
Integer count = withUser.call(() -> {
return performCalculation();
});
} catch (Exception e) {
System.out.println("Calculation failed: " + e.getMessage());
}Wrap Runnables and Callables so they execute with a specific context, useful for passing tasks to other threads or executors.
/**
* Wrap a Runnable so that it executes with this context as the current context.
* @param r Runnable to wrap
* @return Wrapped Runnable that propagates this context
*/
public Runnable wrap(Runnable r);
/**
* Wrap a Callable so that it executes with this context as the current context.
* @param c Callable to wrap
* @return Wrapped Callable that propagates this context
*/
public <C> Callable<C> wrap(Callable<C> c);Usage Examples:
Context.Key<String> REQUEST_ID_KEY = Context.key("requestId");
Context withRequestId = Context.current().withValue(REQUEST_ID_KEY, "req-123");
ExecutorService executor = Executors.newFixedThreadPool(4);
// Wrap a Runnable for execution in another thread
Runnable task = withRequestId.wrap(() -> {
String requestId = REQUEST_ID_KEY.get(); // "req-123"
processRequest(requestId);
});
// Submit to executor - context will be propagated
executor.submit(task);
// Wrap a Callable for execution in another thread
Callable<String> computation = withRequestId.wrap(() -> {
String requestId = REQUEST_ID_KEY.get(); // "req-123"
return performComputation(requestId);
});
// Submit and get result - context was propagated
Future<String> result = executor.submit(computation);
String computationResult = result.get();Wrap an Executor so that all tasks submitted to it execute with a specific context. The context is fixed at creation time.
/**
* Wrap an Executor so that it always executes with this context as the current context.
* @param e Executor to wrap
* @return Wrapped Executor that propagates this context for all tasks
*/
public Executor fixedContextExecutor(Executor e);Usage Example:
Context.Key<String> TENANT_KEY = Context.key("tenant");
Context withTenant = Context.current().withValue(TENANT_KEY, "tenant-abc");
ExecutorService baseExecutor = Executors.newFixedThreadPool(4);
// Create executor that always uses the tenant context
Executor tenantExecutor = withTenant.fixedContextExecutor(baseExecutor);
// All tasks submitted to this executor will have the tenant context
tenantExecutor.execute(() -> {
String tenant = TENANT_KEY.get(); // "tenant-abc"
processTenantRequest(tenant);
});
tenantExecutor.execute(() -> {
String tenant = TENANT_KEY.get(); // "tenant-abc"
performTenantMaintenance(tenant);
});
// Even if current context changes, the executor still uses the fixed context
Context.current().withValue(TENANT_KEY, "different-tenant").run(() -> {
tenantExecutor.execute(() -> {
String tenant = TENANT_KEY.get(); // Still "tenant-abc"!
handleTenantData(tenant);
});
});Create an Executor that propagates whatever the current context is at the time each task is submitted. This is a static method that captures context dynamically.
/**
* Create an executor that propagates the current context when execute() is called.
* The context is captured at submission time, not creation time.
* @param e Base executor to wrap
* @return Wrapped Executor that propagates the current context for each task
*/
public static Executor currentContextExecutor(Executor e);Usage Example:
Context.Key<String> USER_KEY = Context.key("user");
ExecutorService baseExecutor = Executors.newFixedThreadPool(4);
// Create executor that captures current context for each task
Executor contextExecutor = Context.currentContextExecutor(baseExecutor);
// Task submitted with user "alice" context
Context.current().withValue(USER_KEY, "alice").run(() -> {
contextExecutor.execute(() -> {
String user = USER_KEY.get(); // "alice"
processUserTask(user);
});
});
// Different task submitted with user "bob" context
Context.current().withValue(USER_KEY, "bob").run(() -> {
contextExecutor.execute(() -> {
String user = USER_KEY.get(); // "bob"
processUserTask(user);
});
});
// Tasks submitted without user context
contextExecutor.execute(() -> {
String user = USER_KEY.get(); // null (no user in context)
processGuestTask();
});Common patterns for propagating context across asynchronous operations.
CompletableFuture with Context:
Context.Key<String> TRACE_ID_KEY = Context.key("traceId");
Context withTrace = Context.current().withValue(TRACE_ID_KEY, "trace-456");
ExecutorService executor = Executors.newFixedThreadPool(4);
Executor contextExecutor = Context.currentContextExecutor(executor);
CompletableFuture<String> future = withTrace.call(() -> {
return CompletableFuture
.supplyAsync(() -> {
String traceId = TRACE_ID_KEY.get(); // "trace-456"
return fetchData(traceId);
}, contextExecutor)
.thenApplyAsync(data -> {
String traceId = TRACE_ID_KEY.get(); // "trace-456"
return processData(data, traceId);
}, contextExecutor);
});Thread Pool with Shared Context:
Context.Key<String> SERVICE_KEY = Context.key("service");
Context serviceContext = Context.current().withValue(SERVICE_KEY, "payment-service");
ExecutorService pool = Executors.newFixedThreadPool(10);
Executor serviceExecutor = serviceContext.fixedContextExecutor(pool);
// All tasks in this pool will have service context
for (int i = 0; i < 100; i++) {
final int taskId = i;
serviceExecutor.execute(() -> {
String service = SERVICE_KEY.get(); // "payment-service"
processPaymentTask(taskId, service);
});
}Mixed Context Propagation:
Context.Key<String> REQUEST_KEY = Context.key("request");
Context.Key<String> SESSION_KEY = Context.key("session");
ExecutorService executor = Executors.newFixedThreadPool(4);
Executor currentContextExecutor = Context.currentContextExecutor(executor);
// Base context with session
Context sessionContext = Context.current().withValue(SESSION_KEY, "session-789");
sessionContext.run(() -> {
// Each request gets its own context but inherits session
for (int i = 0; i < 5; i++) {
final String requestId = "req-" + i;
Context.current().withValue(REQUEST_KEY, requestId).run(() -> {
// This task will have both session and request context
currentContextExecutor.execute(() -> {
String session = SESSION_KEY.get(); // "session-789"
String request = REQUEST_KEY.get(); // "req-0", "req-1", etc.
handleRequest(request, session);
});
});
}
});Install with Tessl CLI
npx tessl i tessl/maven-io-grpc--grpc-context