Skip to content

[release] feat(lib-cache-tiered-redis): add optional L1 TTL (1.2.0) - #168

Merged
pditommaso merged 2 commits into
masterfrom
feat/tiered-cache-l1-ttl
Oct 23, 2026
Merged

pditommaso merged 2 commits into
masterfrom
feat/tiered-cache-l1-ttl

Conversation

@pditommaso

Copy link
Copy Markdown
Contributor

What changes

Adds an optional L1 TTL to AbstractTieredCache, so a value rewritten in Redis by one instance reaches every other instance within a bounded time.

Today an L1 (Caffeine) entry is kept for as long as the value's own TTL. An instance that holds a key in L1 does not see a newer value another instance writes to L2 until that TTL passes. That's fine for values written once, but it breaks caches whose values are refreshed during their lifetime. For example, sched's CloudInfo snapshot store (seqeralabs/sched#1742) keeps each region's catalogue under a 7-day TTL as an outage safety net, and a cron on one pod rewrites it every hour. Without an L1 bound, every pod except the writer would keep serving its first copy for up to 7 days.

How

  • New protected Duration getL1Ttl() hook, returning null by default, so existing caches behave exactly as before.
  • When it is set, the L1 Caffeine cache uses expireAfterWrite(l1Ttl), and the next read after expiry rehydrates from L2. An entry's L1 lifetime is the shorter of the L1 TTL and the value's own TTL; L2 expiry is unchanged.
  • The L1 TTL is included in the cache's startup config log line.
  • README gains an "L1 TTL" section. The module goes to 1.2.0, with a changelog entry, so it is published on merge ([release]).

Testing

./gradlew :lib-cache-tiered-redis:test passes (Redis Testcontainer). Two new cases in AbstractTieredCacheTest:

  • with an L1 TTL, a reader keeps its L1 copy until the L1 TTL passes, then serves the value another instance rewrote;
  • without one, the reader keeps serving its L1 copy (the current default, pinned).

🤖 Generated with Claude Code

An L1 entry lived as long as the value's own TTL, so an instance holding
a key in L1 never saw a newer value another instance wrote to L2 until
that TTL passed. Caches whose values are refreshed periodically under a
long TTL (e.g. a cron rewriting a 7-day entry hourly) were left serving
the first copy on every instance but the writer.

AbstractTieredCache gains a protected getL1Ttl() hook, null by default
so existing caches are unchanged. When set, the L1 Caffeine cache
expires entries after that duration and the next read rehydrates from
L2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com>
@pditommaso
pditommaso requested a review from jonmarti October 23, 2026 12:57

@jonmarti jonmarti left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pditommaso, approving:

  • with no L1 TTL the cache is built exactly as before;
  • the L1 TTL only bounds how long a copy is served locally (the value's own expiresAt check still applies);
  • the removal listener, which now also fires on L1 expiry, only logs.

The first new test can't pass without the hook, since the reader would keep serving its first copy, as the second test pins. Two non-blocking notes inline.

…tion.ZERO

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pditommaso

Copy link
Copy Markdown
Contributor Author

Addressed both review notes in 69de38d:

  • L1 TTL test timing widened (1s TTL, 1.5s sleep) to avoid CI flakiness.
  • getL1Ttl() Javadoc now documents Duration.ZERO (L1 disabled, every read goes to L2).

./gradlew :lib-cache-tiered-redis:test passes.

@pditommaso
pditommaso merged commit cd1049b into master Oct 23, 2026
7 checks passed
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.

2 participants