-
Notifications
You must be signed in to change notification settings - Fork 6
Context propagation primitives for async task execution #342
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package dev.aikido.agent_api.context; | ||
|
|
||
| import java.util.concurrent.Callable; | ||
|
|
||
| public final class ContextPropagatingCallable<T> implements Callable<T> { | ||
| private final Callable<T> delegate; | ||
| private final ContextObject context; | ||
|
|
||
| public ContextPropagatingCallable(Callable<T> delegate, ContextObject context) { | ||
| this.delegate = delegate; | ||
| this.context = context; | ||
| } | ||
|
|
||
| @Override | ||
| public T call() throws Exception { | ||
| ContextObject previous = Context.get(); | ||
| try { | ||
| Context.set(context); | ||
| return delegate.call(); | ||
| } finally { | ||
| Context.restore(previous); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package dev.aikido.agent_api.context; | ||
|
|
||
| public final class ContextPropagatingRunnable implements Runnable { | ||
| private final Runnable delegate; | ||
| private final ContextObject context; | ||
|
|
||
| public ContextPropagatingRunnable(Runnable delegate, ContextObject context) { | ||
| this.delegate = delegate; | ||
| this.context = context; | ||
| } | ||
|
|
||
| @Override | ||
| public void run() { | ||
| ContextObject previous = Context.get(); | ||
| try { | ||
| Context.set(context); | ||
| delegate.run(); | ||
| } finally { | ||
| Context.restore(previous); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package dev.aikido.agent_api.context; | ||
|
|
||
| import java.util.concurrent.Callable; | ||
|
|
||
| public final class ContextPropagation { | ||
| private ContextPropagation() {} | ||
|
|
||
| public static Runnable wrap(Runnable task) { | ||
| if (task == null || task instanceof ContextPropagatingRunnable) { | ||
| return task; | ||
|
Comment on lines
+9
to
+10
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium - Re-wrapping a retained task keeps the first request's context
Show fixDo not treat already-wrapped tasks as safe to reuse across requests. Either always create a fresh wrapper for each More info - Reply on this comment to give feedback or ignore the issue.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the wrapper is created inside the agent at submit time and never handed back to the app, so a task is only "already wrapped" within the same submit (same request/context). An app can't retain and resubmit a wrapped task across requests, so the stale-context path is unreachable. |
||
| } | ||
|
|
||
| ContextObject context = Context.get(); | ||
| if (context == null) { | ||
| return task; | ||
| } | ||
|
|
||
| return new ContextPropagatingRunnable(task, context.copyForPropagation()); | ||
| } | ||
|
|
||
| public static <T> Callable<T> wrap(Callable<T> task) { | ||
| if (task == null || task instanceof ContextPropagatingCallable) { | ||
| return task; | ||
| } | ||
|
|
||
| ContextObject context = Context.get(); | ||
| if (context == null) { | ||
| return task; | ||
| } | ||
|
|
||
| return new ContextPropagatingCallable<>(task, context.copyForPropagation()); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
super.clone() shallow-copies mutable fields such as headers, query, cookies, params, body, and user into contexts used by parallel tasks. Deep-copy mutable state or make it immutable before propagation.
Show fix
Details
✨ AI Reasoning
The propagation wrappers create a per-task context and therefore make the copied object's state available on worker threads. The clone operation is shallow for fields other than cache and redirectStartNodes, so headers, query, cookies, and potentially mutable params, body, and user objects remain shared with the original context. Existing getters expose several collection references directly, allowing concurrent task code to mutate the same underlying objects without synchronization. Deep-copy mutable fields or make them immutable before publishing the context to worker threads.
Reply
@AikidoSec feedback: [FEEDBACK]to get better review comments in the future.Reply
@AikidoSec ignore: [REASON]to ignore this issue.More info
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
headers/query/cookies/params/body/user are set once at request build time and only read afterwards, so sharing them across tasks is race-free. The only worker-mutated state (cache, redirect chain) is already deep-copied, and body/params are arbitrary Object types with no safe general deep-copy