-
Notifications
You must be signed in to change notification settings - Fork 358
Expand file tree
/
Copy pathAbstractPekkoHttpAsyncHandlerWrapperTest.java
More file actions
217 lines (197 loc) · 9.46 KB
/
Copy pathAbstractPekkoHttpAsyncHandlerWrapperTest.java
File metadata and controls
217 lines (197 loc) · 9.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
import static datadog.trace.agent.test.assertions.SpanMatcher.span;
import static datadog.trace.agent.test.assertions.TraceMatcher.trace;
import static datadog.trace.api.DDSpanTypes.HTTP_SERVER;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import datadog.context.Context;
import datadog.context.ContextScope;
import datadog.trace.agent.test.AbstractInstrumentationTest;
import datadog.trace.agent.test.assertions.SpanMatcher;
import datadog.trace.instrumentation.pekkohttp.DatadogAsyncHandlerWrapper;
import java.lang.reflect.Field;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import org.apache.pekko.http.scaladsl.model.HttpRequest;
import org.apache.pekko.http.scaladsl.model.HttpRequest$;
import org.apache.pekko.http.scaladsl.model.HttpResponse;
import org.apache.pekko.http.scaladsl.model.HttpResponse$;
import org.apache.pekko.http.scaladsl.model.Uri$;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import scala.concurrent.ExecutionContext$;
import scala.concurrent.ExecutionContextExecutorService;
import scala.concurrent.Future;
import scala.concurrent.Promise;
import scala.concurrent.Promise$;
import scala.runtime.AbstractFunction1;
import scala.util.Failure;
import scala.util.Success;
import scala.util.Try;
/**
* Reproduces a request-context leak from the async-handler wrapper into Pekko's completion
* callbacks.
*
* <p>{@link DatadogAsyncHandlerWrapper} creates a server span and Pekko registers callbacks on the
* returned handler {@link Future} after the wrapper has closed the request scope. Those callbacks
* are framework bookkeeping and should therefore not inherit the request context.
*
* <p>The regression was caused by completing the Future exposed to Pekko while the request context
* was active. Scala's promise instrumentation consequently captured that context for Pekko's
* otherwise contextless completion callback. The captured continuation kept the finished trace
* buffered until the callback exited. The test depends on the scala-promise instrumentation in this
* module's test runtime to model that production propagation behavior.
*
* <p>The test makes the race deterministic by holding the simulated Pekko callback on a latch. The
* expected behavior is that the finished request trace is reported while that callback remains
* blocked. Before the instrumentation is fixed, the assertion fails because the trace is reported
* only after test cleanup releases the callback.
*/
abstract class AbstractPekkoHttpAsyncHandlerWrapperTest extends AbstractInstrumentationTest {
/**
* The operation name is a {@code UTF8BytesString}; {@link SpanMatcher#operationName(String)}
* compares object types, while the {@link Pattern} overload compares {@code CharSequence}
* content.
*/
private static final Pattern OPERATION_NAME = Pattern.compile("pekko-http\\.request");
protected abstract boolean expectedCompletionPriority();
@BeforeEach
void verifyPropagationMode() throws Exception {
// Load the injected helper only after the test agent is installed, then verify each concrete
// variant is exercising the intended Scala Promise propagation mode.
assertEquals(
expectedCompletionPriority(),
readStaticBoolean(
Class.forName("datadog.trace.instrumentation.scala.PromiseHelper"),
"completionPriority"),
"Unexpected Scala Promise propagation mode");
// The wrapper resolves the same configuration independently, so pin the two together. A wrapper
// that stopped tracking this mode would otherwise silently skip the defensive Try copy while
// these tests kept passing.
assertEquals(
expectedCompletionPriority(),
readStaticBoolean(DatadogAsyncHandlerWrapper.class, "COMPLETION_PRIORITY"),
"Wrapper propagation mode disagrees with the Scala Promise instrumentation");
}
private static boolean readStaticBoolean(final Class<?> type, final String name)
throws Exception {
final Field field = type.getDeclaredField(name);
field.setAccessible(true);
return (Boolean) field.get(null);
}
/**
* Covers the failed-response path. On Scala 2.12 {@code Promise.resolveTry} allocates a fresh
* {@code Failure}, which incidentally drops any context associated with the completing {@code
* Try}, so this path exercises the root-context attachment only. Scala 2.13 passes the {@code
* Try} through, so there it exercises both defenses.
*/
@Test
void doesNotPropagateRequestContextWhenHandlerFails() throws Exception {
assertRequestTraceIsNotRetained(
new Failure<>(new Exception("controller exception")),
span().root().operationName(OPERATION_NAME).type(HTTP_SERVER).error());
}
/**
* Covers the successful-response path. Both Scala generations pass a {@code Success} through
* completion unchanged, so this is the case that pins the defensive {@code Try} copy in
* completion-priority mode.
*/
@Test
void doesNotPropagateRequestContextWhenHandlerSucceeds() throws Exception {
assertRequestTraceIsNotRetained(
new Success<>(emptyResponse()),
span().root().operationName(OPERATION_NAME).type(HTTP_SERVER).error(false));
}
private void assertRequestTraceIsNotRetained(
final Try<HttpResponse> handlerResult, final SpanMatcher expectedSpan) throws Exception {
try (AsyncHandlerWrapperReproducer reproducer =
new AsyncHandlerWrapperReproducer(handlerResult)) {
reproducer.start();
assertTrue(reproducer.awaitFrameworkCallback(), "Framework callback did not start");
assertTrue(
writer.waitForTracesMax(1, 5),
"Request trace was held by the contextless framework callback");
assertTraces(trace(expectedSpan));
}
}
private static HttpResponse emptyResponse() {
return HttpResponse$.MODULE$.apply(
HttpResponse$.MODULE$.apply$default$1(),
HttpResponse$.MODULE$.apply$default$2(),
HttpResponse$.MODULE$.apply$default$3(),
HttpResponse$.MODULE$.apply$default$4());
}
private static final class AsyncHandlerWrapperReproducer implements AutoCloseable {
private final Try<HttpResponse> handlerResult;
private final Promise<HttpResponse> handlerPromise = Promise$.MODULE$.apply();
private final AtomicReference<Context> requestContext = new AtomicReference<>();
private final CountDownLatch callbackStarted = new CountDownLatch(1);
private final CountDownLatch releaseCallback = new CountDownLatch(1);
private final ExecutionContextExecutorService handlerExecutor =
ExecutionContext$.MODULE$.fromExecutorService(Executors.newSingleThreadExecutor());
private final ExecutionContextExecutorService frameworkExecutor =
ExecutionContext$.MODULE$.fromExecutorService(Executors.newSingleThreadExecutor());
AsyncHandlerWrapperReproducer(final Try<HttpResponse> handlerResult) {
this.handlerResult = handlerResult;
}
void start() {
DatadogAsyncHandlerWrapper wrapper =
new DatadogAsyncHandlerWrapper(
new AbstractFunction1<HttpRequest, Future<HttpResponse>>() {
@Override
public Future<HttpResponse> apply(HttpRequest request) {
requestContext.set(Context.current());
return handlerPromise.future();
}
},
handlerExecutor);
HttpRequest request =
HttpRequest$.MODULE$.apply(
HttpRequest$.MODULE$.apply$default$1(),
Uri$.MODULE$.apply("/exception"),
HttpRequest$.MODULE$.apply$default$3(),
HttpRequest$.MODULE$.apply$default$4(),
HttpRequest$.MODULE$.apply$default$5());
Future<HttpResponse> response = wrapper.apply(request);
// Model a callback registered by Pekko after the wrapper has closed the request scope. It
// should not inherit the request context when the handler Future completes.
response.onComplete(
new AbstractFunction1<Try<HttpResponse>, Void>() {
@Override
public Void apply(Try<HttpResponse> result) {
callbackStarted.countDown();
try {
releaseCallback.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return null;
}
},
frameworkExecutor);
// Application futures normally complete from callbacks running with the propagated request
// context. Model that completion path so the test also covers context captured at promise
// dispatch time, not only at callback construction time.
try (ContextScope ignored = requestContext.get().attach()) {
handlerPromise.complete(handlerResult);
}
}
boolean awaitFrameworkCallback() throws InterruptedException {
return callbackStarted.await(5, TimeUnit.SECONDS);
}
@Override
public void close() throws InterruptedException {
releaseCallback.countDown();
handlerExecutor.shutdown();
frameworkExecutor.shutdown();
assertTrue(
handlerExecutor.awaitTermination(5, TimeUnit.SECONDS),
"Handler executor did not terminate");
assertTrue(
frameworkExecutor.awaitTermination(5, TimeUnit.SECONDS),
"Framework executor did not terminate");
}
}
}