Skip to content

[Do not merge] Feature/master/endpoints bdd s3 ddb staging - #7339

Open
alextwoods wants to merge 10 commits into
masterfrom
feature/master/endpoints-bdd-s3-ddb-staging
Open

[Do not merge] Feature/master/endpoints bdd s3 ddb staging#7339
alextwoods wants to merge 10 commits into
masterfrom
feature/master/endpoints-bdd-s3-ddb-staging

Conversation

@alextwoods

Copy link
Copy Markdown
Contributor

Branch for testing only.

Motivation and Context

Modifications

Testing

Screenshots (if appropriate)

Types of changes

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

Checklist

  • I have read the CONTRIBUTING document
  • Local run of mvn install succeeds
  • My code follows the code style of this project
  • My change requires a change to the Javadoc documentation
  • I have updated the Javadoc documentation accordingly
  • I have added tests to cover my changes
  • All new and existing tests passed
  • I have added a changelog entry. Adding a new entry must be accomplished by running the scripts/new-change script and following the instructions. Commit the new file created by the script in .changes/next-release with your changes.
  • My change is to implement 1.11 parity feature and I have updated LaunchChangelog

License

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

Add the foundational infrastructure for BDD (Binary Decision Diagram)
endpoint resolution without the code generation visitors themselves:

Model loading:
- EndpointBddModel: parse BDD JSON (endpoint-bdd-1.json) with node
  graph, conditions, results, and parameters
- C2jModels/IntermediateModel: wire EndpointBddModel into the codegen
  model pipeline
- GenerationMojo: discover endpoint-bdd-1.json in codegen-resources

Expression/type extensions:
- ExpressionParser: support parseExpressionFrom for BDD result
  templates, negative indexed access, direct negative index syntax
- Tokenizer: add directNegativeIndexedAccess and consumeDirectNegative
  support for BDD template expressions
- RuleRuntimeTypeMirror: add LIST_OF_STRING, coalesce return type
- EndpointUrlCodeEmitter: add urlFromExpression for BDD url emission
- StringConcatExpression: add needsParenthesization flag

Runtime additions (RulesFunctions.java.resource):
- coalesce(T... args) / coalesce(String... args)
- split(value, delimiter, limit)
- ite(condition, ifTrue, ifFalse)
- listAccess: support negative indices

Supporting:
- EndpointRulesSpecUtils: simplify rulesEngineFilesFromDirectory
- EndpointProviderTestSpec: BDD test case loading support
- EndpointProviderTestCase: add bddEndpointProvider field
* feat(codegen): Add BDD endpoint resolver code generation

Implement the BDD endpoint resolver code generation visitors that
transform a Binary Decision Diagram into a Java endpoint provider.

Code generation visitors:
- BddEndpointProviderSpec: main spec emitting the provider class with
  nodeP/nodeN method dispatch — each BDD node becomes a method
  returning Endpoint directly (null = no match, throw = error)
- BddResultCodeGeneratorVisitor: traverses the BDD node graph and
  emits node methods with condition checks and edge traversal
- ConditionFnCodeGeneratorVisitor: emits condition evaluations
  (isSet, booleanEquals, stringEquals, function calls, getAttr)
- ResultFnCodeGeneratorVisitor: emits endpoint construction and error
  results from BDD result nodes
- AssignTypeInferringVisitor: infers types for BDD register variables
  from the condition/result expressions
- RegistryInfo: metadata for BDD registers (name, type, index)

Wiring:
- EndpointProviderTasks: when EndpointBddModel is present, generate
  BddEndpointProvider instead of rules-based provider

The generated resolver uses direct method dispatch per BDD node,
with simple conditions inlined as ternary expressions for
branch-predictor-friendly code paths.

* fix(codegen): Fix null Boolean handling in BDD condition visitor

Boolean.FALSE != x treats null as true, which is incorrect per the
endpoint rules spec (booleanEquals(null, true) should be false).
Use Boolean.TRUE.equals(x) instead, which correctly returns false
when x is null.
…7304)

* perf(endpoints): Add peephole optimizations to BDD endpoint codegen

Reduce allocations and method dispatch in BDD-generated endpoint
resolvers. All codegen changes are scoped to the BDD path; the
tree-based rules code generation is untouched.

New BddPeepholeVisitor (BDD-only IR pass, runs before
PrepareForCodegenVisitor) rewrites:

- stringEquals(coalesce(substring(str, 0, N, false), ""), literal)
  -> str.startsWith(literal)
- stringEquals(coalesce(substring(str, 0, N, true), ""), literal)
  -> str.endsWith(literal)
