Skip to content

Commit 20bf88c

Browse files
authored
[DDB Mapper V2] Add UserAgent (#7480)
* chore(dynamodb-mapper): Fix parent snapshot version The latest master merge bumped the tree to 2.55.14-SNAPSHOT, but the mapper pom was left on 2.55.5-SNAPSHOT, so no module in the reactor could be built (Non-resolvable import POM bom-internal 2.55.5-SNAPSHOT). Mirrors the earlier fix in #7398. Verified: mvn -o validate -pl :dynamodb-mapper,:sdk-core and mvn clean install -pl :dynamodb-mapper -P quick --am both BUILD SUCCESS. * feat(sdk-core): Add DYNAMODB_MAPPER_V2 business metric Sync the already-reserved "Ac" feature ID (DYNAMODB_MAPPER_V2) from the AwsDrSeps FEATURES.md source of truth. It identifies operations called through the v2 DynamoDB mapper and is distinct from the enhanced client's DDB_MAPPER ("d"). VALUE_MAP is case-sensitive, so "Ac" does not collide with CREDENTIALS_PROFILE_LOGIN ("AC"). Verified: - mvn clean install -pl :sdk-core -P quick: BUILD SUCCESS - env -u AWS_SDK_UA_APP_ID mvn clean install -pl :sdk-core: BUILD SUCCESS (1618 tests, 0 failures, 6 skipped; 0 checkstyle; 0 spotbugs). AWS_SDK_UA_APP_ID is unset because the IDE shell exports it, which breaks 3 AppIdResolutionTest cases independent of this change. * feat(dynamodb-mapper): Add DYNAMODB_MAPPER_V2 user agent metric Port the enhanced client's ApplyUserAgentInterceptor to the v2 mapper. Its modifyRequest adds an "sdk-metrics"/"Ac" ApiName to the override configuration of every DynamoDbRequest, preserving any existing override configuration, so mapper usage shows up as the DYNAMODB_MAPPER_V2 business metric rather than the enhanced client's DDB_MAPPER ("d"). The interceptor is registered through the classpath resource software/amazon/awssdk/services/dynamodb/execution.interceptors, like the enhanced client, because the mapper always receives a user-built immutable DynamoDbClient. A GraalVM reflect-config.json mirrors the enhanced module. Caveat: as with the enhanced client, this tags every DynamoDbRequest sent by any DynamoDbClient in a JVM that has the dynamodb-mapper jar on its classpath, mapper-issued or not. Shaded fat jars need an AppendingTransformer for that resource path. Tests: - ApplyUserAgentInterceptorTest (3): adds Ac (not d), preserves an existing ApiName and header, leaves non-DynamoDB requests untouched. - DynamoDBMapperUserAgentTest (18): builds a DynamoDbClient with no explicit interceptor and a capturing SdkHttpClient, runs every mapper operation family (load, save update/put, delete, query/queryPage/ next page/count, scan/scanPage/next page/count, parallelScan, batchLoad, batchSave, transactionWrite, transactionLoad, table mapper load) and asserts every request's User-Agent m/ section contains Ac and not d. Verified: - mvn clean install -pl :dynamodb-mapper (non-quick): BUILD SUCCESS, 748 tests, 0 failures; spotbugs 0 bugs; dependency analyze-only clean. - mvn javadoc:javadoc -pl :dynamodb-mapper: 0 errors. - Installed jar contains the interceptor class, execution.interceptors and reflect-config.json; reflect-config.json is valid JSON. - Negative check: with execution.interceptors emptied, DynamoDBMapperUserAgentTest fails 18/18 (User-Agent had m/D,AK,P,e and no Ac); file restored afterwards. * chore(sdk-benchmarks): Append DynamoDB interceptors in jar dynamodb-enhanced and dynamodb-mapper both ship software/amazon/awssdk/services/dynamodb/execution.interceptors. The benchmarks uber-jar had no AppendingTransformer for that path, so maven-shade kept only one file (the mapper's, declared first) and silently dropped the enhanced client's interceptor, which would skew any before/after comparison. Mirrors test/s3-benchmarks. Verified: mvn clean install -pl :sdk-benchmarks -P quick --am is BUILD SUCCESS, and the benchmarks.jar entry now lists both the mapper and enhanced ApplyUserAgentInterceptor on separate lines (shade inserts a blank line between them, which ClasspathInterceptorChainFactory skips). * test(sdk-benchmarks): Add v2 mapper interceptor benchmarks The V1 mapper comparison benchmarks drive the v2 mapper through V2TestDynamoDb*Client stubs that implement DynamoDbClient directly, so the client pipeline and its classpath execution interceptors never run and cannot show the cost of the mapper's ApplyUserAgentInterceptor. - V2MapperRealClientOverheadBenchmark: v2 mapper load/save(PUT) through a real DynamoDbClient backed by MockHttpClient, for the TINY/SMALL/HUGE/HUGE_FLAT items. - V2MapperApplyUserAgentInterceptorBenchmark: direct cost of modifyRequest on GetItem and PutItem requests of several sizes, with and without a pre-existing override configuration, against a no-op baseline. Verified: mvn clean install -pl :sdk-benchmarks (checkstyle, spotbugs) BUILD SUCCESS; both benchmarks run from target/benchmarks.jar. * fix(dynamodb-mapper): Rename mapper user agent interceptor ClasspathInterceptorChainFactory de-duplicates classpath interceptors by simple class name. The mapper's interceptor shared the simple name ApplyUserAgentInterceptor with the enhanced client's, so with both jars on the classpath only one survived and either the mapper's Ac metric or the enhanced client's d metric was silently dropped. Rename it to ApplyMapperUserAgentInterceptor and assert in DynamoDBMapperUserAgentTest that the enhanced client interceptor on the test classpath still runs alongside it. * refactor(dynamodb-mapper): Tag each mapper request Replace the classpath execution interceptor with per-request tagging, ported from v1's DynamoDBMapper.applyUserAgent. The new internal MapperUserAgent.applyUserAgent sets one shared override configuration carrying the sdk-metrics/Ac API name on each request builder the mapper creates, before build(). This addresses two review findings on the interceptor approach: - Bundle shading collision: dynamodb-enhanced and dynamodb-mapper both shipped services/dynamodb/execution.interceptors. The bundle jar and customer uber-jars keep only one copy, so one of the d and Ac metrics was silently lost. The mapper no longer ships that resource. - Over-attribution: a classpath interceptor tagged every DynamoDB client request in the JVM, including direct low-level, async and enhanced client calls. Now only requests the mapper sends carry Ac. Batch and transaction requests carry Ac too. v1 used separate user agent strings for them, but only one feature ID is reserved. Every client call site is tagged: get, put, update, delete, batch write (each unprocessed-items retry builds a tagged request), batch get, transact write/get, and the scan/query request factories. Later pages, count loops, parallel scan segments and batch get retries keep the tag through toBuilder(). The public PaginatedQueryList and PaginatedScanList, and ParallelScanTask, also tag each next-page builder, as v1 did, because their constructors accept requests the mapper did not build. Remove the interceptor, its execution.interceptors and reflect-config resources, its unit test, and the V2MapperApplyUserAgentInterceptor micro-benchmark that depends on it. DynamoDBMapperUserAgentTest now also covers unprocessed-item retries for batch write and batch get, multi-page count and parallel scan, and batch delete. It adds a negative test that direct calls on a client shared with the mapper do not carry Ac, and drops the hll/ddb-enh assertion that only guarded the interceptor name collision. * chore(sdk-benchmarks): Drop AppendingTransformer Revert the AppendingTransformer for services/dynamodb/execution.interceptors added in 5e9840f. It worked around the bundle shading collision for the benchmarks jar only. The mapper now tags its own requests instead of shipping that resource, so dynamodb-enhanced is again its only provider and the default shade behavior keeps it. Update the V2MapperRealClientOverheadBenchmark javadoc, which still described the removed classpath interceptor. * fix(dynamodb-mapper): Merge mapper user agent tag Per-request tagging replaced the classpath interceptor to fix the bundle shading collision and the over-attribution of non-mapper requests. Its helper replaced the whole request override configuration, though. PaginatedQueryList and PaginatedScanList are public and accept caller-built requests, so a caller's override (credentials provider, timeouts, headers, API names) was dropped from page 2 onward. applyUserAgent now merges, as v1 did: - no override: set the shared override, so the common path copies nothing; - override already has sdk-metrics/Ac: leave it unchanged, so pages derived from each other don't accumulate one Ac per page; - otherwise: add the API name to a copy of the existing override. Add tests that caller-built paginated lists keep the caller's override on every later page and carry Ac exactly once.
1 parent 1380858 commit 20bf88c

10 files changed

Lines changed: 853 additions & 15 deletions

File tree

‎core/sdk-core/src/main/java/software/amazon/awssdk/core/useragent/BusinessMetricFeatureId.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ public enum BusinessMetricFeatureId {
6161
HTTP_CLIENT_EXPLICIT_INSTANCE("AK"),
6262
HTTP_CLIENT_EXPLICIT_FACTORY("AL"),
6363
DDB_MAPPER("d"),
64+
DYNAMODB_MAPPER_V2("Ac"),
6465
BEARER_SERVICE_ENV_VARS("3"),
6566
CREDENTIALS_CODE("e"),
6667
CREDENTIALS_JVM_SYSTEM_PROPERTIES("f"),

‎services-custom/dynamodb-mapper/pom.xml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<parent>
2222
<groupId>software.amazon.awssdk</groupId>
2323
<artifactId>services-custom</artifactId>
24-
<version>2.55.5-SNAPSHOT</version>
24+
<version>2.55.14-SNAPSHOT</version>
2525
</parent>
2626
<artifactId>dynamodb-mapper</artifactId>
2727
<name>AWS Java SDK :: DynamoDB :: Mapper</name>

‎services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMapper.java‎

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
import java.util.Set;
9696
import java.util.function.Consumer;
9797

98+
import static software.amazon.awssdk.mapper.dynamodb.internal.MapperUserAgent.applyUserAgent;
9899
import static software.amazon.awssdk.services.dynamodb.model.KeyType.HASH;
99100
import static software.amazon.awssdk.services.dynamodb.model.KeyType.RANGE;
100101
import static software.amazon.awssdk.mapper.dynamodb.TransactionWriteRequest.TransactionWriteOperation;
@@ -446,7 +447,7 @@ public <T extends Object> T load(T keyObject, DynamoDBMapperConfig config) {
446447

447448
Map<String, AttributeValue> key = model.convertKey(keyObject);
448449

449-
GetItemRequest rq = GetItemRequest.builder()
450+
GetItemRequest rq = applyUserAgent(GetItemRequest.builder())
450451
.key(key)
451452
.tableName(tableName)
452453
.consistentRead(config.getConsistentReads() == ConsistentReads.CONSISTENT)
@@ -876,7 +877,7 @@ protected List<ValueUpdate> getInMemoryUpdates() {
876877
* the returned attributes to detect silent failure on the server-side.
877878
*/
878879
protected UpdateItemResponse doUpdateItem() {
879-
UpdateItemRequest req = UpdateItemRequest.builder()
880+
UpdateItemRequest req = applyUserAgent(UpdateItemRequest.builder())
880881
.tableName(getTableName())
881882
.key(getPrimaryKeyAttributeValues())
882883
.attributeUpdates(
@@ -913,7 +914,7 @@ protected PutItemResponse doPutItem() {
913914
this.clazz,
914915
getTableName(),
915916
saveConfig));
916-
PutItemRequest req = PutItemRequest.builder()
917+
PutItemRequest req = applyUserAgent(PutItemRequest.builder())
917918
.tableName(getTableName())
918919
.item(attributeValues)
919920
.expected(mergeExpectedAttributeValueConditions())
@@ -1084,7 +1085,7 @@ public <T> void delete(T object, DynamoDBDeleteExpression deleteExpression, Dyna
10841085
}
10851086
}
10861087

1087-
DeleteItemRequest.Builder reqBuilder = DeleteItemRequest.builder()
1088+
DeleteItemRequest.Builder reqBuilder = applyUserAgent(DeleteItemRequest.builder())
10881089
.key(key)
10891090
.tableName(tableName);
10901091

@@ -1141,7 +1142,7 @@ public void transactionWrite(TransactionWriteRequest transactionWriteRequest, Dy
11411142

11421143
}
11431144

1144-
TransactWriteItemsRequest transactWriteItemsRequest = TransactWriteItemsRequest.builder()
1145+
TransactWriteItemsRequest transactWriteItemsRequest = applyUserAgent(TransactWriteItemsRequest.builder())
11451146
.clientRequestToken(transactionWriteRequest.getIdempotencyToken())
11461147
.transactItems(transactWriteItems)
11471148
.build();
@@ -1189,7 +1190,7 @@ public List<Object> transactionLoad(TransactionLoadRequest transactionLoadReques
11891190
transactGetItems.add(TransactGetItem.builder().get(getItem.build()).build());
11901191
}
11911192

1192-
TransactGetItemsRequest transactGetItemsRequest = TransactGetItemsRequest.builder()
1193+
TransactGetItemsRequest transactGetItemsRequest = applyUserAgent(TransactGetItemsRequest.builder())
11931194
.transactItems(transactGetItems)
11941195
.build();
11951196
TransactGetItemsResponse transactGetItemsResult = db.transactGetItems(transactGetItemsRequest);
@@ -1359,7 +1360,7 @@ private FailedBatch doBatchWriteItemWithRetry(
13591360

13601361
while (true) {
13611362
try {
1362-
result = db.batchWriteItem(BatchWriteItemRequest.builder().requestItems(pendingItems).build());
1363+
result = db.batchWriteItem(applyUserAgent(BatchWriteItemRequest.builder()).requestItems(pendingItems).build());
13631364
} catch (Exception e) {
13641365
failedBatch = new FailedBatch();
13651366
failedBatch.setUnprocessedItems(pendingItems);
@@ -1472,7 +1473,7 @@ private void processBatchGetRequest(
14721473
final DynamoDBMapperConfig config) {
14731474

14741475
BatchGetItemResponse batchGetItemResult = null;
1475-
BatchGetItemRequest batchGetItemRequest = BatchGetItemRequest.builder()
1476+
BatchGetItemRequest batchGetItemRequest = applyUserAgent(BatchGetItemRequest.builder())
14761477
.requestItems(requestItems)
14771478
.build();
14781479

@@ -1488,6 +1489,7 @@ private void processBatchGetRequest(
14881489
batchLoadContext.setRetriesAttempted(retries);
14891490
if (!isNullOrEmpty(batchGetItemResult.unprocessedKeys())) {
14901491
pause(batchLoadStrategy.getDelayBeforeNextRetry(batchLoadContext));
1492+
// toBuilder() keeps the user agent tag on the retry of the unprocessed keys.
14911493
batchGetItemRequest = batchGetItemRequest.toBuilder()
14921494
.requestItems(batchGetItemResult.unprocessedKeys())
14931495
.build();
@@ -1731,7 +1733,9 @@ public <T> int count(Class<T> clazz, DynamoDBQueryExpression<T> queryExpression,
17311733
* @param config never null
17321734
*/
17331735
private ScanRequest createScanRequestFromExpression(Class<?> clazz, DynamoDBScanExpression scanExpression, DynamoDBMapperConfig config) {
1734-
return ScanRequest.builder()
1736+
// Every scan path (scan, scanPage, count, parallelScan and later pages) derives its requests from this one
1737+
// with toBuilder(), which keeps the user agent tag.
1738+
return applyUserAgent(ScanRequest.builder())
17351739
.tableName(getTableName(clazz, config))
17361740
.indexName(scanExpression.getIndexName())
17371741
.scanFilter(scanExpression.getScanFilter())
@@ -1781,7 +1785,9 @@ protected <T> QueryRequest createQueryRequestFromExpression(Class<T> clazz,
17811785

17821786
DynamoDBMapperTableModel<T> model = getTableModel(clazz, config);
17831787

1784-
QueryRequest.Builder req = QueryRequest.builder();
1788+
// Every query path (query, queryPage, count and later pages) derives its requests from this one with
1789+
// toBuilder(), which keeps the user agent tag.
1790+
QueryRequest.Builder req = applyUserAgent(QueryRequest.builder());
17851791
req.consistentRead(xpress.isConsistentRead());
17861792
req.tableName(getTableName(clazz, xpress.getHashKeyValues(), config));
17871793
req.indexName(xpress.getIndexName());

‎services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/PaginatedQueryList.java‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
package software.amazon.awssdk.mapper.dynamodb;
1717

18+
import static software.amazon.awssdk.mapper.dynamodb.internal.MapperUserAgent.applyUserAgent;
19+
1820
import software.amazon.awssdk.annotations.SdkPublicApi;
1921
import java.util.List;
2022

@@ -85,7 +87,9 @@ protected boolean atEndOfResults() {
8587

8688
@Override
8789
protected synchronized List<T> fetchNextPage() {
88-
queryRequest = queryRequest.toBuilder().exclusiveStartKey(queryResult.lastEvaluatedKey()).build();
90+
// Tag here too: the public constructor accepts a request the mapper did not build. applyUserAgent keeps the
91+
// caller's override configuration and adds the tag only once.
92+
queryRequest = applyUserAgent(queryRequest.toBuilder()).exclusiveStartKey(queryResult.lastEvaluatedKey()).build();
8993
queryResult = dynamo.query(queryRequest);
9094
return mapper.marshallIntoObjects(mapper.toParameters(
9195
queryResult.items(),

‎services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/PaginatedScanList.java‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
package software.amazon.awssdk.mapper.dynamodb;
1717

18+
import static software.amazon.awssdk.mapper.dynamodb.internal.MapperUserAgent.applyUserAgent;
19+
1820
import software.amazon.awssdk.annotations.SdkPublicApi;
1921
import java.util.List;
2022

@@ -84,7 +86,9 @@ protected boolean atEndOfResults() {
8486

8587
@Override
8688
protected synchronized List<T> fetchNextPage() {
87-
scanRequest = scanRequest.toBuilder().exclusiveStartKey(scanResult.lastEvaluatedKey()).build();
89+
// Tag here too: the public constructor accepts a request the mapper did not build. applyUserAgent keeps the
90+
// caller's override configuration and adds the tag only once.
91+
scanRequest = applyUserAgent(scanRequest.toBuilder()).exclusiveStartKey(scanResult.lastEvaluatedKey()).build();
8892
scanResult = dynamo.scan(scanRequest);
8993
return mapper.marshallIntoObjects(mapper.toParameters(
9094
scanResult.items(),

‎services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/ParallelScanTask.java‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
package software.amazon.awssdk.mapper.dynamodb;
1717

18+
import static software.amazon.awssdk.mapper.dynamodb.internal.MapperUserAgent.applyUserAgent;
19+
1820
import software.amazon.awssdk.annotations.SdkInternalApi;
1921
import software.amazon.awssdk.core.exception.SdkClientException;
2022
import software.amazon.awssdk.core.exception.SdkException;
@@ -226,11 +228,14 @@ else if (currentSegmentState == SegmentScanState.Waiting
226228

227229
private ScanResponse scanNextPageOfSegment(int currentSegment, boolean checkLastEvaluatedKey) {
228230
ScanRequest segmentScanRequest = parallelScanRequests.get(currentSegment);
231+
// The mapper already tags the segment requests. Like v1, this also tags each segment page here, which costs
232+
// nothing because the builder already exists.
229233
if (checkLastEvaluatedKey) {
230234
ScanResponse lastScanResult = segmentScanResults.get(currentSegment);
231-
segmentScanRequest = segmentScanRequest.toBuilder().exclusiveStartKey(lastScanResult.lastEvaluatedKey()).build();
235+
segmentScanRequest = applyUserAgent(segmentScanRequest.toBuilder())
236+
.exclusiveStartKey(lastScanResult.lastEvaluatedKey()).build();
232237
} else {
233-
segmentScanRequest = segmentScanRequest.toBuilder().exclusiveStartKey(null).build();
238+
segmentScanRequest = applyUserAgent(segmentScanRequest.toBuilder()).exclusiveStartKey(null).build();
234239
}
235240
ScanResponse scanResult = dynamo.scan(segmentScanRequest);
236241

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.mapper.dynamodb.internal;
17+
18+
import software.amazon.awssdk.annotations.SdkInternalApi;
19+
import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration;
20+
import software.amazon.awssdk.core.ApiName;
21+
import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId;
22+
import software.amazon.awssdk.services.dynamodb.model.DynamoDbRequest;
23+
24+
/**
25+
* Tags the DynamoDB requests that the mapper builds with the {@link BusinessMetricFeatureId#DYNAMODB_MAPPER_V2}
26+
* business metric, so the metric is only reported for requests the mapper itself sends. This is the v2 port of v1's
27+
* {@code DynamoDBMapper.applyUserAgent}.
28+
* <p>
29+
* v1 used separate user agent strings for batch and transaction operations. Only one feature ID is reserved for the
30+
* v2 mapper, so every mapper request, including batch and transaction requests, carries the same metric.
31+
*/
32+
@SdkInternalApi
33+
public final class MapperUserAgent {
34+
35+
private static final ApiName API_NAME = ApiName.builder()
36+
.name("sdk-metrics")
37+
.version(BusinessMetricFeatureId.DYNAMODB_MAPPER_V2.value())
38+
.build();
39+
40+
/**
41+
* Shared, immutable override configuration carrying only the mapper's {@code sdk-metrics} API name. It is used
42+
* as-is for builders that have no override configuration, which covers every request the mapper builds itself.
43+
*/
44+
private static final AwsRequestOverrideConfiguration USER_AGENT_OVERRIDE =
45+
AwsRequestOverrideConfiguration.builder()
46+
.addApiName(API_NAME)
47+
.build();
48+
49+
private MapperUserAgent() {
50+
}
51+
52+
/**
53+
* Adds the mapper's {@code sdk-metrics} API name to a request builder's override configuration, before it is
54+
* built.
55+
* <p>
56+
* Any override configuration already on the builder is kept, so this is safe on requests the mapper did not build
57+
* (for example, a caller-built request passed to a public paginated list). The cases are:
58+
* <ul>
59+
* <li>No override configuration: the shared {@code USER_AGENT_OVERRIDE} is set, so the common path copies
60+
* nothing.</li>
61+
* <li>The override configuration already has the API name: the builder is left unchanged. This keeps requests
62+
* derived page by page from accumulating one API name per page.</li>
63+
* <li>Otherwise: the API name is added to a copy of the existing override configuration.</li>
64+
* </ul>
65+
* Requests later derived from the built request with {@code toBuilder()} keep the tag.
66+
*
67+
* @param builder the request builder to tag
68+
* @param <T> the request builder type
69+
* @return the same builder, for method chaining
70+
*/
71+
public static <T extends DynamoDbRequest.Builder> T applyUserAgent(T builder) {
72+
AwsRequestOverrideConfiguration existing = builder.overrideConfiguration();
73+
if (existing == null) {
74+
builder.overrideConfiguration(USER_AGENT_OVERRIDE);
75+
} else if (!existing.apiNames().contains(API_NAME)) {
76+
builder.overrideConfiguration(existing.toBuilder().addApiName(API_NAME).build());
77+
}
78+
return builder;
79+
}
80+
}

0 commit comments

Comments
 (0)