Add velocity-backend-redis: the sliding hot-path reference backend - #10
Merged
Conversation
The second real velocity-spi backend (v1 Redis/Lettuce sliding reference, P10/§8) — true sliding windows via sorted sets + atomic Lua, validated against real Redis with Testcontainers. Reuses the shared conformance TCK (now window-parameterized) with SLIDING windows + SlidingScenarios. - RedisVelocityBackend implements CountStore/SumStore/DistinctStore + SlidingSupport. Exact, ATOMIC read-your-write: each apply is a single atomic Lua script (INCR a per-key sequence for a unique member, ZADD scored by now, evict aged members, return the post-write windowed value), so concurrent applies never lose an increment (NFR-6, acceptance #2 — the 16-thread shared concurrency scenario passes on real Redis). - Backend owns the clock (ADR 0003/FR-3): a sliding window of duration D covers (now-D, now] with the LEADING EDGE EXCLUSIVE, now from the injected Clock (not Redis TIME) — matches the in-memory reference, so SlidingScenarios (aging / exclusive edge) pass. Windowed value = members with score in (now-D, now]; eviction is cost-cleanup. - Storage: one ZSET per (namespace, subject, aggregation[type+dimension], window) — no feature name in the key (QueryTuple has none), like the in-memory + JDBI backends. COUNT/SUM append a unique member; DISTINCT uses the opaque Base64 member token itself (de-dupes). SUM cents summed client-side as BigDecimal (9e18-scale preserved, negatives honored, scale 0). Unsupported window -> Failure(UNSUPPORTED_WINDOW) on apply and query, never a silent 0. - SeedSupport is intentionally NOT implemented in v1 (seedSupported=false): ADR 0008's per-bucket seed is a tumbling concept and the mix-in is optional (sliding-seed is a tracked follow-up). Capabilities declare SLIDING-only, EXACT, distinctHllSliding=false. - No Dagger/Dropwizard/Jackson (depends only on velocity-spi + Lettuce + slf4j). 27 tests against real Redis (Testcontainers redis:7-alpine), 0 skipped; coverage 97.5% line / 92.3% branch (gate 80/70). Hand-implemented (implementer subagents stalled twice on this task). Validated by the change-validator agent: VERDICT APPROVE (sliding semantics correct, atomicity proven, shared TCK genuinely running). Follow-up (minor, non-blocking): the COUNT/DISTINCT apply return uses ZCARD-after-evict; switching to a windowRange zcount inside the Lua would tighten the running value under cross-thread clock skew (the final authoritative query is already exact). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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
velocity-backend-redis— the second realvelocity-spibackend (the v1 Redis/Lettuce slidinghot-path reference, P10/§8), proven against real Redis via Testcontainers. It reuses the shared
conformance TCK (now window-parameterized, PR #9) with sliding windows plus
SlidingScenarios—demonstrating the harness works across both window shapes.
What's in it
RedisVelocityBackendimplementsCountStore/SumStore/DistinctStore+SlidingSupport.Exact,
ATOMICread-your-write: eachapplyis a single atomic Lua script (INCR a per-keysequence for a unique member → ZADD scored by
now→ evict aged members → return the post-writewindowed value), so concurrent applies never lose an increment (NFR-6, acceptance Bump io.lettuce:lettuce-core from 6.5.5.RELEASE to 7.6.0.RELEASE in the gradle-dependencies group #2 — the shared
16-thread concurrency scenario passes on real Redis).
Dcovers(now-D, now]with the leading edge exclusive,
nowfrom the injectedClock(not RedisTIME) — matchingthe in-memory reference, so
SlidingScenarios(aging / exclusive edge) pass.(namespace, subject, aggregation, window)— no feature name in the key(a
QueryTuplehas none), like the in-memory + JDBI backends. COUNT/SUM append a unique member;DISTINCT uses the opaque Base64 member token itself (de-dupes). SUM cents summed client-side as
BigDecimal(9e18-scale preserved, negatives honored, scale 0). Unsupported window →Failure(UNSUPPORTED_WINDOW)on apply and query, never a silent 0.SeedSupportintentionally not implemented in v1 (seedSupported=false): ADR 0008's per-bucketseed is a tumbling concept and the mix-in is optional; sliding-seed is a tracked follow-up.
velocity-spi+ Lettuce + slf4j.Tests
27 tests against real Redis (Testcontainers
redis:7-alpine), 0 skipped, coverage 97.5% line/ 92.3% branch (gate 80/70). Runs the shared
Count/Sum/Distinct/Purgescenarios with slidingwindows +
SlidingScenarios+CapabilityConformanceScenarios(incl. the apply-side unsupported-windowscenarios).
TumblingScenarios/SeedSupportScenarioscorrectly not run.Review
Hand-implemented (the implementer subagents stalled twice on this task without writing anything — a
delegation-tooling issue, not a design one). Reviewed by the
change-validatoragent against realRedis: VERDICT APPROVE — verified the exclusive-leading-edge sliding semantics, the atomic Lua +
unique-member design (no lost increments), all-aggregation correctness,
seedSupported=falsewith noSeedSupportmix-in, the shared TCK genuinely running, and no forbidden deps.Follow-ups (minor, non-blocking)
ZCARD-after-evict; awindowRangeZCOUNTinside the Luawould tighten the running value under cross-thread clock skew (the final authoritative query is
already exact).
SeedSupportScenariosby window (so a sliding backend couldimplement
SeedSupport) remains a tracked follow-up../gradlew :velocity-backend-redis:build→ BUILD SUCCESSFUL (Testcontainersredis:7-alpine).🤖 Generated with Claude Code