feat(service): async context propagation for task executor#4061
feat(service): async context propagation for task executor#4061flyingImer wants to merge 9 commits intoapache:mainfrom
Conversation
dimas-b
left a comment
There was a problem hiding this comment.
Hi @flyingImer ! Good idea to normalize the code that deals with async context values propagation! Some comments and suggestions below.
...ime/service/src/main/java/org/apache/polaris/service/events/PolarisEventMetadataFactory.java
Outdated
Show resolved
Hide resolved
...ime/service/src/main/java/org/apache/polaris/service/events/PolarisEventMetadataFactory.java
Outdated
Show resolved
Hide resolved
runtime/service/src/main/java/org/apache/polaris/service/task/TaskExecutorImpl.java
Outdated
Show resolved
Hide resolved
runtime/service/src/main/java/org/apache/polaris/service/task/PrincipalContextPropagator.java
Outdated
Show resolved
Hide resolved
runtime/service/src/main/java/org/apache/polaris/service/task/PrincipalContextPropagator.java
Outdated
Show resolved
Hide resolved
runtime/service/src/main/java/org/apache/polaris/service/task/TaskExecutorImpl.java
Outdated
Show resolved
Hide resolved
runtime/service/src/main/java/org/apache/polaris/service/task/RequestIdPropagator.java
Outdated
Show resolved
Hide resolved
runtime/service/src/main/java/org/apache/polaris/service/task/RealmContextPropagator.java
Outdated
Show resolved
Hide resolved
jbonofre
left a comment
There was a problem hiding this comment.
Overall good to me.
Can you please fix the spotless issues ? I will do a new pass after.
Thanks !
runtime/service/src/main/java/org/apache/polaris/service/context/catalog/RequestIdHolder.java
Outdated
Show resolved
Hide resolved
| @Inject Clock clock; | ||
| @Inject CurrentIdentityAssociation currentIdentityAssociation; | ||
| @Inject Instance<RealmContext> realmContext; | ||
| @Inject RequestIdSupplier requestIdSupplier; |
There was a problem hiding this comment.
Why not RequestIdHolder?.. It's simpler, no?
There was a problem hiding this comment.
RequestIdSupplier is the read-only contract and this class only reads. It's also what PersistingMetricsReporter uses, so keeping them consistent felt right. Happy to revisit if you see a reason to diverge.
There was a problem hiding this comment.
I think RequestIdHolder is enough (see my other comment).
There was a problem hiding this comment.
Yes, in this case the "factory" can be thought of as infrastructure code, so using RequestIdHolder is perfectly fine.
runtime/service/src/main/java/org/apache/polaris/service/task/AsyncContextPropagator.java
Outdated
Show resolved
Hide resolved
runtime/service/src/main/java/org/apache/polaris/service/task/PrincipalContextPropagator.java
Outdated
Show resolved
Hide resolved
runtime/service/src/main/java/org/apache/polaris/service/task/RealmContextPropagator.java
Outdated
Show resolved
Hide resolved
|
@dimas-b I think your latest comments convinced me the shape issue is central enough to address in this PR. My current cut is to keep the scope narrow, but switch this SPI to a state/action model so callers no longer deal with raw I may still keep a generic cleanup hook with a default no-op, but the intent there would be generic lifecycle cleanup, not letting MDC shape the main propagation contract. I also plan to clean up the smaller Does that sound like the right cut? |
|
@flyingImer : I'd prefer to keep MDC out of this PR (feel free to improve MDC data in a follow-up PR). I believe it is a totally different concern, not related to Request Context. Let's keep this PR focused on CDI concerns. Looking forward to a new diff. |
… for context management
runtime/service/src/main/java/org/apache/polaris/service/task/AsyncContextPropagator.java
Outdated
Show resolved
Hide resolved
runtime/service/src/main/java/org/apache/polaris/service/task/PrincipalContextPropagator.java
Outdated
Show resolved
Hide resolved
runtime/service/src/main/java/org/apache/polaris/service/task/PrincipalContextPropagator.java
Outdated
Show resolved
Hide resolved
runtime/service/src/main/java/org/apache/polaris/service/task/PrincipalContextPropagator.java
Outdated
Show resolved
Hide resolved
runtime/service/src/main/java/org/apache/polaris/service/task/TaskExecutorImpl.java
Outdated
Show resolved
Hide resolved
runtime/service/src/main/java/org/apache/polaris/service/task/AsyncContextPropagator.java
Outdated
Show resolved
Hide resolved
runtime/service/src/main/java/org/apache/polaris/service/task/RequestIdPropagator.java
Outdated
Show resolved
Hide resolved
| // earlier ones, matching standard stacked-context conventions. | ||
| for (int i = restored - 1; i >= 0; i--) { | ||
| try { | ||
| actions.get(i).close(); |
There was a problem hiding this comment.
I think I understand what you'd like to build, but I believe the term "context propagation SPI" (from PR title) would be really confusing in that case.
If the idea is to establish a convenience framework for running code inside some context and make sure the current thread removes traces of that context on completion, I believe a better approach would to follow the pattern of Bootstrapper (suggested new name: ContrextualTaskRunner).
Inject RequestIdHolder, PolarisPrincipalHolder and RealmContextHolder into it.
Use them to propagate context internally. No need for a new SPI.
TaskExecutorImpl would call:
- (on task creation)
ContextualAction action = ContrextualTaskRunner.capture() - (on execution)
action.runWithNewContext( { ... } );
MDC can be handled internally and cleared upon exit from runWithContext
There was a problem hiding this comment.
hmmm, on second thought, makes sense. I now agree this PR doesn't have a strong enough case for a new SPI. The main value is the propagation itself, not the extension point.
I'll rework this to a concrete helper along the lines of your ContextualTaskRunner suggestion, wiring in the three holders directly. MDC will be a separate follow-up.
dimas-b
left a comment
There was a problem hiding this comment.
Thanks for bearing with me, @flyingImer 🙂
| */ | ||
| CapturedTaskContext capture() { | ||
| return new CapturedTaskContext( | ||
| realmContextHolder.get(), |
There was a problem hiding this comment.
nit: for good measure, it might be best to create a copy for the realm context too. It's similar to the principal in many aspects.
Request ID is just a String, so it's fine to reuse it.
There was a problem hiding this comment.
Thanks for calling this out! Let me tackle it in a follow up pr later
|
Hi @jbonofre, I addressed the earlier feedback and the latest CI is green now. |
| /** | ||
| * Immutable snapshot of request-scoped context captured for propagation across async boundaries. | ||
| */ | ||
| record CapturedTaskContext( |
There was a problem hiding this comment.
This imo should be public as it is being exposed outside the package scope: in the protected handleTaskWithTracing method.
It should probably be made top-level as well.
| tryHandleTask(taskEntityId, clone, eventMetadata, null, 1); | ||
|
|
||
| // Capture request-scoped context for propagation into the task thread. | ||
| TaskContextPropagator.CapturedTaskContext captured = taskContextPropagator.capture(); |
There was a problem hiding this comment.
With this change, do we still need to clone CallContext on line 148? Do we need CallContext in this class at all?
| - Changed from Poetry to UV for Python package management. | ||
| - Exclude KMS policies when KMS is not being used for S3. | ||
| - Improved default KMS permission handling to better distinguish read-only and read-write access. | ||
| - Request IDs are now propagated to async task threads via CDI-injectable `RequestIdSupplier`, so task log messages carry the originating request's ID. Context propagation across task boundaries is now handled by the pluggable `AsyncContextPropagator` SPI. |
There was a problem hiding this comment.
| - Request IDs are now propagated to async task threads via CDI-injectable `RequestIdSupplier`, so task log messages carry the originating request's ID. Context propagation across task boundaries is now handled by the pluggable `AsyncContextPropagator` SPI. | |
| - Request IDs are now propagated to async task threads via CDI-injectable `RequestIdSupplier`, so task log messages carry the originating request's ID. Context propagation across task boundaries is now handled by the pluggable `TaskContextPropagator` SPI. |
| @Produces | ||
| @RequestScoped | ||
| public RequestIdSupplier getRequestIdSupplier() { | ||
| return () -> requestId.get(); |
There was a problem hiding this comment.
nit:
| return () -> requestId.get(); | |
| return requestId::get |
| */ | ||
| @Produces | ||
| @RequestScoped | ||
| public RequestIdSupplier getRequestIdSupplier() { |
There was a problem hiding this comment.
Do we need both a RequestIdHolder and a RequestIdSupplier? I think the holder could now just expose the raw request ID. The supplier creates an unnecessary indirection, it seems.
There was a problem hiding this comment.
RequestIdSupplier is for injection into other beans, as far as I can tell. It's more specific than a Supplier<Integer>.
IMHO, RequestIdHolder is meant to be used only by infrastructure code.
There was a problem hiding this comment.
Alternatively, we could produce a @Named("RequestId") Integer here and get it injected into code that calls RequestIdSupplier.get().
There was a problem hiding this comment.
If "holder" interfaces are meant for use in infrastructure code only, in this case I would just rename RequestIdSupplier to RequestId – it would then look quite similar, in shape and purpose, to the RealmContext interface.
IMHO it's easier to have an injectable interface encapsulating the string, rather than injecting the raw string itself annotated with @Named.
There was a problem hiding this comment.
However, RequestIdSupplier is an old class (from #3385). It might be best to do that renaming in a separate PR 🤔
There was a problem hiding this comment.
To be more specific about "infrastructure" code, I think RequestIdSupplier is for use in polaris-core and runtime/service classes that "consume" requests IDs without participating in the construction of request-scoped objects.
Factories and CDI producers should be able to use RequestIdHolder. Its role is pretty clear in that context, I guess.
| @Inject Clock clock; | ||
| @Inject CurrentIdentityAssociation currentIdentityAssociation; | ||
| @Inject Instance<RealmContext> realmContext; | ||
| @Inject RequestIdSupplier requestIdSupplier; |
There was a problem hiding this comment.
I think RequestIdHolder is enough (see my other comment).
|
Worth noting: this PR introduces a new I wonder if it wouldn't be cleaner to just wire up a custom The custom |
+1 |
|
A note on the "Holder" class pattern ( These classes are meant to allow Polaris code to manage corresponding request-scoped data without relying on the REST framework ( |
This PR adds RequestIdHolder and a concrete context propagation helper for TaskExecutorImpl. Fixes #3444
Problem
When TaskExecutorImpl schedules async work, the task runs on a different thread with a fresh CDI request scope. Request-scoped context (realm, principal, request ID) was previously propagated via ad-hoc hardcoded logic, and request IDs were not propagated at all, since the only way to read them was through RESTEasy's internal CurrentRequestManager API, which is unavailable on task threads.
Solution
RequestIdHolder is a new @RequestScoped CDI bean replacing the removed ServiceProducers.requestIdSupplier() that depended on RESTEasy internals. It produces RequestIdSupplier via CDI so any component can inject it without depending on JAX-RS types. RequestIdFilter now writes to this holder on each request.
TaskContextPropagator is a package-private helper that captures realm, principal, and request ID on the request thread and restores them into the task thread's fresh CDI request scope. It directly injects RealmContextHolder, PolarisPrincipalHolder, and RequestIdHolder. No new SPI or extension point is introduced. The implementation follows the same pattern as Bootstrapper.
CurrentRequestManager is no longer referenced anywhere in the codebase.
Out of scope (follow-up candidates)
Checklist
- Unit tests for TaskContextPropagator (capture, restore, round-trip)
- TaskExecutorImplTest updated for new constructor signature
Disclaimer
Javadoc is mainly assisted by coding agent.