- stringEquals(coalesce(substring(str, X, Y, rev), ""), literal)
  -> str.regionMatches(offset, literal, ...) for interior checks
- stringEquals(a, b) -> a.equals(b), skipping RulesFunctions dispatch
- coalesce(boolExpr, default) -> (expr != null ? expr : default)
- ite(cond, a, b) -> (cond ? a : b)
- isValidHostLabel(str, constBool) -> specialized runtime helper

The BDD condition and result generators emit these synthetic forms as
inline Java. Synthetic names are prefixed with __ so they cannot
collide with endpoint rule standard library functions.

RulesFunctions (shared generated runtime) additions:
- awsPartition(region) gains a per-thread last-value cache. The same
  region resolves on nearly every call in steady state, so this avoids
  a HashMap lookup plus regex fallback each time.
- isValidHostLabelSingle / isValidHostLabelMulti skip the allowDots
  branch when the flag is known at codegen time.

On the S3 test BDD this eliminates all RulesFunctions.stringEquals,
ite, coalesce, and isValidHostLabel dispatches, replacing them with 11
native String comparisons and 9 specialized host-label calls.

Verified: 690 codegen tests (3 pre-existing EndpointRulesClientTestSpec
failures also present on the base branch), rules2 golden files
byte-identical to base, and all 32 bddendpoints endpoint test cases
passing against the generated BDD resolver.

* test(codegen): Update endpoint provider test fixtures for test case names

EndpointProviderTestSpec was changed to pass each endpoint test case's
documentation string to the EndpointProviderTestCase constructor, so
generated test cases are named in test output instead of appearing as
anonymous entries. The three affected golden files were not updated
alongside that change, leaving EndpointRulesClientTestSpecTest failing
on the base branch.

Adds the expected name argument to the fixtures. Test-resource only —
no production code change.

* fix(endpoints): Make BDD peephole spec-faithful

Addresses review findings on the BDD endpoint codegen peephole pass and
the awsPartition memo. Two of the four are silent-wrong-endpoint or
new-exception risks, so they are grouped as one verified changeset rather
than split across commits that share hunks in the same files.

Substring peephole diverged from spec substring semantics. The pass
rewrote stringEquals(coalesce(substring(str, X, Y, rev), ""), literal)
into inlined startsWith/endsWith/regionMatches, none of which reproduce
substring's rejection of input containing a non-ASCII character. A bucket
name with a non-ASCII character anywhere would take a different rule
branch than the spec dictates. This fires today on the S3 BDD model: a
name ending in --x-s3 would be routed to an S3 Express endpoint with
sigv4-s3express instead of a regular S3 endpoint with SigV4. Replaced the
three emitters with a single RulesFunctions.substringEquals helper that
mirrors substring exactly. The positional comparison runs first and fails
for almost every input, so the O(n) ASCII scan is only reached once the
characters already match, which is where substring paid the same cost.
The rewrite is skipped for an empty literal, where coalesce makes the
spec comparison true even for null input.

stringEquals lost null-safety for two nullable operands. The fallback
emitted left.equals(right) whenever neither side was a string constant,
throwing NullPointerException where the spec's stringEquals returns
false. Reachable in the S3 BDD via stringEquals(region,
bucketArn.region()) and two others. Removed the fallback; the pass now
leaves plain comparisons to PrepareForCodegenVisitor, which already
handles the constant cases identically and correctly keeps the null-safe
RulesFunctions.stringEquals call otherwise.

awsPartition ThreadLocal memo replaced with precomputed instances.
RulesFunctions is copied into every service, so a static ThreadLocal
holding a service-loaded RulePartition is the container classloader-leak
pattern Tomcat warns about, and allocates per virtual thread with a
near-zero hit rate. Benchmarking a volatile single-slot alternative
showed it is worse than no cache at all once concurrent threads resolve
different regions (122 ns/op vs 3.0 ns/op at 4 threads, from cache-line
contention on the write). RulePartition is immutable and a pure function
of its Partition, of which there are a handful loaded once, so the
instances are now built up front in loadPartitionData and shared. That
removes the allocation the memo existed to avoid rather than caching
around it: 1.65-2.52 ns/op against a 2.93-3.50 ns/op baseline, flat
across region count and thread count, with no per-call state. The
now-redundant AWS_PARTITION LazyValue is gone.

Removed dead peephole emitters from BddResultCodeGeneratorVisitor. All
six were copy-pasted from ConditionFnCodeGeneratorVisitor but are
unreachable: the BDD hoists computation into conditions, so results only
consume assigned registers, and the visitor already rejects conditions
and let-bindings outright. Dropping the pass from the result path leaves
generated output byte-identical for all three BDD test models.

Verified: 698 codegen tests, 393 S3 and 574 DynamoDB generated endpoint
conformance tests, and a 1670-case differential test asserting
substringEquals is indistinguishable from the spec composition across
Latin-1, CJK and surrogate-pair inputs. Checkstyle and SpotBugs clean.

* fix(endpoints): Address BDD peephole review findings

Applies seven findings from the PR #7304 review. The two correctness ones
are the same class as the bugs the previous commit fixed: an invariant
asserted in a comment rather than enforced in code.

isAlwaysNonNull elided a register null-check on an unenforced assumption
(F1). It returned true for any __ite by name, while the comment claimed
"a ternary between two string literals". simplifyIte only checks arity,
RuleRuntimeTypeMirror types both branches as STRING, and the BDD model is
produced by the endpoint compiler rather than by this repo, so a
{"ref": ...} branch is legal input and can be null. For ite(cond, ref, "")
the assign condition reported success with a null register, flipping a BDD
edge and resolving an endpoint the spec does not permit. Both branches are
now checked. The three literal-branch ite nodes in the S3 BDD keep the
elided check, so the optimization is retained where it is provable.

emitCoalesceBoolean evaluated its operand twice (F3). It emitted
(expr != null ? expr : default), so a non-trivial operand such as a nested
rules function ran twice per evaluation - a pessimization inside a
performance change. Replaced with wrapper equality, which is exactly
equivalent for both defaults, evaluates the subject once, and has no branch
and no boxing: coalesce(x, false) becomes Boolean.TRUE.equals(x) and
coalesce(x, true) becomes !Boolean.FALSE.equals(x).

Removed the unreachable STRING_CONCAT receiver handling (F5). All three
MethodCallExpression producers are in PrepareForCodegenVisitor and put
either a string constant or a boolean-typed expression on the receiver,
never a concatenation. It was needed by the left.equals(right) fallback
removed in the previous commit. With it and a stray blank line gone,
BddResultCodeGeneratorVisitor is byte-identical to the base branch and
drops out of the PR diff entirely.

awsPartition returned null where it previously threw (F6). Generated code
null-checks the result, so an unloadable 'aws' partition silently made the
condition false and took a different rule branch instead of failing.
loadPartitionData now validates it and throws SdkClientException naming
partitions.json. Restructuring for that also lets PartitionData hold all
final fields, removing the non-final awsRulePartition that the previous
commit had exposed to LazyValue's unsafe publication.

Deduplicated host-label validation (F4). isValidHostLabelMulti was a
character-for-character copy of the allowDots branch in isValidHostLabel,
in a template copied into every service module, so the two could drift
independently. Inverted the delegation: isValidHostLabel now dispatches to
the two specialized helpers and there is one copy of each algorithm. Also
dropped the redundant length guard in isValidHostLabelSingle, which
isValidSingleLabel already applies.

Moved RulesFunctionsSubstringEqualsTest to codegen-generated-classes-test
(F8). RulesFunctions is a codegen template shared by every service, not a
class S3 owns, so per docs/guidelines/testing-guidelines.md it belongs with
"generated SDK common functionalities". It now sits beside
EndpointUrlConformanceTest and RuleUrlTest, which cover other generated
classes from the same template set, and gained coverage of the F4
delegation. Test names follow the methodToTest_when_expectedBehavior
convention and the placement rationale is recorded in the class javadoc.

Added the missing regression guards (F2). Three of the four rewrites had no
assertions: only the substring peephole and the stringEquals null-safety
decision were covered. The simple-BDD golden fixture contains none of these
shapes and the S3 BDD was asserted only as non-null, so a change to the
emitters or to PrepareForCodegenVisitor could silently restore the
RulesFunctions dispatches with a green build. New
ConditionFnCodeGeneratorVisitorTest covers the null-check elision decision
and the single-evaluation coalesce form directly; BddEndpointProviderSpecTest
now asserts the ite, coalesce and isValidHostLabel forms in the generated S3
BDD and that no dispatch remains. Confirmed the F1 tests are genuine guards:
reverting isAlwaysNonNull to the name-only check fails two of them.

Not changed, with reasons: the synthetic-name constants stay split across
BddPeepholeVisitor and RuleRuntimeTypeMirror (F7), because moving them onto
the visitor would make the rules2 package depend on rules2.bdd, which
already depends on it, and because Checkstyle's DeclarationOrder forces
SUBSTRING_EQUALS_FN to its current position. The fixture commit stays on
this branch (F9) since relocating it means rewriting pushed history; it is
called out in the PR description instead.

Verified: 705 codegen tests; the 32 generated bddendpoints conformance tests,
which exercise the BDD provider behaviourally and had not previously been
run; 1671 substringEquals differential cases plus 65 sibling generated-class
tests; 393 S3 and 574 DynamoDB endpoint conformance tests. rules2 golden
fixtures remain byte-identical to feature/master/endpoints-bdd. Checkstyle
and SpotBugs clean.

* test(codegen): Add golden file for the S3 BDD provider

The S3 BDD is the only test model that exercises all four peephole
rewrites, the dynamic auth scheme name and the ite assign conditions, and
it was covered only by assertThat(poetSpec()).isNotNull() plus targeted
string assertions. Golden files are the established pattern for rules2
providers and make the effect of a codegen change reviewable as a diff, so
the S3 BDD now has one.

Replaces the isNotNull smoke test with generatesTo. Verified that codegen
output is deterministic across separate JVM runs before adopting it, since a
fixture is otherwise flaky, and that reverting an emitter makes the fixture
test fail.

Keeps the correctness invariants as explicit negative assertions rather than
folding them into the fixture, consolidated into
s3Bdd_neverEmitsSpecViolatingForms. Regenerating a golden file is a
one-command operation and doing it without reading the diff is how a defect
gets blessed into a fixture - this repo already has an instance of that. The
assertions name what must never be emitted and why, so restoring a
faster-but-spec-violating form means deleting a stated invariant rather than
accepting a regenerated file. Confirmed both layers fail independently when
an emitter regresses.

Dropped the positive assertions the fixture now subsumes.
literalBranchIteAssigns_keepElidedNullCheck stays, because it and
ConditionFnCodeGeneratorVisitorTest only make sense as a pair.

Fixture is 4102 lines, in the same raw JavaPoet form as the sibling
endpoint-provider-bdd-class.java. PoetMatchers formats both sides and
compares ignoring whitespace, so the fixture's own indentation is not
significant.

Verified: 703 codegen tests, Checkstyle and SpotBugs clean. Test-only
change; no generated production code is affected.
…#7317)

Endpoint's builder eagerly allocated two HashMaps (headers and
attributes) for every endpoint, and handed them to the built Endpoint.
Both escape into the result, so they cannot be optimized away by the
JIT, and most endpoints need neither: headers are almost always absent
and attributes are almost always a single AUTH_SCHEMES entry.

Stage instead of allocate:

- The header map is allocated only once a header is actually added.
- The first attribute is held in two fields on the builder; a map is
  allocated only if a second distinct key arrives.
- build() collapses whatever was staged into the smallest immutable
  map that fits: emptyMap, singletonMap, or an unmodifiable view.
- Both maps are created with capacity 4 rather than the default 16,
  whose backing table was the largest single allocation on the path.

No API change. The builder itself does not escape resolveEndpoint, so
escape analysis already eliminates it; this removes the allocations
that actually survive.

Measured with JMH (allocation profiler, 2 forks, JDK 21, M-series),
against an Endpoint-shaped holder built only from emptyMap/singletonMap
as the theoretical floor:

  shape               before    after     floor
  no attributes       120 B/op   24 B/op   24 B/op
  one attribute       232 B/op   64 B/op   64 B/op
  two attributes      264 B/op  232 B/op
  one attr + header   424 B/op  288 B/op

The two shapes that dominate real endpoint rules now allocate exactly
what a hand-written factory would. Time per construction drops 67% and
71% for those two shapes. Two-attribute construction costs ~3ns more
because it stages an attribute and then promotes anyway; that path is
S3 Express only and 3ns against a ~50us endpoint resolution.

Behavior notes:

- headers() and the attribute map are now unmodifiable. Mutating a map
  returned from a getter was never supported, and SdkHttpRequest
  already behaves this way.
- Mutating a builder after build() no longer leaks into the Endpoint
  that was already built, for the no-attribute, single-attribute and
  header cases. Previously it leaked in every case. The two-or-more
  attribute case still shares state, unchanged from before.
…7322)

* feat(endpoints): Add a result cache to the BDD endpoint provider

Generate a single-entry (params -> endpoint) cache into each
Default{Service}EndpointProvider produced by the BDD codegen path, so a
client resolving the same endpoint repeatedly skips the BDD walk after
the first call. Scoped to the BDD path only; the rules2 path that every
shipped service uses is untouched, because no service model ships an
endpoint-bdd-1.json yet and that keeps a caching defect away from
customers while the approach is evaluated.

The cache is a volatile field holding an immutable CacheEntry. Racing
threads compute equivalent entries for equal params, so a lost write
costs one re-resolution and needs no further synchronisation. Only
successful resolutions are stored: a rule error or a no-match leaves the
previous entry in place, so a bad call neither poisons the cache nor gets
replayed from it.

Cache-key comparison is generated per parameter from a codegen-time
classification (EndpointCacheKeyClassification, computed by
EndpointProviderCacheIndex), ordered cheapest check first with an early
exit on mismatch:

  BOOLEAN           - identity, which is complete for Boolean rather than
                      merely fast, since autoboxing returns the TRUE and
                      FALSE singletons
  CLIENT_STATIC_REF - identity (AWS::Region, clientContextParams)
  OPERATION_STATIC  - identity (staticContextParams literals)
  SEMI_STABLE       - identity then equals (SDK::Endpoint,
                      AccountIdEndpointMode)
  IDENTITY_DERIVED  - identity then equals (AWS::Auth::AccountId)
  REQUEST_DYNAMIC   - identity then equals (contextParam, JMESPath)
  REQUEST_LIST      - size-capped element-wise identity/equals

Classifications are read from the BDD model's parameters, not the rule
set's. The generated provider evaluates the BDD, nothing in codegen
enforces that the two files agree, and a parameter absent from the key is
the one defect here that returns an endpoint resolved for different
inputs. Codegen fails outright rather than skipping a parameter it
cannot place.

Reference stability, which raises the hit rate but is not required for
correctness since every string tier keeps an equals fallback:

- StaticClientEndpointProvider sanitizes the client endpoint once at
  construction instead of rebuilding the URI per request, exposed through
  a new ClientEndpointProvider#sanitizedEndpointString() that
  AwsEndpointProviderUtils#endpointBuiltIn now delegates to. The
  transform has a single definition so the cached and recomputed forms
  cannot drift.
- AccountIdEndpointMode#endpointModeValue() returns an interned literal
  from a field rather than name().toLowerCase(), and
  EndpointParamsKnowledgeIndex emits it.
- EndpointResolverUtilsSpec hoists staticContextParams array values to
  static final unmodifiable lists.

The last two also remove a per-request allocation on the rules2 path.

Testing:

- BddEndpointProviderCacheTest, 30 tests over the bddendpoints service:
  one no-stale-hit test per parameter, hit assertions via instance
  identity, unset transitions, equals fallbacks, the list size cap,
  errors never cached, and 16-thread concurrent resolution of two
  parameter sets. Mutation-checked: emptying the key fails 21 of 30, and
  dropping only clientStringParam fails exactly the test that names it.
- EndpointProviderCacheIndexTest pins each parameter's tier, the
  comparison order, and that classification reads the BDD rather than the
  rule set. Tier assignment is invisible at runtime, so it is asserted
  here or not at all.
- bddendpoints and the default-regional codegen models declare
  parameters their BDD graphs never read, which is how all seven tiers
  get covered without a BDD compiler: the node graph indexes conditions
  rather than naming parameters, and the cache key spans every declared
  parameter.
- queryServiceModelsWithBddEndpoints now pairs the S3 BDD with the S3
  rule set instead of the four-parameter default-regional one, so the
  17 parameters in the golden file match the params class.

* refactor(endpoints): Simplify the BDD cache key to one comparison form

Replace the seven-tier cacheParamsMatch with a uniform Objects.equals
chain, ordered into three coarse groups: booleans, then strings whose
reference the SDK keeps stable, then everything else, each group in the
model's declaration order.

Benchmarking says the tiers were not earning their complexity. Against
this form they bought nothing on the hit path, which is the only path a
cache exists to improve, and about 0.2 ns on the miss shape that
motivates ordering at all - ahead of a ~1400 ns resolution. They cost a
classification pass over every operation, a seven-value enum, and three
different emitted code shapes. Full data, including why hashing the
params would be worse than comparing them and why the comparison stays a
private static method in the provider rather than moving onto the params
class, is in .kiro/reference/endpoint_cache_key_benchmark.md.

Objects.equals is what makes one emitter sufficient: it tries identity
before equals, so a parameter whose reference is stable settles on the
identity check and one that arrives fresh falls through and still
matches. That was the tiers' main trick, available for free.

Ordering survives because it is nearly free to derive - a parameter's
group follows from its declared type plus whether it is AWS::Region or a
client context param, with no analysis of the service's operations - and
it is worth 20 ns on a miss against a late-declared boolean. It cannot
affect correctness, since the chain compares every parameter before
returning true.

