Skip to content

Commit a88ebb5

Browse files
authored
Merge pull request #770 from DataDog/mar-kolya/aws-sdk-v0-memory-leak
AWS v0: Close span non AWS SDK errors
2 parents 25ea44e + b22b11b commit a88ebb5

File tree

7 files changed

+607
-20
lines changed

7 files changed

+607
-20
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package datadog.trace.instrumentation.aws.v0;
2+
3+
import static datadog.trace.instrumentation.aws.v0.AwsSdkClientDecorator.DECORATE;
4+
import static java.util.Collections.singletonMap;
5+
import static net.bytebuddy.matcher.ElementMatchers.isAbstract;
6+
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
7+
import static net.bytebuddy.matcher.ElementMatchers.named;
8+
import static net.bytebuddy.matcher.ElementMatchers.not;
9+
10+
import com.amazonaws.AmazonClientException;
11+
import com.amazonaws.Request;
12+
import com.amazonaws.handlers.RequestHandler2;
13+
import com.google.auto.service.AutoService;
14+
import datadog.trace.agent.tooling.Instrumenter;
15+
import io.opentracing.Scope;
16+
import java.util.Map;
17+
import net.bytebuddy.asm.Advice;
18+
import net.bytebuddy.description.method.MethodDescription;
19+
import net.bytebuddy.description.type.TypeDescription;
20+
import net.bytebuddy.matcher.ElementMatcher;
21+
22+
/**
23+
* This is additional 'helper' to catch cases when HTTP request throws exception different from
24+
* {@link AmazonClientException} (for example an error thrown by another handler). In these cases
25+
* {@link RequestHandler2#afterError} is not called.
26+
*/
27+
@AutoService(Instrumenter.class)
28+
public class AWSHttpClientInstrumentation extends Instrumenter.Default {
29+
30+
public AWSHttpClientInstrumentation() {
31+
super("aws-sdk");
32+
}
33+
34+
@Override
35+
public ElementMatcher<TypeDescription> typeMatcher() {
36+
return named("com.amazonaws.http.AmazonHttpClient");
37+
}
38+
39+
@Override
40+
public String[] helperClassNames() {
41+
return new String[] {
42+
"datadog.trace.agent.decorator.BaseDecorator",
43+
"datadog.trace.agent.decorator.ClientDecorator",
44+
"datadog.trace.agent.decorator.HttpClientDecorator",
45+
packageName + ".AwsSdkClientDecorator",
46+
packageName + ".TracingRequestHandler",
47+
};
48+
}
49+
50+
@Override
51+
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
52+
return singletonMap(
53+
isMethod().and(not(isAbstract())).and(named("doExecute")),
54+
HttpClientAdvice.class.getName());
55+
}
56+
57+
public static class HttpClientAdvice {
58+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
59+
public static void methodExit(
60+
@Advice.Argument(value = 0, optional = true) final Request<?> request,
61+
@Advice.Thrown final Throwable throwable) {
62+
if (throwable != null) {
63+
final Scope scope = request.getHandlerContext(TracingRequestHandler.SCOPE_CONTEXT_KEY);
64+
if (scope != null) {
65+
request.addHandlerContext(TracingRequestHandler.SCOPE_CONTEXT_KEY, null);
66+
DECORATE.onError(scope.span(), throwable);
67+
DECORATE.beforeFinish(scope.span());
68+
scope.close();
69+
}
70+
}
71+
}
72+
}
73+
74+
/**
75+
* Due to a change in the AmazonHttpClient class, this instrumentation is needed to support newer
76+
* versions. The above class should cover older versions.
77+
*/
78+
@AutoService(Instrumenter.class)
79+
public static final class RequestExecutorInstrumentation extends AWSHttpClientInstrumentation {
80+
81+
@Override
82+
public ElementMatcher<TypeDescription> typeMatcher() {
83+
return named("com.amazonaws.http.AmazonHttpClient$RequestExecutor");
84+
}
85+
86+
@Override
87+
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
88+
return singletonMap(
89+
isMethod().and(not(isAbstract())).and(named("doExecute")),
90+
RequestExecutorAdvice.class.getName());
91+
}
92+
93+
public static class RequestExecutorAdvice {
94+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
95+
public static void methodExit(
96+
@Advice.FieldValue("request") final Request<?> request,
97+
@Advice.Thrown final Throwable throwable) {
98+
if (throwable != null) {
99+
final Scope scope = request.getHandlerContext(TracingRequestHandler.SCOPE_CONTEXT_KEY);
100+
if (scope != null) {
101+
request.addHandlerContext(TracingRequestHandler.SCOPE_CONTEXT_KEY, null);
102+
DECORATE.onError(scope.span(), throwable);
103+
DECORATE.beforeFinish(scope.span());
104+
scope.close();
105+
}
106+
}
107+
}
108+
}
109+
}
110+
}

