|
1 | 1 | # Cycles Protocol v0.1.25 — Server Implementation Audit |
2 | 2 |
|
3 | | -**Date:** 2026-04-14 (v0.1.25.9 — second-wave test additions: overdraft property, expire.lua conformance, admin-release race, multi-scope attribution, idempotency-cache expiry, clock-skew, metrics correctness, audit-log completeness), |
| 3 | +**Date:** 2026-04-14 (v0.1.25.10 — custom Micrometer counters for reserve/commit/release/extend/expired/events + overdraft, plus Redis-disconnect resilience test; dormant emitExpiredEvent key-prefix bug fixed as a side effect), |
| 4 | +2026-04-14 (v0.1.25.9 — second-wave test additions: overdraft property, expire.lua conformance, admin-release race, multi-scope attribution, idempotency-cache expiry, clock-skew, metrics correctness, audit-log completeness), |
4 | 5 | 2026-04-14 (property-based concurrent budget-exhaustion test + jqwik-spring lifecycle and tries-override follow-up fixes; passing green on Docker Desktop), |
5 | 6 | 2026-04-12 (spec endpoint-coverage report — parity with admin), |
6 | 7 | 2026-04-12 (spec tracking: pinned SHA → cycles-protocol@main for immediate drift detection), |
|
13 | 14 |
|
14 | 15 | --- |
15 | 16 |
|
| 17 | +### 2026-04-14 — v0.1.25.10: custom business metrics + resilience test |
| 18 | + |
| 19 | +Addresses the largest remaining gap flagged in the v0.1.25.9 retrospective: the service emitted no domain-level metrics, only Spring Boot's generic `http.server.requests` timer. Operators answering "how many denials in the last 5 minutes by reason and tenant" could only infer it from HTTP status codes. This release wires domain counters through a new shared component and extends the existing Micrometer integration test to cover them. |
| 20 | + |
| 21 | +**New class: `CyclesMetrics` (`cycles-protocol-service-data/.../metrics/`)** |
| 22 | +Centralised Micrometer instrumentation. One `record*` method per operation, each mapping to a counter under the `cycles.*` namespace. Tag set prioritises operational signal (tenant, decision, reason, overage_policy, actor_type) while keeping cardinality bounded; the only high-card tag (`tenant`) is toggleable via `cycles.metrics.tenant-tag.enabled` for deployments with many thousands of tenants. |
| 23 | + |
| 24 | +**Counters emitted:** |
| 25 | +- `cycles.reservations.reserve` — every reserve outcome, tagged {tenant, decision, reason, overage_policy}. Idempotent replays record `reason=IDEMPOTENT_REPLAY` so an operator can tell real ALLOWs from cached replays. |
| 26 | +- `cycles.reservations.commit` — every commit outcome, same tag set. |
| 27 | +- `cycles.reservations.release` — every release, tagged {tenant, actor_type=tenant|admin_on_behalf_of, decision, reason}. The actor_type split was v0.1.25.8's dual-auth surface — now directly queryable. |
| 28 | +- `cycles.reservations.extend` — every extend outcome. |
| 29 | +- `cycles.reservations.expired` — one per actual ACTIVE→EXPIRED transition from the sweep. Does NOT bump for grace-period skips or already-finalised candidates. |
| 30 | +- `cycles.events` — every event outcome, same four-tag shape as reserve/commit. |
| 31 | +- `cycles.overdraft.incurred` — incremented whenever a commit or event actually accrued non-zero debt. Unit-free (the amount is in the balance store; this is "how often did we go into overdraft"). |
| 32 | + |
| 33 | +**Modified:** |
| 34 | +- `RedisReservationRepository` — wraps each of `createReservation`, `commitReservation`, `releaseReservation`, `extendReservation`, `createEvent` so the counter emits on both success and exception paths. Method signatures for `commitReservation` and `releaseReservation` gained a `tenant` parameter (and `releaseReservation` an `actorType`); callers in `ReservationController` updated. |
| 35 | +- `ReservationExpiryService` — increments `cycles.reservations.expired` for each Lua-reported EXPIRED result (not per sweep candidate). |
| 36 | +- `cycles-protocol-service-data/pom.xml` — added `io.micrometer:micrometer-core`. |
| 37 | + |
| 38 | +**Dormant bug surfaced and fixed:** `ReservationExpiryService.emitExpiredEvent` was reading `reservation:<id>` instead of `reservation:res_<id>`. Because `jedis.hgetAll` on a missing key returns an empty map (not an error), the method silently no-op'd on every expiry in production — the `reservation.expired` event was never actually emitted. The new counter test exposed it immediately. Existing unit tests (`ReservationExpiryServiceTest`) used mocks keyed to the same wrong prefix so they were self-consistent but didn't catch the real path divergence; test mocks aligned to production in the same commit. |
| 39 | + |
| 40 | +**New integration test: `RedisDisconnectResilienceIntegrationTest`** |
| 41 | +Uses a dedicated Testcontainers Redis (not the shared one from `BaseIntegrationTest`, to avoid breaking parallel tests). Pauses the container mid-request via Docker pause, asserts the commit operation fails with a structured error (not a hang, not a silent 200), resumes the container, asserts a retry succeeds using the still-valid pre-outage reservation, and verifies the TTL index has no orphaned entry post-recovery. Guards the failure class this codebase's positioning claims to prevent (silent failures under a paused downstream). |
| 42 | + |
| 43 | +**Extended: `MetricsCorrectnessIntegrationTest`** |
| 44 | +Adds five nested classes covering every new counter. Each test seeds a clean state, reads the aggregate counter count, drives a known workload, and asserts the exact delta. Uses `Search.counters().stream().mapToDouble(...).sum()` rather than `Search.counter()` because multiple counters can match a partial tag filter (e.g. same tenant+decision but different overage_policy) and `.counter()` on ambiguous searches returns an arbitrary one — the aggregate is what the test needs. |
| 45 | + |
| 46 | +**New unit test: `CyclesMetricsTest`** |
| 47 | +10 tests in `cycles-protocol-service-data` covering every `record*` method's tag shape, null/blank normalisation to the `UNKNOWN` sentinel, and the `cycles.metrics.tenant-tag.enabled=false` path (verifies `tenant` tag is omitted for high-cardinality deployments). |
| 48 | + |
| 49 | +**Wire format:** Unchanged. Response bodies, Lua scripts, error codes, idempotency semantics all identical to v0.1.25.9. |
| 50 | + |
| 51 | +**Verification:** |
| 52 | +- `mvn -B verify --file cycles-protocol-service/pom.xml`: 133 api + 320 data = 453 tests, 0 failures. JaCoCo coverage met (≥95%). Spec coverage 9/9. |
| 53 | +- Property-tests profile unchanged, still passes. |
| 54 | + |
| 55 | +**Modified files:** |
| 56 | +- `cycles-protocol-service/pom.xml` — `<revision>` → `0.1.25.10`. |
| 57 | +- `cycles-protocol-service/cycles-protocol-service-data/pom.xml` — `micrometer-core` dep. |
| 58 | +- `cycles-protocol-service/cycles-protocol-service-data/src/main/java/io/runcycles/protocol/data/metrics/CyclesMetrics.java` — NEW. |
| 59 | +- `cycles-protocol-service/cycles-protocol-service-data/src/main/java/io/runcycles/protocol/data/repository/RedisReservationRepository.java` — instrumented; commit/release signatures gained `tenant`/`actorType`. |
| 60 | +- `cycles-protocol-service/cycles-protocol-service-data/src/main/java/io/runcycles/protocol/data/service/ReservationExpiryService.java` — counter emission + `res_` prefix fix. |
| 61 | +- `cycles-protocol-service/cycles-protocol-service-api/src/main/java/io/runcycles/protocol/api/controller/ReservationController.java` — pass `tenant` + `actorType` to repo. |
| 62 | +- `cycles-protocol-service/cycles-protocol-service-data/src/test/java/io/runcycles/protocol/data/metrics/CyclesMetricsTest.java` — NEW. |
| 63 | +- `cycles-protocol-service/cycles-protocol-service-api/src/test/java/io/runcycles/protocol/api/MetricsCorrectnessIntegrationTest.java` — extended with 8 new tests. |
| 64 | +- `cycles-protocol-service/cycles-protocol-service-api/src/test/java/io/runcycles/protocol/api/RedisDisconnectResilienceIntegrationTest.java` — NEW. |
| 65 | +- Updated mocks/test signatures in `ReservationControllerTest`, `RedisReservationCommitReleaseTest`, `RedisReservationEdgeCaseTest`, `ReservationExpiryServiceTest`, `BaseRedisReservationRepositoryTest`, `BalanceControllerTest`, `DecisionControllerTest`, `EventControllerTest` to match new signatures / provide `CyclesMetrics` mock bean. |
| 66 | +- `AUDIT.md`, `README.md` — this entry + version bump. |
| 67 | + |
| 68 | +--- |
| 69 | + |
16 | 70 | ### 2026-04-14 — v0.1.25.9: second-wave test additions |
17 | 71 |
|
18 | 72 | Follow-up to v0.1.25.8's `BudgetExhaustionConcurrentPropertyTest`. A test-quality review flagged eight further high-leverage gaps (ordered by expected bug-catch density). This release lands all eight in one PR. Every addition reuses the existing `BaseIntegrationTest` + Testcontainers Redis harness and the `@Tag("property-tests")` convention for long-running jqwik suites. |
|
0 commit comments