Skip to content

Latest commit

 

History

History
506 lines (381 loc) · 41.5 KB

File metadata and controls

506 lines (381 loc) · 41.5 KB

Cycles Protocol Server - Benchmark History

Performance benchmark history across versions. All benchmarks use CyclesProtocolBenchmarkTest, CyclesProtocolReadBenchmarkTest, and CyclesProtocolConcurrentBenchmarkTest (Spring Boot + Jedis + Redis 7 via Testcontainers). 200 measured iterations after 50 warmup per operation.

Results are environment-dependent. Use for relative comparison across versions on the same hardware, not as absolute SLA targets. Latencies include the full HTTP round-trip: Spring Boot request handling, auth filter, JSON serialization, Redis EVALSHA, Lua execution, and response building.

Run benchmarks: mvn test -Pbenchmark (requires Docker).

Release coverage

Benchmarks are captured when a release contains hot-path code changes (Lua scripts, repository methods, controller logic). They are deliberately skipped for releases whose changes are wire-format-neutral or test-only — running them would only measure environmental noise. Skipped releases:

  • v0.1.25.8 — admin-on-behalf-of dual-auth (new auth filter, no change to the reserve/commit/release hot path).
  • v0.1.25.9 — test additions only, no production code changes.
  • v0.1.25.10 — metrics instrumentation (Micrometer counter increments on success/failure paths; no Redis/Lua changes). The ReservationExpiryService prefix fix is off the request hot path (runs in the background sweep).
  • v0.1.25.11 — test-only release (concurrent-retry / counter- accuracy regression tests).
  • v0.1.25.12sort_by + sort_dir on GET /v1/reservations list endpoint. The sorted path is opt-in (clients must pass the new params to activate it); legacy list behaviour is byte-for-byte unchanged and all existing benchmarks exercise the legacy path. Benchmarks for the sorted path are worth adding once real tenant populations exercise the O(N) full-SCAN; see OPERATIONS.md.
  • v0.1.25.13 — hydration cap + enum wire annotations on the sorted list path. Write-path unchanged.
  • v0.1.25.14 — trace_id (W3C Trace Context) correlation. The new TraceContextFilter adds two regex matches + one secure-random read per request (~microseconds, no Redis / no locking / no allocation beyond a 32-char String). Request-path code only; Lua / reserve / commit / release / extend hot paths untouched. Benchmarks deliberately skipped — they would only measure environmental noise. [benchmark-skip]

Last benchmarked release: v0.1.25.7.


v0.1.25.7 — Typed Enums.ReasonCode refactor + flaky test fix

Date: 2026-04-11 Tag: v0.1.25.7 Base commit: (to be tagged after PR merge) Environment: Windows 11 Pro for Workstations, AMD Ryzen Threadripper 3990X 64-Core, Java 21, Docker + Redis 7 (Testcontainers)

