Skip to content

Commit 6f88295

Browse files
committed
Add context propagation primitives for async task execution
Introduce ContextPropagatingRunnable/Callable and the ContextPropagation factory that captures the current request Context at wrap time and restores it around task execution, restoring the worker's previous context afterwards. Each task gets its own copy of the context (ContextObject.copyForPropagation) so parallel workers sharing one request never race on its mutable state. Wrapping is idempotent and passes through null / already-wrapped / no-context tasks. Unit-tested in isolation without agent weaving.
1 parent 468e2c9 commit 6f88295

6 files changed

Lines changed: 389 additions & 1 deletion

File tree

agent_api/src/main/java/dev/aikido/agent_api/context/ContextObject.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import java.util.*;
88

9-
public class ContextObject {
9+
public class ContextObject implements Cloneable {
1010
protected String method;
1111
protected String source;
1212
protected String url;
@@ -25,6 +25,24 @@ public class ContextObject {
2525
protected transient Map<String, Map<String, String>> cache = new HashMap<>();
2626
protected transient Optional<Boolean> forcedProtectionOff = Optional.empty();
2727

28+
// Async tasks get their own copy so parallel workers sharing one request's
29+
// context never race on its mutable working state (cache, redirect nodes).
30+
public ContextObject copyForPropagation() {
31+
try {
32+
ContextObject copy = (ContextObject) super.clone();
33+
copy.cache = new HashMap<>(this.cache);
34+
if (this.redirectStartNodes != null) {
35+
copy.redirectStartNodes = new ArrayList<>(this.redirectStartNodes.size());
36+
for (RedirectNode starter : this.redirectStartNodes) {
37+
copy.redirectStartNodes.add(starter.copyChain());
38+
}
39+
}
40+
return copy;
41+
} catch (CloneNotSupportedException e) {
42+
return this;
43+
}
44+
}
45+
2846
public boolean middlewareExecuted() {return executedMiddleware; }
2947
public void setExecutedMiddleware(boolean value) { executedMiddleware = value; }
3048

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package dev.aikido.agent_api.context;
2+
3+
import java.util.concurrent.Callable;
4+
5+
public final class ContextPropagatingCallable<T> implements Callable<T> {
6+
private final Callable<T> delegate;
7+
private final ContextObject context;
8+
9+
public ContextPropagatingCallable(Callable<T> delegate, ContextObject context) {
10+
this.delegate = delegate;
11+
this.context = context;
12+
}
13+
14+
@Override
15+
public T call() throws Exception {
16+
ContextObject previousContext = Context.get();
17+
18+
try {
19+
Context.set(context);
20+
return delegate.call();
21+
} finally {
22+
if (previousContext != null) {
23+
Context.set(previousContext);
24+
} else {
25+
Context.reset();
26+
}
27+
}
28+
}
29+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package dev.aikido.agent_api.context;
2+
3+
public final class ContextPropagatingRunnable implements Runnable {
4+
private final Runnable delegate;
5+
private final ContextObject context;
6+
7+
public ContextPropagatingRunnable(Runnable delegate, ContextObject context) {
8+
this.delegate = delegate;
9+
this.context = context;
10+
}
11+
12+
@Override
13+
public void run() {
14+
ContextObject previousContext = Context.get();
15+
16+
try {
17+
Context.set(context);
18+
delegate.run();
19+
} finally {
20+
if (previousContext != null) {
21+
Context.set(previousContext);
22+
} else {
23+
Context.reset();
24+
}
25+
}
26+
}
27+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package dev.aikido.agent_api.context;
2+
3+
import java.util.concurrent.Callable;
4+
5+
public final class ContextPropagation {
6+
private ContextPropagation() {}
7+
8+
public static Runnable wrap(Runnable task) {
9+
if (task == null || task instanceof ContextPropagatingRunnable) {
10+
return task;
11+
}
12+
13+
ContextObject context = Context.get();
14+
if (context == null) {
15+
return task;
16+
}
17+
18+
return new ContextPropagatingRunnable(task, context.copyForPropagation());
19+
}
20+
21+
public static <T> Callable<T> wrap(Callable<T> task) {
22+
if (task == null || task instanceof ContextPropagatingCallable) {
23+
return task;
24+
}
25+
26+
ContextObject context = Context.get();
27+
if (context == null) {
28+
return task;
29+
}
30+
31+
return new ContextPropagatingCallable<>(task, context.copyForPropagation());
32+
}
33+
}

agent_api/src/main/java/dev/aikido/agent_api/storage/RedirectNode.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,19 @@ public void setChild(RedirectNode child) {
3636
this.child = child;
3737
}
3838

39+
// Deep-copies the chain from this node downward so a propagated context gets its own
40+
// RedirectNode instances instead of sharing mutable ones (see ContextObject.copyForPropagation).
41+
public RedirectNode copyChain() {
42+
RedirectNode copy = new RedirectNode(this.url);
43+
RedirectNode source = this.child;
44+
RedirectNode tail = copy;
45+
while (source != null) {
46+
tail = new RedirectNode(tail, source.url);
47+
source = source.child;
48+
}
49+
return copy;
50+
}
51+
3952
@Override
4053
public boolean equals(Object obj) {
4154
if (obj == null || getClass() != obj.getClass()) {

0 commit comments

Comments
 (0)