dd-java-agent/instrumentation/aws-java-sdk-1.11.0/src/main/java/datadog/trace/instrumentation/aws/v0/TracingRequestHandler.java

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public class TracingRequestHandler extends RequestHandler2 {
1616

1717
// Note: aws1.x sdk doesn't have any truly async clients so we can store scope in request context
1818
// safely.
19-
private static final HandlerContextKey<Scope> SCOPE_CONTEXT_KEY =
19+
public static final HandlerContextKey<Scope> SCOPE_CONTEXT_KEY =
2020
new HandlerContextKey<>("DatadogScope");
2121

2222
@Override
@@ -35,16 +35,22 @@ public void beforeRequest(final Request<?> request) {
3535
@Override
3636
public void afterResponse(final Request<?> request, final Response<?> response) {
3737
final Scope scope = request.getHandlerContext(SCOPE_CONTEXT_KEY);
38-
DECORATE.onResponse(scope.span(), response);
39-
DECORATE.beforeFinish(scope.span());
40-
scope.close();
38+
if (scope != null) {
39+
request.addHandlerContext(SCOPE_CONTEXT_KEY, null);
40+
DECORATE.onResponse(scope.span(), response);
41+
DECORATE.beforeFinish(scope.span());
42+
scope.close();
43+
}
4144
}
4245

4346
@Override
4447
public void afterError(final Request<?> request, final Response<?> response, final Exception e) {
4548
final Scope scope = request.getHandlerContext(SCOPE_CONTEXT_KEY);
46-
DECORATE.onError(scope.span(), e);
47-
DECORATE.beforeFinish(scope.span());
48-
scope.close();
49+
if (scope != null) {
50+
request.addHandlerContext(SCOPE_CONTEXT_KEY, null);
51+
DECORATE.onError(scope.span(), e);
52+
DECORATE.beforeFinish(scope.span());
53+
scope.close();
54+
}
4955
}
5056
}

dd-java-agent/instrumentation/aws-java-sdk-1.11.0/src/test/groovy/AWSClientTest.groovy

Lines changed: 194 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,42 @@
1+
import com.amazonaws.AmazonClientException
2+
import com.amazonaws.ClientConfiguration
3+
import com.amazonaws.Request
14
import com.amazonaws.SDKGlobalConfiguration
5+
import com.amazonaws.auth.AWSCredentialsProviderChain
26
import com.amazonaws.auth.BasicAWSCredentials
7+
import com.amazonaws.auth.EnvironmentVariableCredentialsProvider
8+
import com.amazonaws.auth.InstanceProfileCredentialsProvider
9+
import com.amazonaws.auth.SystemPropertiesCredentialsProvider
10+
import com.amazonaws.auth.profile.ProfileCredentialsProvider
311
import com.amazonaws.handlers.RequestHandler2
12+
import com.amazonaws.retry.PredefinedRetryPolicies
413
import com.amazonaws.services.ec2.AmazonEC2Client
514
import com.amazonaws.services.rds.AmazonRDSClient
615
import com.amazonaws.services.rds.model.DeleteOptionGroupRequest
716
import com.amazonaws.services.s3.AmazonS3Client
817
import com.amazonaws.services.s3.S3ClientOptions
918
import datadog.trace.agent.test.AgentTestRunner
1019
import datadog.trace.api.DDSpanTypes
20+
import io.opentracing.Tracer
1121
import io.opentracing.tag.Tags
22+
import org.apache.http.conn.HttpHostConnectException
23+
import org.apache.http.impl.execchain.RequestAbortedException
1224
import spock.lang.AutoCleanup
1325
import spock.lang.Shared
1426

1527
import java.util.concurrent.atomic.AtomicReference
1628

1729
import static datadog.trace.agent.test.server.http.TestHttpServer.httpServer
30+
import static datadog.trace.agent.test.utils.PortUtils.UNUSABLE_PORT
1831

1932
class AWSClientTest extends AgentTestRunner {
33+
34+
private static final CREDENTIALS_PROVIDER_CHAIN = new AWSCredentialsProviderChain(
35+
new EnvironmentVariableCredentialsProvider(),
36+
new SystemPropertiesCredentialsProvider(),
37+
new ProfileCredentialsProvider(),
38+
new InstanceProfileCredentialsProvider())
39+
2040
def setupSpec() {
2141
System.setProperty(SDKGlobalConfiguration.ACCESS_KEY_SYSTEM_PROPERTY, "my-access-key")
2242
System.setProperty(SDKGlobalConfiguration.SECRET_KEY_SYSTEM_PROPERTY, "my-secret-key")
@@ -63,9 +83,11 @@ class AWSClientTest extends AgentTestRunner {
6383
def "send #operation request with mocked response"() {
6484
setup:
6585
responseBody.set(body)
86+
87+
when:
6688
def response = call.call(client)
6789

68-
expect:
90+
then:
6991
response != null
7092

7193
client.requestHandler2s != null
@@ -135,4 +157,175 @@ class AWSClientTest extends AgentTestRunner {
135157
</DeleteOptionGroupResponse>
136158
""" | new AmazonRDSClient().withEndpoint("http://localhost:$server.address.port")
137159
}
160+
161+
def "send #operation request to closed port"() {
162+
setup:
163+
responseBody.set(body)
164+
165+
when:
166+
call.call(client)
167+
168+
then:
169+
thrown AmazonClientException
170+
171+
assertTraces(1) {
172+
trace(0, 2) {
173+
span(0) {
174+
serviceName "java-aws-sdk"
175+
operationName "aws.http"
176+
resourceName "$service.$operation"
177+
spanType DDSpanTypes.HTTP_CLIENT
178+
errored true
179+
parent()
180+
tags {
181+
"$Tags.COMPONENT.key" "java-aws-sdk"
182+
"$Tags.HTTP_URL.key" "http://localhost:${UNUSABLE_PORT}"
183+
"$Tags.HTTP_METHOD.key" "$method"
184+
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
185+
"aws.service" { it.contains(service) }
186+
"aws.endpoint" "http://localhost:${UNUSABLE_PORT}"
187+
"aws.operation" "${operation}Request"
188+
"aws.agent" "java-aws-sdk"
189+
errorTags AmazonClientException, ~/Unable to execute HTTP request/
190+
defaultTags()
191+
}
192+
}
193+
span(1) {
194+
operationName "http.request"
195+
resourceName "$method /$url"
196+
spanType DDSpanTypes.HTTP_CLIENT
197+
errored true
198+
childOf(span(0))
199+
tags {
200+
"$Tags.COMPONENT.key" "apache-httpclient"
201+
"$Tags.HTTP_URL.key" "http://localhost:${UNUSABLE_PORT}/$url"
202+
"$Tags.PEER_HOSTNAME.key" "localhost"
203+
"$Tags.PEER_PORT.key" UNUSABLE_PORT
204+
"$Tags.HTTP_METHOD.key" "$method"
205+
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
206+
errorTags HttpHostConnectException, ~/Connection refused/
207+
defaultTags()
208+
}
209+
}
210+
}
211+
}
212+
213+
where:
214+
service | operation | method | url | call | body | client
215+
"S3" | "GetObject" | "GET" | "someBucket/someKey" | { client -> client.getObject("someBucket", "someKey") } | "" | new AmazonS3Client(CREDENTIALS_PROVIDER_CHAIN, new ClientConfiguration().withRetryPolicy(PredefinedRetryPolicies.getDefaultRetryPolicyWithCustomMaxRetries(0))).withEndpoint("http://localhost:${UNUSABLE_PORT}")
216+
}
217+
218+
def "naughty request handler doesn't break the trace"() {
219+
setup:
220+
def client = new AmazonS3Client(CREDENTIALS_PROVIDER_CHAIN)
221+
client.addRequestHandler(new RequestHandler2() {
222+
void beforeRequest(Request<?> request) {
223+
throw new RuntimeException("bad handler")
224+
}
225+
})
226+
227+
when:
228+
client.getObject("someBucket", "someKey")
229+
230+
then:
231+
((Tracer) TEST_TRACER).activeSpan() == null
232+
thrown RuntimeException
233+
234+
assertTraces(1) {
235+
trace(0, 1) {
236+
span(0) {
237+
serviceName "java-aws-sdk"
238+
operationName "aws.http"
239+
resourceName "S3.GetObject"
240+
spanType DDSpanTypes.HTTP_CLIENT
241+
errored true
242+
parent()
243+
tags {
244+
"$Tags.COMPONENT.key" "java-aws-sdk"
245+
"$Tags.HTTP_URL.key" "https://s3.amazonaws.com"
246+
"$Tags.HTTP_METHOD.key" "GET"
247+
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
248+
"aws.service" "Amazon S3"
249+
"aws.endpoint" "https://s3.amazonaws.com"
250+
"aws.operation" "GetObjectRequest"
251+
"aws.agent" "java-aws-sdk"
252+
errorTags RuntimeException, "bad handler"
253+
defaultTags()
254+
}
255+
}
256+
}
257+
}
258+
}
259+
260+
def "timeout and retry errors captured"() {
261+
setup:
262+
def server = httpServer {
263+
handlers {
264+
all {
265+
Thread.sleep(500)
266+
response.status(200).send()
267+
}
268+
}
269+
}
270+
AmazonS3Client client = new AmazonS3Client(new ClientConfiguration().withRequestTimeout(50 /* ms */))
271+
.withEndpoint("http://localhost:$server.address.port")
272+
273+
when:
274+
client.getObject("someBucket", "someKey")
275+
276+
then:
277+
((Tracer) TEST_TRACER).activeSpan() == null
278+
thrown AmazonClientException
279+
280+
assertTraces(1) {
281+
trace(0, 5) {
282+
span(0) {
283+
serviceName "java-aws-sdk"
284+
operationName "aws.http"
285+
resourceName "S3.GetObject"
286+
spanType DDSpanTypes.HTTP_CLIENT
287+
errored true
288+
parent()
289+
tags {
290+
"$Tags.COMPONENT.key" "java-aws-sdk"
291+
"$Tags.HTTP_URL.key" "http://localhost:$server.address.port"
292+
"$Tags.HTTP_METHOD.key" "GET"
293+
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
294+
"aws.service" "Amazon S3"
295+
"aws.endpoint" "http://localhost:$server.address.port"
296+
"aws.operation" "GetObjectRequest"
297+
"aws.agent" "java-aws-sdk"
298+
errorTags AmazonClientException, ~/Unable to execute HTTP request/
299+
defaultTags()
300+
}
301+
}
302+
(1..4).each {
303+
span(it) {
304+
operationName "http.request"
305+
resourceName "GET /someBucket/someKey"
306+
spanType DDSpanTypes.HTTP_CLIENT
307+
errored true
308+
childOf(span(0))
309+
tags {
310+
"$Tags.COMPONENT.key" "apache-httpclient"
311+
"$Tags.HTTP_URL.key" "http://localhost:$server.address.port/someBucket/someKey"
312+
"$Tags.PEER_HOSTNAME.key" "localhost"
313+
"$Tags.PEER_PORT.key" server.address.port
314+
"$Tags.HTTP_METHOD.key" "GET"
315+
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
316+
try {
317+
errorTags SocketException, "Socket closed"
318+
} catch (AssertionError e) {
319+
errorTags RequestAbortedException, "Request aborted"
320+
}
321+
defaultTags()
322+
}
323+
}
324+
}
325+
}
326+
}
327+
328+
cleanup:
329+
server.close()
330+
}
138331
}

0 commit comments

Comments
 (0)