Changes from v0.1.25.6:

  • DecisionResponse.reasonCode / ReservationCreateResponse.reasonCode retyped from free-form String to typed Enums.ReasonCode (6 values). Compile-time safety against drift from the companion spec enum in runcycles/cycles-protocol#26.
  • 10 .reasonCode(...) call sites in RedisReservationRepository (5 in evaluateDryRun, 5 in decide) updated to pass enum constants; 2 controller boundaries that feed EventDataReservationDenied (which keeps its String-typed reasonCode as that's its own webhook wire contract) now convert via .name() with a null guard.
  • EventEmitterServiceTest de-flaked (#82): 13 Thread.sleep(200) + verify() patterns replaced with Mockito timeout(5000) / after(200).never() verification modes.
  • No wire-format change. Jackson's default enum serialization produces the same JSON strings as the previous String-typed field. Zero runtime path impact expected, and observed — all deltas below are within environmental noise.

Single-Threaded Write-Path Latency

Operation p50 p95 p99 min max mean
Reserve 5.3ms 6.6ms 7.3ms 3.8ms 14.5ms 5.3ms
Commit 4.6ms 5.4ms 5.8ms 3.1ms 14.4ms 4.6ms
Release 4.8ms 5.8ms 6.5ms 3.4ms 6.9ms 4.8ms
Extend 7.5ms 9.4ms 11.0ms 5.7ms 11.8ms 7.5ms
Decide 4.7ms 5.8ms 6.3ms 3.3ms 6.8ms 4.7ms
Event 4.3ms 5.3ms 6.2ms 3.2ms 8.9ms 4.3ms
Reserve + Commit 14.0ms 16.5ms 17.6ms 9.7ms 20.6ms 14.0ms
Reserve + Release 12.1ms 14.1ms 14.9ms 8.1ms 25.0ms 12.1ms

Write-path analysis: All operations equal-or-faster than v0.1.25.6 within ±1ms (Reserve 5.3ms vs 6.0ms, Commit 4.6ms vs 5.0ms, Release 4.8ms vs 4.8ms, Extend 7.5ms vs 7.5ms, Decide 4.7ms vs 5.9ms, Event 4.3ms vs 5.0ms, Reserve+Commit 14.0ms vs 14.3ms, Reserve+Release 12.1ms vs 12.2ms). Deltas are environmental/warmth variance — the refactor is pure compile-time Java typing with Jackson serializing the enum to the same name() string the previous code emitted as a literal. No Redis calls, no Lua, no new allocation on the hot path. No regressions.

Single-Threaded Read-Path Latency

Operation p50 p95 p99 min max mean
GET reservation 4.4ms 5.3ms 5.8ms 2.1ms 7.2ms 4.4ms
GET balances 4.1ms 5.4ms 5.7ms 2.3ms 5.7ms 4.1ms
LIST reservations 4.5ms 5.5ms 5.8ms 2.5ms 7.0ms 4.4ms
Decide (pipelined) 5.8ms 6.9ms 7.1ms 3.9ms 12.0ms 5.8ms

Read-path analysis: Read paths are unchanged in code; numbers within ±1.2ms of v0.1.25.6 (GET reservation 4.4ms vs 3.7ms, GET balances 4.1ms vs 3.9ms, LIST reservations 4.5ms vs 3.9ms, Decide pipelined 5.8ms vs 4.6ms). Environmental noise on a loaded dev box, not a structural change. The Decide (pipelined) read path's slight uptick (+1.2ms) is worth watching but has no code explanation — likely JVM warmup variance since the benchmark doesn't pin CPU affinity.

Concurrent Throughput (Reserve+Commit lifecycle)

Threads Total Ops Ops/sec p50 p95 p99 Errors
8 3,933 786.6 9.9ms 12.0ms 21.9ms 0
16 5,382 1,076.4 14.7ms 20.4ms 24.5ms 0
32 13,158 2,631.6 11.8ms 16.6ms 20.7ms 0

Concurrency analysis: Throughput at 32 threads is 2,632 ops/s — essentially identical to v0.1.25.6's 2,624 ops/s (+0.3%, within noise). Scaling ratio from 8→32 threads is 3.3x (787 → 2,632), matching v0.1.25.6 and v0.1.25.5. p50/p95/p99 latencies at every concurrency level are within ±1.5ms of v0.1.25.6. Zero errors at all concurrency levels. The typed-enum refactor has zero runtime cost on the hot path — confirmed.


v0.1.25.6 — UNIT_MISMATCH vs BUDGET_NOT_FOUND detection on reserve/event/decide

Date: 2026-04-10 Tag: v0.1.25.6 Base commit: 89d2651 Environment: Windows 11 Pro for Workstations, AMD Ryzen Threadripper 3990X 64-Core, Java 21, Docker + Redis 7 (Testcontainers)

Changes from v0.1.25.5:

  • reserve.lua + event.lua — new units_csv ARGV slot; alternate-unit probe added to the #budgeted_scopes == 0 error path (returns 400 UNIT_MISMATCH with {scope, requested_unit, expected_units} when a budget exists at the scope in a different unit, else falls through to BUDGET_NOT_FOUND)
  • event.lua — defensive data-integrity branch aligned to the same response shape
  • RedisReservationRepository — shared probeAlternateUnits(jedis, scope, requestedUnit) helper used by evaluateDryRun and decide(); symmetric Java-side probe throws 400 UNIT_MISMATCH
  • No hot-path change: the probe fires only when every affected scope missed at the requested unit — benchmarks exercise the success path, so the probe is never triggered during measurement

Single-Threaded Write-Path Latency

Operation p50 p95 p99 min max mean
Reserve 6.0ms 7.3ms 7.8ms 4.0ms 8.8ms 6.0ms
Commit 5.0ms 6.2ms 6.7ms 3.5ms 11.6ms 5.0ms
Release 4.8ms 5.9ms 6.9ms 3.6ms 12.9ms 4.8ms
Extend 7.5ms 9.1ms 10.6ms 5.7ms 18.0ms 7.5ms
Decide 5.9ms 7.1ms 7.6ms 4.0ms 12.5ms 5.9ms
Event 5.0ms 6.1ms 7.4ms 3.4ms 13.2ms 5.0ms
Reserve + Commit 14.3ms 17.0ms 20.1ms 11.0ms 21.7ms 14.3ms
Reserve + Release 12.2ms 14.6ms 16.2ms 9.0ms 20.8ms 12.2ms

Write-path analysis: All operations equal-or-faster than v0.1.25.5 (Reserve 6.0ms vs 7.2ms, Commit 5.0ms vs 5.8ms, Release 4.8ms vs 6.0ms, Extend 7.5ms vs 9.0ms, Decide 5.9ms vs 6.3ms, Event 5.0ms vs 6.2ms, Reserve+Commit 14.3ms vs 16.7ms, Reserve+Release 12.2ms vs 14.2ms). The deltas are environmental/warmth variance — the probe block added to reserve.lua / event.lua only executes when #budgeted_scopes == 0, which never fires during benchmark runs (all measured requests hit valid budgets). The extra units_csv ARGV string per request is parsed only if the probe fires, so the hot path ignores it. No regressions; no measurable overhead from the fix.

Single-Threaded Read-Path Latency

Operation p50 p95 p99 min max mean
GET reservation 3.7ms 4.8ms 5.3ms 2.4ms 5.5ms 3.7ms
GET balances 3.9ms 4.7ms 4.9ms 2.3ms 6.1ms 3.9ms
LIST reservations 3.9ms 5.0ms 6.0ms 2.4ms 6.6ms 3.9ms
Decide (pipelined) 4.6ms 5.5ms 5.7ms 3.1ms 5.9ms 4.6ms

Read-path analysis: No read-path code was changed. Numbers are within ±0.4ms of v0.1.25.5 (GET reservation 3.7ms vs 3.5ms, GET balances 3.9ms vs 3.6ms, LIST reservations 3.9ms vs 3.9ms, Decide pipelined 4.6ms vs 4.2ms) — environmental noise, no structural change.

Concurrent Throughput (Reserve+Commit lifecycle)

Threads Total Ops Ops/sec p50 p95 p99 Errors
8 3,947 789.4 9.9ms 12.1ms 17.6ms 0
16 5,612 1,122.4 14.3ms 19.8ms 22.8ms 0
32 13,120 2,624.0 11.8ms 16.5ms 20.5ms 0

Concurrency analysis: Throughput at 32 threads is 2,624 ops/s — within 4% of v0.1.25.5's 2,520 ops/s (slightly higher, well within run-to-run noise). Scaling ratio from 8→32 threads is 3.3x (789 → 2,624), consistent with v0.1.25.5 (3.3x) and prior versions. p99 latency at 32 threads (20.5ms) improved vs v0.1.25.5's 34.5ms — again, environmental variance on a warm system. Zero errors at all concurrency levels. The wrong-unit probe has zero cost on the success path; its cost is paid only when a reservation/event/decide request misses every affected scope at the requested unit (a 4xx error path that no benchmark workload exercises).


v0.1.25.5 — Transition-Based Event Emission (Duplicate Event Fix)

Date: 2026-04-08 Branch: release/v0.1.25.5 Base commit: 9b2f4e1 Environment: Windows 11 Pro, AMD Ryzen Threadripper 3990X 64-Core, Java 21.0.5, Docker 29.3.1, Redis 7 (Testcontainers)

Changes from v0.1.25.4:

  • Lua scripts (reserve, commit, event) include pre-mutation pre_remaining and pre_is_over_limit per scope in balance snapshots
  • Java emits budget state events only on state transitions (not every matching post-state)
  • commit.lua ALLOW_IF_AVAILABLE path: HGET → HMGET (remaining + is_over_limit in one call)
  • commit.lua ALLOW_WITH_OVERDRAFT path: added is_over_limit to existing HMGET (4 → 5 fields)
  • event.lua: folded is_over_limit into existing HMGET (5 → 6 fields, removed separate HGET)
  • No extra Redis calls on any path

Single-Threaded Write-Path Latency

Operation p50 p95 p99 min max mean
Reserve 7.2ms 8.3ms 8.7ms 5.8ms 16.1ms 7.2ms
Commit 5.8ms 6.6ms 7.4ms 4.6ms 14.2ms 5.8ms
Release 6.0ms 6.9ms 7.2ms 4.4ms 14.2ms 6.0ms
Extend 9.0ms 10.3ms 12.9ms 7.4ms 19.3ms 9.0ms
Decide 6.3ms 7.5ms 8.4ms 4.4ms 10.5ms 6.3ms
Event 6.2ms 7.3ms 7.7ms 4.4ms 8.2ms 6.2ms
Reserve + Commit 16.7ms 18.9ms 20.6ms 13.0ms 28.5ms 16.7ms
Reserve + Release 14.2ms 17.2ms 21.3ms 11.5ms 25.3ms 14.2ms

Write-path analysis: All operations consistent with v0.1.25.4. Reserve (7.2ms vs 5.7ms), Commit (5.8ms vs 4.7ms), Release (6.0ms vs 4.8ms), Extend (9.0ms vs 7.6ms), Decide (6.3ms vs 5.5ms), Event (6.2ms vs 5.1ms) — all within normal container/JVM warmth variance across benchmark sessions. The pre-state caching in Lua adds zero extra Redis calls: reserve.lua caches from its existing validation HMGET, commit.lua from existing overage-path reads, event.lua folds is_over_limit into its existing HMGET. No regressions.

Single-Threaded Read-Path Latency

Operation p50 p95 p99 min max mean
GET reservation 3.5ms 4.3ms 4.8ms 2.0ms 5.2ms 3.5ms
GET balances 3.6ms 4.5ms 4.8ms 2.1ms 4.9ms 3.6ms
LIST reservations 3.9ms 4.6ms 4.8ms 2.4ms 4.9ms 3.8ms
Decide (pipelined) 4.2ms 5.0ms 5.5ms 2.9ms 6.2ms 4.2ms

Read-path analysis: No read-path code was changed. All operations consistent with v0.1.25.4 (GET reservation 3.5ms vs 3.8ms, GET balances 3.6ms vs 4.0ms). No regressions.

Concurrent Throughput (Reserve+Commit lifecycle)

Threads Total Ops Ops/sec p50 p95 p99 min max Errors
8 3,764 752.8 10.3ms 12.8ms 23.2ms 7.4ms 32.0ms 0
16 5,316 1,063.2 14.7ms 21.4ms 26.0ms 6.8ms 51.5ms 0
32 12,599 2,519.8 11.5ms 21.2ms 34.5ms 6.7ms 67.6ms 0

Concurrency analysis: Throughput at 32 threads is 2,520 ops/s — within 5% of v0.1.25.4's 2,655 ops/s. The scaling ratio from 8→32 threads is 3.3x (753 → 2,520), consistent with prior versions (v0.1.25.4: 3.4x, v0.1.25.3: 3.5x). p99 at 32 threads (34.5ms) is comparable to v0.1.25.4's 29.8ms. Zero errors at all concurrency levels. The transition fix adds no measurable overhead — pre-state caching piggybacks on existing Lua reads with zero extra Redis calls. Earlier runs on this day showed degraded numbers (~725 ops/s) due to TuneupUI background processes consuming CPU; after termination, normal throughput restored.


v0.1.25.4 — Event Data Payload Completeness

Date: 2026-04-07 Branch: release/v0.1.25.4 Base commit: 175f2fc Environment: Windows 11 Pro, AMD Ryzen Threadripper 3990X 64-Core, Java 21.0.5, Docker 29.3.1, Redis 7 (Testcontainers)

Changes from v0.1.25.3:

  • Populated all missing EventData fields for 6 runtime event types (admin spec compliance)
  • Added per-scope scope_debt_incurred tracking in commit.lua and event.lua (1 extra table insert per overdraft scope)
  • commit.lua reads 1 additional field (scope_path) in initial HMGET and returns 2 extra fields in response JSON
  • Added buildActor() helper extracting keyId/sourceIp from auth context per request
  • ObjectMapper.convertValue calls for action/subject maps on DENY path only (non-hot-path)
  • No changes to reserve.lua, release.lua, extend.lua, or expire.lua

Single-Threaded Write-Path Latency

Operation p50 p95 p99 min max mean
Reserve 5.7ms 6.6ms 6.9ms 4.4ms 7.2ms 5.7ms
Commit 4.7ms 5.6ms 5.9ms 3.2ms 6.5ms 4.7ms
Release 4.8ms 5.8ms 6.0ms 3.2ms 6.3ms 4.8ms
Extend 7.6ms 9.4ms 10.3ms 5.5ms 16.8ms 7.7ms
Decide 5.5ms 6.5ms 8.0ms 3.5ms 16.1ms 5.7ms
Event 5.1ms 6.0ms 6.6ms 3.9ms 14.0ms 5.1ms
Reserve + Commit 14.0ms 15.9ms 19.8ms 10.9ms 23.0ms 14.1ms
Reserve + Release 11.7ms 13.7ms 17.0ms 9.4ms 20.4ms 11.8ms

Write-path analysis: All write operations are within noise of v0.1.25.3. Reserve (5.7ms vs 6.2ms), Commit (4.7ms vs 4.1ms), Release (4.8ms vs 4.8ms), Extend (7.6ms vs 7.4ms), Decide (5.5ms vs 5.5ms), Event (5.1ms vs 5.2ms) — all within normal environmental variance. The extra HMGET field in commit.lua (scope_path) and the scope_debt_incurred table insert add no measurable overhead — both are in-memory Lua operations on a single Redis thread. The buildActor() helper is a lightweight SecurityContext lookup (no I/O). The ObjectMapper.convertValue for action/subject maps only executes on the DENY path (not the benchmark happy path). No regressions detected.

Single-Threaded Read-Path Latency

Operation p50 p95 p99 min max mean
GET reservation 3.8ms 4.7ms 5.2ms 2.3ms 5.8ms 3.8ms
GET balances 4.0ms 4.9ms 5.3ms 2.4ms 5.4ms 4.0ms
LIST reservations 4.3ms 5.2ms 5.3ms 2.8ms 5.8ms 4.3ms
Decide (pipelined) 5.1ms 6.2ms 6.5ms 3.6ms 6.6ms 5.1ms

Read-path analysis: Read operations are slightly higher than v0.1.25.3 (GET reservation 3.8ms vs 2.8ms, GET balances 4.0ms vs 2.9ms) — environmental variance from container state. No read-path code was changed. These numbers remain well within acceptable range and are consistent with v0.1.25.1 baselines (GET reservation 4.0ms, GET balances 4.1ms).

Concurrent Throughput (Reserve+Commit lifecycle)

Threads Total Ops Ops/sec p50 p95 p99 min max Errors
8 3,945 789.0 9.9ms 12.0ms 21.1ms 7.5ms 25.7ms 0
16 5,506 1,101.2 14.2ms 19.8ms 24.2ms 7.0ms 36.5ms 0
32 13,277 2,655.4 11.3ms 17.6ms 29.8ms 7.1ms 66.0ms 0

Concurrency analysis: Throughput at 32 threads is 2,655 ops/s — within 8% of v0.1.25.3's 2,873 ops/s, attributable to environmental variance. The scaling ratio from 8→32 threads is 3.4x (789 → 2,655 ops/s), consistent with prior versions (v0.1.25.3: 3.5x, v0.1.25.1: 3.2x). p99 at 32 threads (29.8ms) is higher than v0.1.25.3's 19.3ms but comparable to v0.1.24.3's 22.7ms — container GC variance. Zero errors at all concurrency levels. The benchmark happy-path lifecycle does not trigger overdraft logic, so the new scope_debt_incurred table tracking is a no-op during benchmarks. Real-world overhead for overdraft commits would be one additional Lua table insert per scope — negligible compared to Redis I/O.


v0.1.25.3 — Extended Runtime Event Emission + PROTOCOL_VERSION Fix

Date: 2026-04-03 Branch: release/v0.1.25.3 Base commit: 32293e4 Environment: Windows 11 Pro, AMD Ryzen Threadripper 3990X 64-Core, Java 21.0.5, Docker 29.3.1, Redis 7 (Testcontainers)

Changes from v0.1.25.1:

  • 4 new async event emissions: budget.exhausted, budget.over_limit_entered, budget.debt_incurred, reservation.expired
  • EventEmitterService.emitBalanceEvents() inspects post-operation balances (no extra Redis calls)
  • ReservationExpiryService.emitExpiredEvent() adds 1 HGETALL per expired reservation for event payload
  • Fixed PROTOCOL_VERSION constant: "0.1.24" → "0.1.25"
  • No changes to Lua scripts or core hot-path logic

Single-Threaded Write-Path Latency

Operation p50 p95 p99 min max mean
Reserve 6.2ms 7.3ms 7.9ms 4.6ms 16.3ms 6.3ms
Commit 4.1ms 5.2ms 5.7ms 3.1ms 6.3ms 4.1ms
Release 4.8ms 6.1ms 6.5ms 3.2ms 6.9ms 4.9ms
Extend 7.4ms 9.2ms 10.2ms 5.4ms 19.3ms 7.5ms
Decide 5.5ms 6.7ms 7.0ms 3.8ms 20.5ms 5.6ms
Event 5.2ms 6.2ms 6.9ms 3.3ms 8.4ms 5.2ms
Reserve + Commit 14.9ms 17.5ms 18.4ms 10.8ms 23.7ms 14.6ms
Reserve + Release 11.4ms 15.5ms 16.7ms 8.7ms 23.8ms 11.9ms

Write-path analysis: All write operations are within noise of v0.1.25.1, confirming the new emitBalanceEvents() calls add zero measurable overhead. Commit improved slightly (4.1ms vs 5.6ms) — environmental variance from a warmer container. Reserve (6.2ms vs 6.9ms), Event (5.2ms vs 6.0ms), and Release (4.8ms vs 5.8ms) all show minor improvements attributable to container warmth. The new balance event emission runs on the existing async thread pool and only iterates the in-memory balances list (no Redis calls), so it does not touch the request hot path. Extend (7.4ms vs 8.6ms) is consistent with its Lua script complexity. No regressions detected.

Single-Threaded Read-Path Latency

Operation p50 p95 p99 min max mean
GET reservation 2.8ms 3.6ms 4.0ms 2.0ms 5.3ms 2.8ms
GET balances 2.9ms 3.7ms 3.9ms 2.1ms 4.0ms 2.9ms
LIST reservations 3.3ms 4.6ms 5.2ms 2.3ms 5.9ms 3.4ms
Decide (pipelined) 3.5ms 4.5ms 5.7ms 2.8ms 6.9ms 3.6ms

Read-path analysis: Read operations improved from v0.1.25.1 (GET reservation 2.8ms vs 4.0ms, GET balances 2.9ms vs 4.1ms, LIST 3.3ms vs 5.0ms, Decide 3.5ms vs 5.6ms). No read-path code was changed, so these improvements are environmental — the v0.1.25.1 benchmark session had higher GC pressure from a different container state. These numbers are now comparable to v0.1.24.3 baselines (GET reservation 2.8ms vs 2.8ms, GET balances 2.9ms vs 2.1ms), confirming the event emission infrastructure adds no sustained overhead to read operations.

Concurrent Throughput (Reserve+Commit lifecycle)

Threads Total Ops Ops/sec p50 p95 p99 min max Errors
8 4,082 816.4 9.6ms 11.6ms 21.0ms 7.1ms 24.5ms 0
16 5,810 1,162.0 13.7ms 19.2ms 22.4ms 6.1ms 28.7ms 0
32 14,363 2,872.6 10.8ms 15.1ms 19.3ms 6.6ms 43.1ms 0

Concurrency analysis: Throughput at 32 threads is 2,873 ops/s — an 11% improvement over v0.1.25.1's 2,584 ops/s and 13% over v0.1.24.3's 2,534 ops/s. This is environmental variance (warmer container, Docker engine update from 29.2.1 to 29.3.1), not a code improvement. The scaling ratio from 8→32 threads is 3.5x (816 → 2,873 ops/s), consistent with prior versions. p99 at 32 threads (19.3ms) improved from v0.1.25.1's 27.2ms and v0.1.24.3's 22.7ms. Max latency (43.1ms) is consistent with expected Redis Lua serialization tail. Zero errors at all concurrency levels confirms the new balance event emission does not introduce contention — emitBalanceEvents() only reads the in-memory balance list on the async thread pool, never competing for Redis connections on the request path. The benchmark happy-path lifecycle (reserve+commit with sufficient budget) does not trigger any of the new events (no exhaustion, no over-limit, no debt), so the emit code path is a no-op during benchmarks. Real-world overhead would be one additional emit() call per triggered condition per scope — the same async fire-and-forget path already validated in v0.1.25.1.


v0.1.25.1 — Webhook Event Emission + TTL Retention

Date: 2026-04-01 Branch: claude/server-events-implementation-uEXga Base commit: 06134dd Environment: Windows 11 Pro, AMD Ryzen Threadripper 3990X 64-Core, Java 21.0.5, Docker 29.2.1, Redis 7 (Testcontainers)

Changes from v0.1.24.3:

  • Webhook event emission on reservation deny and commit overage (EventEmitterService + EventEmitterRepository)
  • 11 event model classes + 6 webhook model classes added
  • CryptoService for AES-256-GCM webhook signing secret encryption
  • Event/delivery TTL retention (90d/14d configurable)
  • RetentionCleanupService for hourly ZSET index trimming
  • No changes to core Lua scripts or hot-path Redis operations

Performance optimizations (post-initial benchmark):

  • Fixed commit overage emit condition: was firing on every commit (actual != null), now only fires when charged > estimated — eliminates emit overhead on normal commits
  • Async event emission: EventEmitterService uses CompletableFuture.runAsync() on a dedicated daemon thread pool, so emit never blocks the request thread
  • Pipelined Redis commands in EventEmitterRepository: event save + subscription lookup in 1 round-trip (was 6 sequential commands), subscription GETs pipelined, delivery creation pipelined

Single-Threaded Write-Path Latency

Operation p50 p95 p99 min max mean
Reserve 6.9ms 7.8ms 8.0ms 5.8ms 13.2ms 7.0ms
Commit 5.6ms 6.5ms 6.7ms 4.5ms 7.5ms 5.7ms
Release 5.8ms 6.5ms 6.8ms 4.6ms 7.0ms 5.7ms
Extend 8.6ms 10.1ms 13.3ms 5.9ms 16.3ms 8.8ms
Decide 6.5ms 7.3ms 8.2ms 5.2ms 17.5ms 6.6ms
Event 6.0ms 6.7ms 7.0ms 4.9ms 8.5ms 6.0ms
Reserve + Commit 16.0ms 17.9ms 19.0ms 13.4ms 22.4ms 16.1ms
Reserve + Release 14.0ms 16.0ms 18.3ms 10.5ms 25.6ms 14.1ms

Write-path analysis: After fixing the emit condition and making emission async, write-path latencies are within noise of v0.1.24.3. Commit dropped from 13.4ms (pre-fix, when every commit triggered a synchronous emit with 6 Redis round-trips) to 5.6ms — now comparable to v0.1.24.3's 4.9ms. The remaining ~0.7ms delta is environmental variance (different container state between benchmark runs). Reserve (6.9ms vs 5.9ms) and Event (6.0ms vs 4.5ms) show small increases attributable to the additional Spring bean initialization overhead, not per-request cost. The buggy emit condition was the dominant cause of the initial 173% commit regression — with the fix, the event emission system adds near-zero overhead to normal (non-overage) operations. Extend remains the slowest operation (8.6ms p50) due to its Lua script complexity, unchanged from v0.1.24.3's 6.9ms (environmental delta).

Single-Threaded Read-Path Latency

Operation p50 p95 p99 min max mean
GET reservation 4.0ms 4.6ms 4.9ms 2.7ms 7.4ms 4.0ms
GET balances 4.1ms 4.9ms 5.2ms 2.7ms 6.0ms 4.0ms
LIST reservations 5.0ms 5.8ms 6.2ms 4.2ms 9.8ms 5.0ms
Decide (pipelined) 5.6ms 6.3ms 6.5ms 4.4ms 7.0ms 5.6ms

Read-path analysis: Read operations show a slight increase from v0.1.24.3 (GET reservation 4.0ms vs 2.8ms, GET balances 4.1ms vs 2.1ms). Since no read-path code was changed in v0.1.25, this delta is environmental: the additional Spring beans (EventEmitterService, CryptoService, RetentionCleanupService) increase the application footprint and may cause slightly more GC pressure. The relative ordering is preserved — GET balances remains the fastest read, Decide (pipelined) remains the slowest due to multi-scope HGETALL batching. These numbers remain well within acceptable latency bounds for a budget authority API.

Concurrent Throughput (Reserve+Commit lifecycle)

Threads Total Ops Ops/sec p50 p95 p99 min max Errors
8 3,922 784.4 10.0ms 12.1ms 18.1ms 7.0ms 34.7ms 0
16 5,447 1,089.4 14.5ms 20.1ms 23.7ms 7.1ms 32.8ms 0
32 12,922 2,584.4 11.7ms 18.2ms 27.2ms 7.0ms 53.0ms 0

Concurrency analysis: Throughput at 32 threads is 2,584 ops/s — fully recovered from the pre-fix 1,801 ops/s and matching v0.1.24.3's 2,534 ops/s. The 50 ops/s improvement over v0.1.24.3 is within noise but confirms the async emit + pipelined Redis commands add no measurable overhead to the hot path. The scaling ratio is 3.3x from 8→32 threads (784 → 2,584 ops/s), consistent with prior versions. Zero errors at all concurrency levels. Max latency at 32 threads (53.0ms) is a single outlier — p99 (27.2ms) is comparable to v0.1.24.3's 22.7ms. The async emit thread pool (daemon threads, availableProcessors/4 size) does not compete with request threads for Redis connections since emission only fires on actual deny/overage events, which don't occur during the benchmark's happy-path lifecycle.


v0.1.24.3 — Performance Optimizations

Date: 2026-03-30 Branch: feature/performance-optimizations Base commit: 0555529 Environment: Windows 11 Pro, AMD Ryzen Threadripper 3990X 64-Core, Java 21.0.5, Docker 29.2.1, Redis 7 (Testcontainers)

Changes from v0.1.24.2:

  • Lua HMGET consolidation (expire, extend, commit, event scripts)
  • Caffeine caches replacing ConcurrentHashMap (ApiKeyRepository, tenant config)
  • ThreadLocal MessageDigest in ApiKeyRepository
  • Pipelined idempotency reads/writes (dry_run, decide)
  • Connection pool tuning (128/32/16, testWhileIdle, idle eviction)
  • Script loading via readAllBytes
  • expire.lua guard condition bugfix

Single-Threaded Write-Path Latency

Operation p50 p95 p99 min max mean
Reserve 5.9ms 7.1ms 7.4ms 4.4ms 7.6ms 5.9ms
Commit 4.9ms 6.1ms 12.2ms 3.4ms 20.9ms 5.1ms
Release 4.8ms 6.2ms 6.9ms 3.5ms 13.9ms 4.9ms
Extend 6.9ms 8.7ms 9.3ms 5.3ms 10.8ms 7.1ms
Decide 5.8ms 6.8ms 7.1ms 4.0ms 7.1ms 5.7ms
Event 4.5ms 5.6ms 6.0ms 3.5ms 12.0ms 4.6ms
Reserve + Commit 14.3ms 17.2ms 18.3ms 11.3ms 22.0ms 14.4ms
Reserve + Release 13.2ms 15.6ms 18.4ms 9.0ms 21.6ms 13.0ms

Write-path analysis: Median latencies are within noise of v0.1.24.2 when accounting for test execution order (this run had warm-container advantage). Extend improved slightly (6.9ms vs 7.1ms) due to HMGET consolidation reducing 7 separate HGET calls to 2 batched HMGET calls inside the Lua script. Event is flat at 4.5ms — the scope budget cache eliminates redundant HGET calls across validation/capping/mutation phases, but the dominant cost is the Lua script execution itself which is unchanged. Commit p99 tail (12.2ms) is a known pattern from the PEXPIRE command on terminal hashes occasionally coinciding with Redis background save or container GC pauses.

Single-Threaded Read-Path Latency

Operation p50 p95 p99 min max mean
GET reservation 2.8ms 3.8ms 4.3ms 1.5ms 5.0ms 2.8ms
GET balances 2.1ms 2.9ms 3.6ms 1.5ms 3.8ms 2.2ms
LIST reservations 2.6ms 3.6ms 4.2ms 1.8ms 4.6ms 2.7ms
Decide (pipelined) 3.2ms 4.5ms 5.0ms 2.2ms 5.4ms 3.2ms

Read-path analysis: Read operations are the fastest since they bypass Lua scripts entirely — just direct Redis hash reads (pipelined for multi-scope queries). GET balances at 2.1ms p50 is identical to v0.1.24.2, confirming no regression. Decide at 3.2ms p50 benefits from the pipelined idempotency check (2 GETs in 1 round-trip instead of 2 sequential calls). These operations were not the focus of the Lua optimizations since they don't execute Lua scripts.

Concurrent Throughput (Reserve+Commit lifecycle)

Threads Total Ops Ops/sec p50 p95 p99 min max Errors
8 3,844 768.8 10.2ms 12.6ms 17.6ms 7.4ms 30.0ms 0
16 5,496 1,099.2 14.3ms 19.9ms 24.1ms 6.9ms 42.3ms 0
32 12,668 2,533.6 12.1ms 17.9ms 22.7ms 6.8ms 45.2ms 0

Concurrency analysis: Throughput scales near-linearly from 8 to 32 threads (769 to 2,534 ops/s — 3.3x at 4x threads). Zero errors at all levels. The pool increase from 50 to 128 max connections prevents exhaustion under load. testWhileIdle validates idle connections in the background (every 30s) which avoids stale-connection spikes without adding per-request PING overhead. Max latency at 32 threads (45.2ms) improved from 51.0ms in v0.1.24.0 — fewer extreme outliers due to better connection pool health. The p50 at 32 threads (12.1ms) being lower than 16 threads (14.3ms) reflects Redis Lua script serialization: more threads means more pipeline overlap while waiting for Lua execution.


v0.1.24.2 — Baseline

Date: 2026-03-30 Branch: main Commit: 0555529 Environment: Windows 11 Pro, AMD Ryzen Threadripper 3990X 64-Core, Java 21.0.5, Docker 29.2.1, Redis 7 (Testcontainers)

Note: This run executed first (cold container) in the comparison session. v0.1.24.3 ran second (warm). Single-threaded numbers may be ~10% pessimistic due to cold JVM and container OS page cache.

Single-Threaded Write-Path Latency

Operation p50 p95 p99 min max mean
Reserve 5.3ms 6.7ms 7.9ms 4.2ms 14.1ms 5.5ms
Commit 4.1ms 5.2ms 5.6ms 3.2ms 10.6ms 4.2ms
Release 4.3ms 5.1ms 5.7ms 3.4ms 6.3ms 4.3ms
Extend 7.1ms 8.6ms 9.9ms 5.4ms 17.0ms 7.2ms
Decide 5.5ms 6.6ms 7.6ms 4.5ms 7.8ms 5.6ms
Event 4.5ms 5.5ms 5.8ms 3.5ms 9.4ms 4.5ms
Reserve + Commit 12.9ms 15.5ms 16.3ms 10.3ms 23.6ms 12.9ms
Reserve + Release 10.4ms 12.7ms 14.3ms 8.4ms 22.8ms 10.6ms

Write-path analysis: This is the pre-optimization baseline. Each Lua script uses individual HGET calls (e.g., commit.lua makes 9 separate HGET calls plus 2 more for expires_at/grace_ms). The connection pool is sized at 50/10/5 (max/maxIdle/minIdle) with no health checks. Extend is the slowest operation (7.1ms p50) because it executes the Lua script, then makes 3 additional HGET calls for balance collection — these are now consolidated into a single HMGET in v0.1.24.3. Reserve max of 14.1ms shows occasional cold-connection latency spikes that the pool health checks in v0.1.24.3 prevent.

Single-Threaded Read-Path Latency

Operation p50 p95 p99 min max mean
GET reservation 2.1ms 2.9ms 3.4ms 1.4ms 5.0ms 2.2ms
GET balances 2.1ms 3.2ms 3.9ms 1.5ms 4.2ms 2.2ms
LIST reservations 2.3ms 3.2ms 3.6ms 1.8ms 3.9ms 2.4ms
Decide (pipelined) 3.1ms 4.1ms 4.7ms 2.4ms 7.1ms 3.2ms

Read-path analysis: Read operations were already pipelined in v0.1.24.2 (added in Phase 3 of that release). These are pure Redis hash reads — no Lua scripts involved. Decide uses a pipeline to batch budget HGETALL calls across all scopes, but its idempotency check still makes 2 sequential GET calls (optimized to a single pipeline in v0.1.24.3). The low min values (1.4ms for GET) represent the best case when the JVM is warm and Redis response is immediate.

Concurrent Throughput (Reserve+Commit lifecycle)

Threads Total Ops Ops/sec p50 p95 p99 min max Errors
8 4,199 839.8 9.3ms 11.4ms 16.3ms 6.5ms 31.9ms 0
16 6,772 1,354.4 12.0ms 17.9ms 21.8ms 5.4ms 31.5ms 0
32 13,687 2,737.4 11.2ms 16.6ms 21.0ms 6.5ms 41.4ms 0

Concurrency analysis: Good throughput scaling at 2,737 ops/s with 32 threads. The connection pool (max 50) is sufficient for this test but leaves limited headroom — with 32 threads each needing a connection for Reserve then Commit (2 connections per lifecycle), pool contention is possible. The ConcurrentHashMap-based caches (API key, tenant config) use O(n) lazy eviction when size exceeds 10,000 entries, which could block request threads under sustained load — replaced with Caffeine's O(1) eviction in v0.1.24.3. Zero errors across all concurrency levels confirms Redis Lua atomicity prevents race conditions even under contention.


v0.1.24.0 — Initial Benchmarks (from AUDIT.md)

Date: 2026-03-24 Environment: Prior machine/environment (not directly comparable to v0.1.24.2+)

Note: These numbers were captured on different hardware than v0.1.24.2/v0.1.24.3. They represent the state after the initial performance optimization pass (EVALSHA, BCrypt cache, pipelined balance fetch, Lua-returned balances). Use only for trend analysis, not direct comparison.

Single-Threaded Write-Path Latency

Operation p50 p95 p99 min max mean
Reserve 5.1ms 6.1ms 6.8ms 4.1ms 12.9ms 5.1ms
Commit 4.3ms 5.5ms 12.1ms 2.8ms 24.1ms 4.5ms
Release 4.4ms 5.3ms 5.8ms 3.2ms 6.2ms 4.5ms
Extend 7.4ms 9.1ms 10.7ms 5.9ms 17.7ms 7.6ms
Decide 5.4ms 6.2ms 6.7ms 4.2ms 6.9ms 5.5ms
Event 4.6ms 5.7ms 6.4ms 3.4ms 8.3ms 4.7ms
Reserve + Commit 12.9ms 15.6ms 17.9ms 10.2ms 21.8ms 13.2ms
Reserve + Release 10.4ms 12.1ms 13.9ms 9.0ms 20.3ms 10.6ms

Write-path analysis: Baseline after the first round of optimizations: EVALSHA (saves 1-5KB network per Lua call), BCrypt API key cache (eliminates ~100ms+ BCrypt on cache hit), and Lua-returned balance snapshots (eliminates post-operation Java balance fetch). Commit p99 of 12.1ms and max of 24.1ms show significant tail latency — the PEXPIRE on terminal reservation hashes occasionally coincides with Redis background saves. Extend at 7.4ms is the slowest write due to its Lua script making 7 individual HGET calls (3 for validation + 3 for balance scope data + 1 more for estimate_unit).

Single-Threaded Read-Path Latency

Operation p50 p95 p99 min max mean
GET reservation 2.5ms 3.3ms 3.7ms 1.5ms 9.2ms 2.5ms
GET balances 2.8ms 4.0ms 4.6ms 1.6ms 4.8ms 2.8ms
LIST reservations 3.2ms 4.2ms 4.6ms 2.0ms 5.3ms 3.2ms
Decide (pipelined) 4.6ms 5.8ms 6.5ms 2.7ms 7.7ms 4.7ms

Read-path analysis: Read-path pipelining was introduced in this version (Phase 3). Decide at 4.6ms p50 uses a pipeline for budget HGETALL calls but still has 2 sequential GET calls for idempotency. GET reservation max of 9.2ms is an outlier — likely a cold connection or container GC pause. LIST at 3.2ms uses Redis SCAN with pipelined HGETALL for each batch.

Concurrent Throughput (Reserve+Commit lifecycle)

Threads Total Ops Ops/sec p50 p95 p99 min max Errors
8 3,993 798.6 9.8ms 11.9ms 16.9ms 6.7ms 30.9ms 0
16 5,737 1,147.4 13.9ms 19.1ms 22.6ms 6.8ms 32.1ms 0
32 12,775 2,555.0 11.7ms 18.9ms 27.9ms 6.8ms 51.0ms 0

Concurrency analysis: Near-linear scaling from 8 to 32 threads (799 to 2,555 ops/s — 3.2x at 4x threads). The max latency at 32 threads (51.0ms) is notable — this is the highest observed across all versions and likely reflects connection pool contention at the max-50 pool size combined with ConcurrentHashMap eviction pauses. The 27.9ms p99 at 32 threads shows the tail cost of Redis Lua script serialization: only one script can execute at a time on the Redis server, so under high concurrency, scripts queue behind each other. Zero errors confirms the system is functionally correct under load.