Skip to content

Add velocity-backend-redis: the sliding hot-path reference backend - #10

Merged
wolpert merged 1 commit into
mainfrom
velocity-backend-redis
Jul 19, 2026
Merged

Add velocity-backend-redis: the sliding hot-path reference backend#10
wolpert merged 1 commit into
mainfrom
velocity-backend-redis

Conversation

@wolpert

@wolpert wolpert commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Summary

velocity-backend-redis — the second real velocity-spi backend (the v1 Redis/Lettuce sliding
hot-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

  • 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 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).
  • 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) — matching
    the in-memory reference, so SlidingScenarios (aging / exclusive edge) pass.
  • Storage: one ZSET per (namespace, subject, aggregation, window) — no feature name in the key
    (a 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 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.
  • No Dagger/Dropwizard/Jackson — depends only on 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/Purge scenarios with sliding
windows + SlidingScenarios + CapabilityConformanceScenarios (incl. the apply-side unsupported-window
scenarios). TumblingScenarios/SeedSupportScenarios correctly 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-validator agent against real
Redis: VERDICT APPROVE — verified the exclusive-leading-edge sliding semantics, the atomic Lua +
unique-member design (no lost increments), all-aggregation correctness, seedSupported=false with no
SeedSupport mix-in, the shared TCK genuinely running, and no forbidden deps.

Follow-ups (minor, non-blocking)

  • The COUNT/DISTINCT apply return uses ZCARD-after-evict; a windowRange ZCOUNT inside the Lua
    would tighten the running value under cross-thread clock skew (the final authoritative query is
    already exact).
  • Sliding-seed design + parameterizing SeedSupportScenarios by window (so a sliding backend could
    implement SeedSupport) remains a tracked follow-up.

./gradlew :velocity-backend-redis:buildBUILD SUCCESSFUL (Testcontainers redis:7-alpine).

🤖 Generated with Claude Code

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>
@wolpert
wolpert merged commit c88f227 into main Jul 19, 2026
3 checks passed
@wolpert
wolpert deleted the velocity-backend-redis branch July 19, 2026 14:05
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