List parameters route through a generated cacheListsMatch helper instead
of Objects.equals, keeping every term in the chain a single boolean
expression and keeping the comparison bounded. List.equals is unbounded,
and resolution is typically indifferent to list length, so an unbounded
key check can cost more than the resolution it avoids and turn the cache
into a pessimisation for that request shape. Above the cap the provider
reports a miss and resolves, which is what it would have done anyway.
The helper is only emitted when the model declares a stringArray.

Deletes EndpointCacheKeyClassification, EndpointProviderCacheIndex and
EndpointProviderCacheIndexTest.

Testing:

- The classification unit test is replaced by two assertions on the
  generated source, which is a stronger place to make them: that the key
  compares every parameter the BDD declares, and that the three-group
  ordering holds. The first is the invariant that matters - a parameter
  missing from the key returns an endpoint resolved for a different value
  of it - and it now covers all three BDD test models.
- Mutation-checked both levels. Dropping a parameter that exists only in
  the runtime model fails exactly one of the 30 BddEndpointProviderCacheTest
  cases, the one that names it; dropping parameters present in the codegen
  models fails the completeness assertion, the ordering assertion and both
  golden files.
- codegen 707 pass, codegen-generated-classes-test 3677 pass, checkstyle
  clean.

* perf(endpoints): Key the BDD cache on what the BDD actually reads

Two changes to the generated cache key, both driven by the per-service
measurements in .kiro/reference/endpoint_cache_service_shapes.md.

1. Exclude parameters no condition and no result reads.

A parameter nothing reads cannot change the resolved endpoint, so
comparing it can only turn a hit into a miss that resolves to the
endpoint already cached.

S3 is why this matters. It declares Key, Prefix and CopySource, reads
none of them, and binds Key as a contextParam - so Key changes on
essentially every object request. With Key in the key, S3's cache misses
on almost every GetObject and pays 6.4 ns per request for nothing.
Dropping the three unread parameters takes the key from 17 comparisons
to 14, makes a hit 41% cheaper on fresh references, and converts the
dominant miss into a hit.

2. Compare only element 0 of a stringArray read only at index 0.

When every read of a list is getAttr(list, "[0]"), nothing past the
first element reaches the endpoint, so the rest cannot change the answer.

DynamoDB is why this matters. It reads ResourceArnList only through
getAttr(ResourceArnList, "[0]"), and comparing a freshly built
three-element ARN list measured 15.5 ns against a 28 ns regional
resolution - over half the cost the cache exists to avoid, on a latency
path that matters. Comparing element 0 makes it O(1).

This also makes an absent list and an empty one the same key, which is
correct rather than a concession: the runtime's listAccess returns null
for both, so both take the same branch during resolution. It is strictly
more permissive than comparing whole lists, so it can only turn misses
into hits.

Detection is conservative in the safe direction. BddParameterReferences
walks the conditions and results with the same parser the generator uses,
and any read that is not an index-0 access - isSet, a template
interpolation, a non-zero index, passing the list to a function - marks
the parameter as needing a full comparison. Erring that way costs
comparison work; erring the other way would drop something from the key
that can change the endpoint.

Testing:

- BddParameterReferences is not tested directly. Both behaviours are
  asserted on the generated source and on runtime behaviour, because
  those are what can be wrong in a way that matters; a unit test over the
  usage map would restate the implementation.
- The codegen tests assert the key covers every referenced parameter for
  all three BDD models, omits the unreferenced ones, routes a whole-list
  parameter and an index-0-only parameter to their respective helpers,
  and that the provider really does read only element 0 - so the
  comparison and the thing it depends on cannot drift apart.
- The runtime suite grows to 38 tests: changing element 0 invalidates
  while changing a later element, shortening the list, or going far past
  the size cap all hit; an unread parameter never invalidates; and the
  whole-list parameter keeps the previous element-wise coverage.
- Mutation-checked three ways. Misclassifying whole-list reads as
  index-0-only fails the 4 whole-list tests; treating every parameter as
  unreferenced fails 22; never detecting index-0-only fails the 4
  first-element tests. The suite discriminates in both directions.
- Both test models needed their extra parameters wired into the node
  graph. They had been declared but unreferenced, so they would now be
  correctly excluded and the tests covering them would assert nothing.
  Each new condition is a node whose branches share a successor, which
  makes it genuinely evaluated without changing what any request
  resolves to.

codegen 709 pass, codegen-generated-classes-test 3685 pass, checkstyle
clean.

* fix(endpoints): Let an isSet guard keep the first-element list comparison

