Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .brazil.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"sdk-core": { "packageName": "AwsJavaSdk-Core" },
"url-connection-client": { "packageName": "AwsJavaSdk-HttpClient-UrlConnectionClient" },
"utils": { "packageName": "AwsJavaSdk-Core-Utils" },
"utils-lite": { "packageName": "AwsJavaSdk-Core-UtilsLite" },
"imds": { "packageName": "AwsJavaSdk-Imds" },
"crt-core": { "packageName": "AwsJavaSdk-Core-CrtCore" },
"checksums-spi": { "packageName": "AwsJavaSdk-Core-ChecksumsSpi" },
Expand Down
6 changes: 6 additions & 0 deletions .changes/next-release/feature-AWSSDKforJavav2-a3984eb.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "feature",
"category": "AWS SDK for Java v2",
"contributor": "",
"description": "Add a utils-lite package that wraps threadlocal meant for internal shared usage across multiple applications across multiple applications"
}
5 changes: 5 additions & 0 deletions bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,11 @@
<artifactId>utils</artifactId>
<version>${awsjavasdk.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>utils-lite</artifactId>
<version>${awsjavasdk.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>cloudwatch-metric-publisher</artifactId>
Expand Down
5 changes: 5 additions & 0 deletions core/aws-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@
<groupId>software.amazon.eventstream</groupId>
<artifactId>eventstream</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>utils-lite</artifactId>
<version>${awsjavasdk.version}</version>
</dependency>

<dependency>
<groupId>software.amazon.awssdk</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
import software.amazon.awssdk.annotations.SdkProtectedApi;
import software.amazon.awssdk.awscore.internal.interceptor.TracingSystemSetting;
import software.amazon.awssdk.core.interceptor.Context;
import software.amazon.awssdk.core.interceptor.ExecutionAttribute;
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
import software.amazon.awssdk.http.SdkHttpRequest;
import software.amazon.awssdk.utils.SystemSetting;
import software.amazon.awssdk.utilslite.SdkInternalThreadLocal;

/**
* The {@code TraceIdExecutionInterceptor} copies the trace details to the {@link #TRACE_ID_HEADER} header, assuming we seem to
Expand All @@ -32,27 +34,57 @@
public class TraceIdExecutionInterceptor implements ExecutionInterceptor {
private static final String TRACE_ID_HEADER = "X-Amzn-Trace-Id";
private static final String LAMBDA_FUNCTION_NAME_ENVIRONMENT_VARIABLE = "AWS_LAMBDA_FUNCTION_NAME";
private static final String CONCURRENT_TRACE_ID_KEY = "AWS_LAMBDA_X_TRACE_ID";
private static final ExecutionAttribute<String> TRACE_ID = new ExecutionAttribute<>("TraceId");

@Override
public void beforeExecution(Context.BeforeExecution context, ExecutionAttributes executionAttributes) {
String traceId = SdkInternalThreadLocal.get(CONCURRENT_TRACE_ID_KEY);
Copy link
Contributor

@joviegas joviegas Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are using ThreadLocal can we confirm if this can come across virtual thread ?
As in if customer makes call on a virtual thread , if the impact has been investigated ?

if (traceId != null) {
executionAttributes.putAttribute(TRACE_ID, traceId);
Copy link
Contributor

@joviegas joviegas Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I was thinking about this case where client is running on host where SdkInternalThreadLocal was not cleared/not-used

1. beforeExecution():
   - SdkInternalThreadLocal.get(CONCURRENT_TRACE_ID_KEY) → null (first time)
   - Creates HashMap for Thread-1 (empty)

Now lets imagine the user is creating new request for every thread as below

for (int i = 0; i < 1_000_000; i++) {
    new Thread(() -> {
        AmazonS3 client = AmazonS3ClientBuilder.standard().build();
        client.getObject(...);  // Creates HashMap for this thread
        client.shutdown();
        // Thread dies, but not sure what will happen to the above empty HashMap instantaited
    }).start();
}

Result: 1,000,000 HashMaps created ? Can we please confirm this will not cause any leak of empty hash maps ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great call out. I didnt think about this. I changed it so that the hashmap only gets initialized when .put() is being called (initially called by lambda RIC)

}
}

@Override
public SdkHttpRequest modifyHttpRequest(Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) {
Optional<String> traceIdHeader = traceIdHeader(context);
if (!traceIdHeader.isPresent()) {
Optional<String> lambdafunctionName = lambdaFunctionNameEnvironmentVariable();
Optional<String> traceId = traceId();
Optional<String> traceId = traceId(executionAttributes);

if (lambdafunctionName.isPresent() && traceId.isPresent()) {
return context.httpRequest().copy(r -> r.putHeader(TRACE_ID_HEADER, traceId.get()));
}
}

return context.httpRequest();
}

@Override
public void afterExecution(Context.AfterExecution context, ExecutionAttributes executionAttributes) {
saveTraceId(executionAttributes);
}

@Override
public void onExecutionFailure(Context.FailedExecution context, ExecutionAttributes executionAttributes) {
saveTraceId(executionAttributes);
}

private static void saveTraceId(ExecutionAttributes executionAttributes) {
String traceId = executionAttributes.getAttribute(TRACE_ID);
if (traceId != null) {
SdkInternalThreadLocal.put(CONCURRENT_TRACE_ID_KEY, executionAttributes.getAttribute(TRACE_ID));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see SdkInternalThreadLocal we do a put and we never call remove/clear thus if TRACE_ID is unique then the map in SdkInternalThreadLocal will keep growing . How do we clear/remove CONCURRENT_TRACE_ID_KEY for request that are compete

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a thread goes back to thread pool, and a fresh request is being fired from the Lambda Runtime Client interface, it will contain a brand new traceID stored under the same key on the map so it will overwrite itself.

If the thread is killed, then when a new thread is being spun up, a clean map is created.

}
}

private Optional<String> traceIdHeader(Context.ModifyHttpRequest context) {
return context.httpRequest().firstMatchingHeader(TRACE_ID_HEADER);
}

private Optional<String> traceId() {
private Optional<String> traceId(ExecutionAttributes executionAttributes) {
Optional<String> traceId = Optional.ofNullable(executionAttributes.getAttribute(TRACE_ID));
if (traceId.isPresent()) {
return traceId;
}
return TracingSystemSetting._X_AMZN_TRACE_ID.getStringValue();
}

Expand All @@ -61,4 +93,4 @@ private Optional<String> lambdaFunctionNameEnvironmentVariable() {
return SystemSetting.getStringValueFromEnvironmentVariable(LAMBDA_FUNCTION_NAME_ENVIRONMENT_VARIABLE);
// CHECKSTYLE:ON
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import software.amazon.awssdk.http.SdkHttpMethod;
import software.amazon.awssdk.http.SdkHttpRequest;
import software.amazon.awssdk.testutils.EnvironmentVariableHelper;
import software.amazon.awssdk.utilslite.SdkInternalThreadLocal;

public class TraceIdExecutionInterceptorTest {
@Test
Expand Down Expand Up @@ -111,6 +112,78 @@ public void headerNotAddedIfNoTraceIdEnvVar() {
});
}

@Test
public void modifyHttpRequest_whenMultiConcurrencyModeWithInternalThreadLocal_shouldAddTraceIdHeader() {
EnvironmentVariableHelper.run(env -> {
resetRelevantEnvVars(env);
env.set("AWS_LAMBDA_FUNCTION_NAME", "foo");
SdkInternalThreadLocal.put("AWS_LAMBDA_X_TRACE_ID", "SdkInternalThreadLocal-trace-123");

try {
TraceIdExecutionInterceptor interceptor = new TraceIdExecutionInterceptor();
ExecutionAttributes executionAttributes = new ExecutionAttributes();

interceptor.beforeExecution(null, executionAttributes);
Context.ModifyHttpRequest context = context();

SdkHttpRequest request = interceptor.modifyHttpRequest(context, executionAttributes);
assertThat(request.firstMatchingHeader("X-Amzn-Trace-Id")).hasValue("SdkInternalThreadLocal-trace-123");
} finally {
SdkInternalThreadLocal.remove("AWS_LAMBDA_X_TRACE_ID");
}
});
}

@Test
public void modifyHttpRequest_whenMultiConcurrencyModeWithBothInternalThreadLocalAndSystemProperty_shouldUseInternalThreadLocalValue() {
EnvironmentVariableHelper.run(env -> {
resetRelevantEnvVars(env);
env.set("AWS_LAMBDA_FUNCTION_NAME", "foo");

SdkInternalThreadLocal.put("AWS_LAMBDA_X_TRACE_ID", "SdkInternalThreadLocal-trace-123");
Properties props = System.getProperties();
props.setProperty("com.amazonaws.xray.traceHeader", "sys-prop-345");

try {
TraceIdExecutionInterceptor interceptor = new TraceIdExecutionInterceptor();
ExecutionAttributes executionAttributes = new ExecutionAttributes();

interceptor.beforeExecution(null, executionAttributes);

Context.ModifyHttpRequest context = context();
SdkHttpRequest request = interceptor.modifyHttpRequest(context, executionAttributes);

assertThat(request.firstMatchingHeader("X-Amzn-Trace-Id")).hasValue("SdkInternalThreadLocal-trace-123");
} finally {
SdkInternalThreadLocal.remove("AWS_LAMBDA_X_TRACE_ID");
props.remove("com.amazonaws.xray.traceHeader");
}
});
}

@Test
public void modifyHttpRequest_whenNotInLambdaEnvironmentWithInternalThreadLocal_shouldNotAddHeader() {
EnvironmentVariableHelper.run(env -> {
resetRelevantEnvVars(env);

SdkInternalThreadLocal.put("AWS_LAMBDA_X_TRACE_ID", "should-be-ignored");

try {
TraceIdExecutionInterceptor interceptor = new TraceIdExecutionInterceptor();
ExecutionAttributes executionAttributes = new ExecutionAttributes();

interceptor.beforeExecution(null, executionAttributes);

Context.ModifyHttpRequest context = context();
SdkHttpRequest request = interceptor.modifyHttpRequest(context, executionAttributes);

assertThat(request.firstMatchingHeader("X-Amzn-Trace-Id")).isEmpty();
} finally {
SdkInternalThreadLocal.remove("AWS_LAMBDA_X_TRACE_ID");
}
});
}

private Context.ModifyHttpRequest context() {
return context(SdkHttpRequest.builder()
.uri(URI.create("https://localhost"))
Expand Down
2 changes: 2 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
<module>metric-publishers</module>
<module>release-scripts</module>
<module>utils</module>
<module>utils-lite</module>
<module>codegen-lite</module>
<module>codegen-lite-maven-plugin</module>
<module>archetypes</module>
Expand Down Expand Up @@ -665,6 +666,7 @@
<includeModule>cloudwatch-metric-publisher</includeModule>
<includeModule>emf-metric-logging-publisher</includeModule>
<includeModule>utils</includeModule>
<includeModule>utils-lite</includeModule>
<includeModule>imds</includeModule>
<includeModule>retries</includeModule>
<includeModule>retries-spi</includeModule>
Expand Down
5 changes: 5 additions & 0 deletions test/architecture-tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@
<groupId>software.amazon.awssdk</groupId>
<version>${awsjavasdk.version}</version>
</dependency>
<dependency>
<artifactId>utils-lite</artifactId>
<groupId>software.amazon.awssdk</groupId>
<version>${awsjavasdk.version}</version>
</dependency>
<dependency>
<artifactId>s3</artifactId>
<groupId>software.amazon.awssdk</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.awssdk.archtests;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;

import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;

/**
* Architecture tests for the utils-lite package to ensure it only contains allowed classes.
*/
public class UtilsLitePackageTest {

private static final JavaClasses CLASSES = new ClassFileImporter()
.importPackages("software.amazon.awssdk.utilslite");

@Test
public void utilsLitePackage_shouldOnlyContainAllowedClasses() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a requirement ?
Why is this test required ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was following @zoewangg suggestion to add a manual check to make sure we don't add classes to this package unintentionally. The sole purpose of this package as of now is to add this wrapper. If we want to expand this in the future, this presents another checkpoint.

ArchRule rule = classes()
.that().resideInAPackage("software.amazon.awssdk.utilslite")
.should().haveNameMatching(".*\\.(SdkInternalThreadLocal|SdkInternalThreadLocalTest)")
.allowEmptyShould(true)
.because("utils-lite package should only contain SdkInternalThreadLocal and its test");

rule.check(CLASSES);
}
}
Loading
Loading