You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: AUDIT.md
+25-1Lines changed: 25 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,6 +1,7 @@
1
1
# Cycles Protocol v0.1.25 — Server Implementation Audit
2
2
3
-
**Date:** 2026-04-16 (v0.1.25.12 — `sort_by` + `sort_dir` on `GET /v1/reservations` per cycles-protocol spec revision 2026-04-16; 7-value sort enum, opaque cursor binds `(sort_by, sort_dir, filters)` tuple, legacy SCAN-cursor path preserved when both params omitted),
3
+
**Date:** 2026-04-16 (v0.1.25.13 — hydration cap + enum wire annotations on the sorted `GET /v1/reservations` path; `SORTED_HYDRATE_CAP=2000` guard on the in-memory sort hydration with WARN-on-cap, matches admin plane's v0.1.25.24 pattern; `@JsonValue`/`@JsonCreator fromWire` on `ReservationSortBy` + `SortDirection` to mirror admin's `SortSpec`/`SortDirection` contract),
4
+
2026-04-16 (v0.1.25.12 — `sort_by` + `sort_dir` on `GET /v1/reservations` per cycles-protocol spec revision 2026-04-16; 7-value sort enum, opaque cursor binds `(sort_by, sort_dir, filters)` tuple, legacy SCAN-cursor path preserved when both params omitted),
4
5
2026-04-14 (automated performance regression detection — nightly trend + release gate, no version bump),
5
6
2026-04-14 (nightly soak test — long-duration stability coverage, no version bump),
6
7
2026-04-14 (v0.1.25.11 — concurrent retry-storm test for idempotency cache expiry + concurrent accuracy test for custom counters; closes two gaps flagged in the v0.1.25.10 review),
@@ -18,6 +19,29 @@
18
19
19
20
---
20
21
22
+
### 2026-04-16 — v0.1.25.13: hydration cap + enum wire annotations on the sorted list path
23
+
24
+
Closes two follow-up gaps surfaced by the three-step review of v0.1.25.12 against the admin-plane implementation of the same feature in `cycles-server-admin` v0.1.25.24:
25
+
26
+
**P1 — `listReservationsSorted` hydrates an unbounded population before the in-memory sort.** The sorted path in v0.1.25.12 does a full `SCAN` of `reservation:res_*`, hydrates every matching row into a `List<ReservationSummary>`, then sorts + slices. For a tenant with 2M reservations under a single workspace, that's 2M `HGETALL` round-trips and a 2M-entry heap object before the cursor walk even starts. The admin plane ran into exactly the same shape on `ApiKeyRepository.listAllTenantsSorted` and `BudgetRepository.listAllTenantsSorted` and closed it with a `SORTED_HYDRATE_CAP = 2000` constant: when the per-key hydration loop observes `matching.size() >= cap` it sets `capped = true`, breaks out of the SCAN loop, logs a WARN naming the tenant + sort tuple, and the sort/slice/cursor code then operates on the capped slice as normal. Page still fills, `has_more` + `next_cursor` still populate from the capped slice, so the UI isn't blocked — the cap is a heap-safety bound, not a correctness bound.
27
+
28
+
This release ports the same constant + `scanLoop:` labeled-break + post-loop WARN to `RedisReservationRepository.listReservations` on the sorted path only. The legacy (no-sort-params, no-decoded-sorted-cursor) path is intentionally uncapped because it streams page-by-page via the SCAN cursor and never builds an unbounded in-memory list.
29
+
30
+
**Why 2000:** Same rationale as admin — covers 99%+ of production tenants, keeps the heap bound predictable at roughly 2000 × ~2 KB ReservationSummary = ~4 MB per concurrent sorted-list call. Operators who outgrow the cap should either (a) narrow filters (`status`, `idempotency_key`, scope segments `workspace`/`app`/`workflow`/`agent`/`toolset`), or (b) revisit the deferred per-key ZSET index ADR at `docs/deferred-optimizations/sorted-list-zset-indices.md`.
31
+
32
+
**P2 — `ReservationSortBy` and `SortDirection` lacked Jackson wire annotations.** In v0.1.25.12 the enums were plain Java enums; the controller did manual `String.toUpperCase()` → `Enum.valueOf()` conversion with a try/catch → 400. That works, but it diverges from the admin plane where `SortSpec` and `SortDirection` carry `@JsonValue getWire()` + `@JsonCreator fromWire(String)` so Jackson handles the lowercase-on-wire contract natively and controllers delegate to `fromWire(...)` with the same try/catch → 400 shape. This release brings the two runtime-plane enums into line: lowercase `getWire()`, `null → null` on `fromWire(null)`, `IllegalArgumentException` on unknown tokens (which the controller converts to 400 with the documented allow-list payload). The wire contract is unchanged — lowercase tokens in and out — but direct JSON-over-wire uses of these enums (future list-response DTOs that echo the sort tuple back, for instance) now serialize/deserialize correctly without custom converters.
33
+
34
+
**Test strategy:**
35
+
36
+
-`RedisReservationQueryTest#sortedHydrationStopsAtCap` — mocks a SCAN page returning `cap + 10` keys, stubs pipeline `hgetAll` for each, invokes the 5-row sorted page, asserts exactly 5 rows in the documented ascending-`created_at_ms` order and `has_more=true`, `next_cursor != null`. Exercises the capped-slice cursor path. (The full hydration loop consults only up to `cap` rows; remaining stubs are unused as designed.)
No spec change; wire format is identical. No signature changes. No caller-visible behaviour change for tenants under the cap.
40
+
41
+
**Backward compatibility:** Full. Behaviour-visible for tenants whose sorted-list query previously returned >2000 matching rows (silently; those were already on the O(N) full-SCAN path documented in v0.1.25.12). Post-cap, rows beyond row 2000 in the capped slice are unreachable without narrowing filters — same contract the admin plane established in v0.1.25.24 for `ApiKey` + `Budget` cross-tenant sorted paths.
42
+
43
+
---
44
+
21
45
### 2026-04-16 — v0.1.25.12: `sort_by` + `sort_dir` on GET /v1/reservations
22
46
23
47
Closes the runtime-protocol gap opened by **cycles-protocol spec revision 2026-04-16** (commits `064e95f` + `a2a8f13`): list-reservations needed server-side ordering with client-selectable sort key + direction, and the cursor needed to encode the sort state so page breaks remain consistent.
Copy file name to clipboardExpand all lines: README.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -117,7 +117,7 @@ mvn clean install
117
117
./build-all.sh
118
118
```
119
119
120
-
The fat JAR is produced at `cycles-protocol-service-api/target/cycles-protocol-service-api-<version>.jar` (where `<version>` is the `revision` property in `cycles-protocol-service/pom.xml` — e.g. `0.1.25.12`).
120
+
The fat JAR is produced at `cycles-protocol-service-api/target/cycles-protocol-service-api-<version>.jar` (where `<version>` is the `revision` property in `cycles-protocol-service/pom.xml` — e.g. `0.1.25.13`).
121
121
122
122
## Docker Deployment
123
123
@@ -136,7 +136,7 @@ Pre-built images are published to GitHub Container Registry on each release:
136
136
137
137
```
138
138
ghcr.io/runcycles/cycles-server:latest
139
-
ghcr.io/runcycles/cycles-server:<version> # e.g. 0.1.25.12
139
+
ghcr.io/runcycles/cycles-server:<version> # e.g. 0.1.25.13
Copy file name to clipboardExpand all lines: cycles-protocol-service/cycles-protocol-service-api/src/main/java/io/runcycles/protocol/api/controller/ReservationController.java
+6-4Lines changed: 6 additions & 4 deletions
Original file line number
Diff line number
Diff line change
@@ -256,19 +256,21 @@ public ResponseEntity<ReservationListResponse> list(
Copy file name to clipboardExpand all lines: cycles-protocol-service/cycles-protocol-service-data/src/main/java/io/runcycles/protocol/data/repository/RedisReservationRepository.java
+23Lines changed: 23 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -47,6 +47,21 @@ public class RedisReservationRepository {
47
47
.map(Enums.UnitEnum::name)
48
48
.collect(Collectors.joining(","));
49
49
50
+
/**
51
+
* v0.1.25.13 hydration cap for the sorted listReservations path. The sorted
52
+
* path must full-SCAN every `reservation:res_*` key across all tenants
53
+
* before the in-memory sort, which is unbounded in the naive case. At
54
+
* runtime scale (10k reservations per active tenant) this can exhaust
55
+
* heap before the cursor even runs. Cap at 2000 rows per request; when
56
+
* the cap is hit we break out of hydration, sort the capped slice, and
57
+
* serve the cursor from that slice. Operators that need to see beyond
58
+
* the cap should narrow filters (status, scope segments, tenant) — the
Copy file name to clipboardExpand all lines: cycles-protocol-service/cycles-protocol-service-data/src/test/java/io/runcycles/protocol/data/repository/RedisReservationQueryTest.java
Copy file name to clipboardExpand all lines: cycles-protocol-service/cycles-protocol-service-model/src/main/java/io/runcycles/protocol/model/Enums.java
0 commit comments