The rules language requires a null check before an indexed access, so a
model that reads list[0] always reads isSet(list) as well. Counting that
null check as a whole-value read meant the first-element comparison never
fired on a real model: DynamoDB's ResourceArnList, the case it was added
for, was still compared element by element.

isSet observes only whether the parameter is present, so on its own it no
longer disqualifies the parameter. Verified against the real DynamoDB BDD:
the analysis now classifies ResourceArnList as FIRST_ELEMENT_ONLY and the
other eight parameters as FULL.

Presence does have to stay in the cache key, though, and that is a change
from the previous commit. Because isSet tells an absent list apart from an
empty one, the generated comparison now checks presence as well as element
0, rather than treating both as null:

    if (a == b) return true;
    if (a == null || b == null) return false;
    String firstA = a.isEmpty() ? null : a.get(0);
    String firstB = b.isEmpty() ? null : b.get(0);
    return Objects.equals(firstA, firstB);

Collapsing absent and empty is sound only when the BDD's branches for the
two converge. DynamoDB's do - traced through its graph, cond20 false and
cond21 false both land on nodeP27, and likewise nodeP53 for the second
occurrence - so it would have been correct there. It is a property of the
graph rather than of the parameter, so relying on it would mean a future
model could quietly invalidate the comparison. One extra reference check
buys independence from that, and the comparison stays O(1).

Test model changes:

- Both BDD fixtures gained the isSet guard ahead of their index-0 access,
  so they match the shape a real model produces. Without it the fixtures
  were testing a case that cannot occur.
- The whole-list parameters were previously read only via isSet, which now
  correctly qualifies them for the first-element comparison and left the
  whole-list path uncovered. Each now also reads index 1, the smallest
  realistic change that makes the rest of the list matter.
- firstElementList_emptyAndUnset_areTheSameKey becomes
  ..._areDistinguished, matching the new semantics.
- New codegen assertion that a list read past the head is compared in
  full, since the boundary between the two helpers is a correctness line
  rather than a preference.
- Mutation-checked the new logic: treating isSet as a whole-value read -
  exactly the reported regression - fails three runtime tests, the
  codegen assertion, and the golden file.

codegen 710 pass, codegen-generated-classes-test 3685 pass, checkstyle
clean.

* refactor(endpoints): Drop the public sanitizeEndpoint helper

It was public only because of a package boundary: the transformation lived
on StaticClientEndpointProvider in core.internal, and the caller -
ClientEndpointProvider's default method - sits in core, so nothing weaker
than public could reach it. That is a poor reason for a public member,
even on an @SdkInternalApi class.

The transformation now lives in the interface default, which is the
implementation every provider that does not override it already uses, and
StaticClientEndpointProvider's constructor calls
ClientEndpointProvider.super.sanitizedEndpointString() to compute the value
it caches. One definition, as before, with nothing public added.

The class is now final. That is what makes the constructor call provably
safe: the super call is non-virtual, but the default it invokes reads
clientEndpoint() and isEndpointOverridden(), and a subclass overriding
either could have observed partial construction. Nothing subclasses it and
it is @SdkInternalApi, so sealing it costs nothing and removes the hazard
rather than documenting it.

Verified the two implementations still agree: for a matrix of endpoints
covering query parameters, user info, explicit ports, fragments and plain
hosts, the caching implementation and the interface default produce the
same string, the not-overridden case still yields null, and the caching one
still returns an identical reference across calls.

sdk-core 1505 + 624 pass, aws-core 317, codegen 710,
codegen-generated-classes-test 3685, checkstyle clean.

* Cleanups

* Additional cleanups/fixes from review pass

* Add tests (missed adding in earlier commit)

* Fix minor edge issues with split and loadPartitionData

* Minor - rename method in AccoundIdEndpointMode to just value
@alextwoods
alextwoods requested a review from a team as a code owner September 1, 2026 18:53
…oints-bdd

Master merged #7294 "Use code-generated endpoint rules", which renamed the
codegen poet.rules2 package into poet.rules and deleted the legacy interpreted
endpoint rules path. Resolve against the BDD work as follows:

* EndpointProviderTasks: master collapsed the compiled/interpreted branch into
  a single unconditional path. Keep master's shape and re-add only the BDD
  branch, selecting the BDD provider when an endpoint BDD model is present.
  The two runtime-copy tasks the BDD path used, RulesEngineRuntimeLiteGenerator
  Task and RulesEngineRuntimeGeneratorTask2, were deleted upstream; they
  collapse into master's single RulesEngineRuntimeGeneratorTask, whose resource
  set is a superset of what the BDD provider needs.

* Move poet/rules2/bdd to poet/rules/bdd (main, test and test resources) and
  rewrite poet.rules2 references to poet.rules. Git's rename detection carried
  the edits to the 28 renamed rules2 classes automatically, but the new BDD
  files were adds rather than renames and were left behind importing a package
  that no longer exists.

* Re-sort imports in the moved BDD files. Once rules2.* becomes rules.*, the
  previously sorted import block no longer satisfies the CustomImportOrder
  checkstyle rule relative to the pre-existing poet.rules imports.

Note that the RuleRuntimeTypeMirror listAccess return type fix and the
RulesFunctions.java.resource additions now apply to the only remaining codegen
path rather than just the BDD path, since master removed the interpreted one.

Verified with mvn install -pl :codegen (checkstyle plus tests): all endpoint
rules and BDD codegen tests pass, including both BDD golden files.
…ints-bdd-s3-ddb-staging

Brings in the origin/master merge resolved on the feature branch, which moves
the BDD codegen from poet.rules2 to poet.rules following upstream #7294.
Merged cleanly; this branch's only delta is the S3 and DynamoDB BDD models.

Note that S3 codegen does not yet compile on this branch, for a reason that
predates this merge: BddResultCodeGeneratorVisitor emits a reference to
software.amazon.awssdk.services.s3.endpoints.authscheme.DynamicEndpointAuth
SchemeFactory, and that class was never carried into the BDD PR chain. It
exists only on benchmark_only/endpoints-bdd-complete. The feature branch ships
no service BDD models, so nothing exercised the path until this branch added
the S3 model.
BddResultCodeGeneratorVisitor emits a reference to
software.amazon.awssdk.services.s3.endpoints.authscheme.DynamicEndpointAuth
SchemeFactory whenever an endpoint result carries an auth scheme whose name is
resolved at runtime, but the class itself was never carried into the BDD PR
chain. Port it in from cd0c5a3.

Endpoint rulesets normally declare the auth scheme name as a string literal, so
codegen can emit a direct call to the matching concrete builder. S3 is the
exception: its BDD ruleset merges two otherwise identical results whose auth
scheme names differ, lifting the difference into a runtime conditional, so the
concrete type cannot be selected at codegen time. This factory collects the
shared properties up front and defers type selection to create(name).

The gap was invisible on this branch because it ships no service BDD models, so
nothing exercised the path. It surfaces as an S3 compile failure as soon as a
service BDD model is added.

Ported faithfully; the only additions are the @NotThreadSafe annotation and
documentation of the null-property and null-name behaviour.

Testing: adds DynamicEndpointAuthSchemeFactoryTest covering both scheme names,
property pass-through, fluent and order-independent setters, unset and
explicitly null properties, reuse across calls, and fail-fast on unsupported and
null names. Verified the tests are non-vacuous by mutating the class twice
(swapping the two scheme names, and returning a default instead of throwing);
each mutation failed the suite. Full mvn install -pl :s3 passes, 1804 tests,
including checkstyle and spotbugs.
…ints-bdd-s3-ddb-staging

Brings in DynamicEndpointAuthSchemeFactory, which the BDD endpoint codegen
emits for S3 whenever an endpoint result carries an auth scheme whose name is
resolved at runtime. This unblocks S3 codegen on this branch: the class was
missing, so the generated DefaultS3EndpointProvider referenced a type that did
not exist and S3 failed to compile.

Verified on this branch, where the S3 and DynamoDB BDD models actually put both
services on the BDD path (553 and 82 generated BDD node methods respectively,
with 5 uses of the factory in the S3 provider):

* mvn clean install -pl :s3,:dynamodb - BUILD SUCCESS, 1804 S3 and 61 DynamoDB
  unit tests, including checkstyle and spotbugs.
* mvn verify -pl :s3,:dynamodb -P endpoint-tests - BUILD SUCCESS. S3 823 tests
  (393 provider, 430 client) and DynamoDB 1108 tests (548 provider, 534 client,
  26 streams provider), no failures. All 393 S3 ruleset test cases run against
  the BDD provider with no skips. The 26 skipped S3 client tests are the
  pre-existing skipEndpointTests entries in the S3 customization.config, a file
  this branch does not modify.
@alextwoods alextwoods added changelog-not-required Indicate changelog entry is not required for a specific PR no-api-surface-area-change Indicate there is no API surface area change and thus API surface area review is not required labels Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog-not-required Indicate changelog entry is not required for a specific PR no-api-surface-area-change Indicate there is no API surface area change and thus API surface area review is not required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant