fix: treat Redis deserialize failures as cache misses and enable the MethodRouting cache#2859
Merged
simonredfern merged 3 commits intoJul 13, 2026
Merged
Conversation
Redis.deserialize returned the sentinel "NONE".asInstanceOf[T] when Kryo failed to decode the cached bytes (corrupt entry, class-shape change across a redeploy, Kryo registration drift). scalacache then treated that sentinel as a cache HIT and handed a String to callers expecting the real type, producing ClassCastException on every request until the TTL expired. Rethrow the decode failure instead: scalacache.TypedApi._caching treats a throwing cache read as a miss, recomputes the value from the source block, and repopulates the key with a fresh, valid serialization - the cache self-heals on the next call instead of staying poisoned.
The getMethodRoutings cache was shipped disabled (TTL default 0) because nothing invalidated it: createOrUpdateMethodRouting/deleteMethodRouting only touched the DB provider, so an enabled cache would serve stale routings for a full TTL after every /management/method_routings write. With the cache off, StarConnector re-queries the method_routing table on every intercepted connector call - up to twice per call - so a single API request issues dozens of identical getMethodRoutings queries. - Invalidate on write: createOrUpdateMethodRouting and deleteMethodRouting now pattern-delete '*getMethodRoutings*'. The scalacache memoize key embeds the literal method name, so one pattern delete clears all argument variants. - Enable the cache: methodRouting.cache.ttl.seconds now defaults to 30, guarded to 0 in test/dev mode (same pattern as dynamicEntityTTL) so tests that mutate routings and assert immediately are unaffected. - Document the prop in sample.props.template. The cache-invalidation and corrupt-entry self-healing behaviour is covered by MethodRoutingCacheInvalidationTest (cancelled when no Redis is reachable). This change depends on the previous commit: an enabled cache is only safe when a corrupt Redis entry heals as a miss instead of poisoning reads for the whole TTL.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
Two commits, ordered on purpose:
1.
fix(cache)— Redis deserialization failure poisoned the cacheRedis.deserializereturned the sentinel"NONE".asInstanceOf[T]when Kryo failed to decode cached bytes (corrupt entry, class-shape change across a redeploy, Kryo registration drift). scalacache treated that sentinel as a valid HIT, so every caller expecting the real type got aClassCastExceptionfor the whole TTL. The fix rethrows the decode failure:scalacache.TypedApi._cachingtreats a throwing cache read as a miss, recomputes from the source block, and repopulates the key — the entry self-heals on the next call.2.
fix(routing)— invalidate and enable the MethodRouting lookup cacheThe
getMethodRoutingscache shipped disabled (TTL default 0) because nothing invalidated it. With it off, StarConnector re-queries themethod_routingtable on every intercepted connector call (up to twice per call), so a single API request issues dozens of identical queries.createOrUpdateMethodRouting/deleteMethodRoutingnow pattern-delete*getMethodRoutings*— the memoize key embeds the literal method name, so one pattern delete clears all argument variants. Writes via/management/method_routingstake effect immediately; the TTL is only a staleness backstop.methodRouting.cache.ttl.secondsnow defaults to 30, guarded to 0 in test/dev mode (same pattern asdynamicEntityTTL), so existing tests that mutate routings and assert immediately are unaffected.sample.props.template.Tests
RedisDeserializeMissTest: undecodable bytes make the codec throw (never the legacy"NONE"sentinel), and a codec round-trip still works.MethodRoutingCacheInvalidationTest(cancelled viaassumewhen no Redis is reachable): pattern invalidation forces a recompute on the next read; a corrupted entry behaves as a miss — exactly one recompute, key repopulated with valid bytes.MethodRoutingTest(v3.1.0) andCacheEndpointsTest(v6.0.0) pass: 8 suites, 26 tests, 0 failed, 0 canceled (local Redis running).Known follow-ups (out of scope here)
getConnectorNameAndMethodRoutinglookup per intercepted call (existing TODO inbankconnectors/package.scala) — the cache makes it cheap, dedup still worthwhile.getEndpointMappingshas the same disabled-TTL/no-invalidation pattern.deleteKeysByPattern/scanKeysuse RedisKEYS; should move toSCANfor large keyspaces.