Skip to content

[DDB Mapper V2] Add UserAgent - #7480

Merged
alextwoods merged 9 commits into
feature/master/DDB-mapperv2from
alexwoo/ddb_mapperv2_useragent
Oct 23, 2026
Merged

alextwoods merged 9 commits into
feature/master/DDB-mapperv2from
alexwoo/ddb_mapperv2_useragent

Conversation

@alextwoods

@alextwoods alextwoods commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Note: This PR merges to the DDB MapperV2 feature branch and NOT to master!

Motivation and Context

This PR adds user agent tracking for the v2 DynamoDB mapper using the business metrics feature. Every request the mapper sends is tagged with the reserved DYNAMODB_MAPPER_V2 (Ac) feature ID. This keeps v2 mapper usage separate from the Enhanced Client's (DDB_MAPPER / d).

Modifications

  • Adds the new feature ID (Ac) to BusinessMetricFeatureId
  • Adds an internal helper, MapperUserAgent.applyUserAgent(builder), which adds ApiName("sdk-metrics", "Ac") to each request builder before the mapper builds and sends the request. This ports v1's DynamoDBMapper.applyUserAgent. It is applied at all 17 client call sites in DynamoDBMapper, PaginatedQueryList, PaginatedScanList and ParallelScanTask. That covers every query/scan page, parallel scan segments, count loops, transactions, and batch retries for unprocessed items/keys.

Testing

Adds new unit tests to cover.

Also adds benchmarking to confirm the overhead of the interceptor is minimal. See results below.

Benchmark results

There is no overhead above measurement noise. For requests the mapper builds itself, the helper does one null check and sets one shared reference on the request builder. There is no request copy.

V2MapperRealClientOverheadBenchmark drives the mapper's load (GetItem) and save (PutItem) through a real DynamoDbClient backed by MockHttpClient, so the full client pipeline runs but there is no network I/O. Arms: with = this PR; none = the same jar with applyUserAgent replaced by a no-op. Each arm was verified before timing (Ac on mapper requests only in "with", absent in "none"). JMH -f 1 -wi 3 -i 5 -w 1s -r 2s, 3 interleaved repetitions per arm, Apple M4 Pro, JDK 21.

Benchmark none µs/op with µs/op Δ time/op
v2MapperGet[TINY] 10.51 11.12 +5.8%
v2MapperGet[SMALL] 10.96 11.53 +5.3%
v2MapperGet[HUGE] 24.58 25.25 +2.8%
v2MapperGet[HUGE_FLAT] 19.38 19.82 +2.3%
v2MapperPut[TINY] 9.93 10.26 +3.3%
v2MapperPut[SMALL] 10.90 10.93 +0.3%
v2MapperPut[HUGE] 26.73 26.66 -0.3%
v2MapperPut[HUGE_FLAT] 21.68 22.18 +2.3%

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

License

  • I confirm that this pull request can be released under the Apache 2 license

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.
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.
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.
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).
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.
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.
@alextwoods
alextwoods requested a review from a team as a code owner October 23, 2026 15:11
@alextwoods alextwoods added the changelog-not-required Indicate changelog entry is not required for a specific PR label Oct 23, 2026
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.
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.
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.
@alextwoods
alextwoods requested a review from dagnir October 23, 2026 17:59
@alextwoods
alextwoods merged commit 20bf88c into feature/master/DDB-mapperv2 Oct 23, 2026
5 of 6 checks passed
@github-actions

Copy link
Copy Markdown

This pull request has been closed and the conversation has been locked. Comments on closed PRs are hard for our team to see. If you need more assistance, please open a new issue that references this one.

@github-actions github-actions Bot locked as resolved and limited conversation to collaborators Oct 23, 2026
@alextwoods
alextwoods deleted the alexwoo/ddb_mapperv2_useragent branch October 23, 2026 21:10
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

changelog-not-required Indicate changelog entry is not required for a specific PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants