Skip to content

fix: drop CallContext from the endpoint-mapping and FX rate cache keys - #92

Open
hongwei1 wants to merge 1 commit into
develop-obpfrom
feature/cache-key-drop-callcontext
Open

fix: drop CallContext from the endpoint-mapping and FX rate cache keys#92
hongwei1 wants to merge 1 commit into
develop-obpfrom
feature/cache-key-drop-callcontext

Conversation

@hongwei1

@hongwei1 hongwei1 commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Problem

Two memoize keys rendered the whole CallContext:

  • NewStyle.function.getEndpointMappings
  • LocalMappedConnectorInternal.getCurrentFxRateCached

CacheKeyFromArguments renders every parameter that is not annotated @CacheKeyOmit, and CallContext carries per-request state (startTime, correlationId, url, verb, ipAddress, user). Both keys were therefore unique per request: the cache could never hit.

Measured A/B with both TTLs forced on — two calls differing only in CallContext:

site before after
getCurrentFxRateCached 2 Redis keys (one per request, each living out its TTL) 1
getEndpointMappings 0 Redis keys 1

getEndpointMappings wrote nothing because it cached the (mappings, callContext) tuple, and chill/Kryo cannot encode the lambda reachable through CallContext.resourceDocument (KryoException: Could not serialize lambda). cachePut swallows that as result served uncached, so endpointMapping.cache.ttl.seconds bought nothing but a WARN per call. A hit would also have handed the caller the originating request's CallContext.

This is pre-existing, not a regression. Every other cache site in the codebase already keys on business arguments only, and ConnectorBuilderUtil stamps @CacheKeyOmit onto callContext for the methods it generates — these two hand-written sites simply never did.

Changes

  • @CacheKeyOmit callContext on getCurrentFxRateCached.
  • getEndpointMappings: split the memoized half into getEndpointMappingsCached(bankId), which caches the mappings alone and pairs them with the caller's own callContext on return.
  • invalidateEndpointMappingCache() on create/update/delete, mirroring invalidateMethodRoutingCache. While callContext was in the key nothing could hit, so a stale entry was unreachable by construction; now that the cache works, writes have to publish themselves.

Why a helper method rather than @CacheKeyOmit on the caller

CacheKeyFromArguments reads the parameters of the method whose body ends in buildCacheKey. Binding the result to a val first —

val x = CacheKeyFromArguments.buildCacheKey { ... }
(x, callContext)

— leaves the macro with no parameters and it emits Nil.mkString("_"): an empty argument segment, i.e. every bankId sharing one cache entry. That compiles, and no test catches it, because both TTLs default to 0 and Caching.memoizeSyncWithProvider short-circuits on Duration.Zero without touching Redis.

Verification

javap on the compiled classes, on both Scala versions this commit has to work on — 2.12.21 (where it was developed) and 2.13.18 (this PR's base):

method rendered key arguments
getEndpointMappingsCached $colon$colon(aload_1, Nil) = bankId :: Nil
getCurrentFxRateCached $colon$colon(aload_1, aload_2, aload_3, Nil) = bankId :: from :: to :: Nil; the callContext slot is absent

Byte-for-byte the same shape on both compilers, so the macro's @CacheKeyOmit handling and its tail-expression rule behave identically across the 2.13 migration.

Testing

run_tests_parallel.sh: 3450 tests, 0 failures, 0 errors, 0 skipped — all 4 shards green.

Additionally, with endpointMapping.cache.ttl.seconds and code.fx.exchangeRate.cache.ttl.seconds forced to 60, EndpointMappingTest, EndpointMappingBankLevelTest and ExchangeRateTest pass — the caching path itself is exercised, not just short-circuited.

CacheKeyFromArguments renders every parameter that is not annotated
@CacheKeyOmit. CallContext carries per-request state (startTime,
correlationId, url, verb, ipAddress, user), so both keys were unique per
request: the cache could never hit, and getCurrentFxRateCached wrote a fresh
Redis entry per call that lived out its TTL.

getEndpointMappings additionally cached the (mappings, callContext) tuple.
chill/Kryo cannot encode the lambda reachable through
CallContext.resourceDocument, so every write failed and cachePut swallowed it
as "result served uncached" - endpointMapping.cache.ttl.seconds bought nothing
but a WARN per call. A hit would also have handed the caller the originating
request's CallContext.

Split the memoized half into getEndpointMappingsCached(bankId) rather than
annotating callContext on the caller: CacheKeyFromArguments reads the
parameters of the method whose body ends in buildCacheKey, so binding the
result to a val first leaves it with no parameters and it emits
Nil.mkString("_") - an empty argument segment, i.e. every bankId sharing one
entry. Verified with javap that the key now renders bankId :: Nil, and that
getCurrentFxRateCached renders bankId :: from :: to :: Nil.

Add invalidateEndpointMappingCache() on create/update/delete, mirroring
invalidateMethodRoutingCache: while callContext was in the key nothing could
hit, so a stale entry was unreachable by construction; now that the cache
works, writes have to publish themselves.
@hongwei1 hongwei1 closed this Aug 17, 2026
@hongwei1 hongwei1 reopened this Aug 17, 2026
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant