Context Propagation¶
Scatter/gather operations dispatch per-entity slices to a virtual-thread executor. Thread-local state (tenant ID, MDC trace context, security principal) captured on the calling thread is not automatically available on executor threads. ContextPropagator bridges this gap.
Interface¶
public interface ContextPropagator {
Supplier<Runnable> wrap(Supplier<Runnable> task);
static ContextPropagator noOp() { ... }
}
wrap is a higher-order function. It receives a Supplier<Runnable> (the slice task) and returns a new Supplier<Runnable> that:
- Captures context on the calling thread when
wrapis invoked. - Restores context before executing the task on the executor thread.
- Cleans up context after the task completes.
When to Use¶
You need ContextPropagator when:
- Scatter/gather is enabled (
payloadSplitterset) - Your store/recover path reads thread-local state (e.g.
TenantContext.current(),MDC.get("traceId")) - You use
failover.scatter.parallel=true(the default)
Spring Security context is not propagated by default
Parallel scatter/gather runs each slice on an executor (virtual) thread, and the async store decorator offloads writes the same way. Anything stored in a ThreadLocal — most importantly Spring Security's SecurityContextHolder (default MODE_THREADLEDGER/ThreadLocal strategy) — is silently absent on those threads. If your TenantResolver, KeyGenerator, store, or splitter reads SecurityContextHolder.getContext(), it will see an empty context on a scatter slice and may route, key, or authorize incorrectly.
There is no built-in SecurityContextPropagator — you must provide one (or disable parallel scatter). A minimal propagator capturing the security context on the calling thread:
@Component
public class SecurityContextPropagator implements ContextPropagator {
@Override
public Runnable wrap(Runnable task) {
SecurityContext ctx = SecurityContextHolder.getContext(); // captured on caller thread
return () -> {
SecurityContextHolder.setContext(ctx); // restored on executor thread
try { task.run(); }
finally { SecurityContextHolder.clearContext(); }
};
}
}
Combine multiple propagators (tenant + security + MDC) with CompositeContextPropagator. If you cannot propagate, set failover.scatter.parallel=false so slices run on the calling thread.
Step 1 — Implement ContextPropagator¶
@Component
public class MdcContextPropagator implements ContextPropagator {
@Override
public Supplier<Runnable> wrap(Supplier<Runnable> task) {
Map<String, String> mdcSnapshot = MDC.getCopyOfContextMap(); // capture on caller thread
return () -> {
Runnable runnable = task.get();
return () -> {
Map<String, String> previous = MDC.getCopyOfContextMap();
try {
if (mdcSnapshot != null) MDC.setContextMap(mdcSnapshot);
else MDC.clear();
runnable.run();
} finally {
if (previous != null) MDC.setContextMap(previous);
else MDC.clear();
}
};
};
}
}
Step 2 — Tenant Context Example¶
@Component
public class TenantContextPropagator implements ContextPropagator {
@Override
public Supplier<Runnable> wrap(Supplier<Runnable> task) {
String tenantId = TenantContext.current(); // captured on the calling thread
return () -> {
Runnable runnable = task.get();
return () -> {
TenantContext.set(tenantId);
try {
runnable.run();
} finally {
TenantContext.clear();
}
};
};
}
}
Multiple ContextPropagator beans are composed into a CompositeContextPropagator by auto-configuration — register one bean per concern.
Next Steps¶
- Payload Splitter — scatter/gather implementation
- Scatter / Gather Concepts — how parallel dispatch works