feat(listReservations): add expires_*/finalized_* time-window filters (v0.1.25.21, closes #162)#163
Merged
Merged
Conversation
… (v0.1.25.21) Closes #162. Implements cycles-protocol-v0.yaml revision 2026-05-22 (runcycles/cycles-protocol#98). Follow-up to v0.1.25.20's from/to window filter — addresses the operational use case that revision intentionally left out: cleanup sweepers that need to locate reservations expiring or already finalized within a window. Four new optional query parameters on GET /v1/reservations: * expires_from / expires_to — inclusive bounds on expires_at_ms. * finalized_from / finalized_to — inclusive bounds on finalized_at_ms. All ISO 8601 date-time. Each pair binds to its target field regardless of sort_by. The three windows (from/to + expires_* + finalized_*) compose with AND semantics. ReservationSummary gains an optional finalized_at_ms field so clients filtering on finalized_* can see the timestamp without a follow-up getReservation call. Optional (@JsonInclude NON_NULL) for strict-schema back-compat. ACTIVE/EXPIRED exclusion under finalized_*: per the spec, rows where finalized_at_ms is absent MUST be excluded when either bound is supplied. The predicate naturally fails on field-absent rows; making the exclusion normative ensures conformant servers agree. Implementation: * Controller: 4 new query params, each validated independently (malformed → 400 with distinct per-param message, from > to → 400 before any repository call, blank strings treated as unset). Mirrors the v0.1.25.20 parseIsoToEpochMs pattern. * Repository: two new predicate helpers (expiresAtInWindow, finalizedAtInWindow). finalizedAtInWindow resolves the timestamp from committed_at OR released_at, matching buildReservationSummary's projection logic. Applied in both legacy SCAN-cursor and sorted paths. * FilterHasher: gains four trailing Long args (10 → 14) with INDEPENDENT gated emission per pair. Preserves byte-exact back-compat for BOTH v0.1.25.18 cursors (no window canonical, golden 2f397ea0e8fb53b7) AND v0.1.25.20 cursors with from/to set (canonical |fr=|to= without expires/finalized, golden ad7204d521cfd133 — newly locked down here). * ReservationSummary model gets the new field; toSummary projection updated. * Java signature change: listReservations 14 → 18 args. No wire change for clients that omit the new params; the new response-body field is optional with NON_NULL serialization. Coverage (557 tests, +19 from v0.1.25.20's 538): * FilterHasherTest +3: expires/finalized distinctness, finalized vs from/to distinctness, v0.1.25.20 8-byte golden lockdown. * RedisReservationQueryTest +6 under ExpiresAndFinalizedWindowFilter nested class: legacy expires_from excludes-below, legacy expires_to excludes-above, finalized excludes ACTIVE rows (no committed_at/released_at), finalized resolves from released_at when committed_at absent, all-three AND composition, cursor mismatch on expires window change. * ReservationControllerTest +10 under ListReservations nested class: 4 malformed-*, 2 reversed-window, expires propagation with verify() lock, finalized propagation with verify() lock, all-three combined with distinct epoch-ms per pair to catch slot mix-ups, blank-as-unset for new windows. JaCoCo 95% bundle gate met. Docs: * AUDIT.md: top date line + new long-form section walking through the rationale, three-pair design, schema addition, cursor back-compat, validation choices, and out-of-scope (time_field-pivoted alternative). * CHANGELOG.md: [0.1.25.21] entry under Keep-a-Changelog format. * README.md (cycles-protocol-service): four new param rows. * pom.xml revision bumped 0.1.25.20 → 0.1.25.21.
…and projection Real Low finding from reviewer on #163. `finalizedAtInWindow` (the new filter predicate) used a committed-wins rule when both `committed_at` and `released_at` were set on a row: `committedAt != null ? committedAt : releasedAt`. `buildReservationSummary` (the existing projection that emits the wire `finalized_at_ms`) used a last-write-wins assignment order where `released_at` overwrites `committed_at`: if (committedAtStr != null) finalizedAtMs = parseLong(committedAtStr); if (releasedAtStr != null) finalizedAtMs = parseLong(releasedAtStr); In the normal lifecycle a row has at most one of these fields (commit XOR release per the Lua scripts), so the disagreement only surfaces on malformed Redis writes. But the spec contract is that the filter operates against the returned `finalized_at_ms` — under the bug, a row with both fields would be filtered using one timestamp and returned with another. Centralized via `resolveFinalizedAtStr(fields)`: String releasedAt = fields.get("released_at"); if (releasedAt != null) return releasedAt; return fields.get("committed_at"); Both call sites now agree on released-wins, matching the pre-fix projection behavior (so no wire change) and the spec contract. Returns the raw String form so each caller can choose its own parse-failure policy: - Predicate (finalizedAtInWindow): catches NumberFormatException, excludes the row defensively when the filter is active. - Projection (buildReservationSummary): lets it propagate as a data-corruption signal (unchanged from pre-fix behavior). Regression test: finalizedResolverAgreesWhenBothFieldsSet — constructs a malformed row with committed_at=3000 and released_at=5000, queries with finalized window [4000, 6000]: - Pre-fix: committed-wins → 3000 not in window → row excluded → response empty (filter says "no match"). But the projection would have emitted 5000 if the row HAD been returned. Disagree. - Post-fix: released-wins → 5000 in window → row included. Projection also emits 5000 on the returned summary. Both paths agree. Test verifies BOTH the inclusion AND the response's finalized_at_ms == 5000. 558 protocol-service tests pass (385 data + 173 api), +1 vs the initial #163 commit. JaCoCo 95% bundle gate met. No version bump — staying at 0.1.25.21. Pure correctness fix on a same-PR finding.
This was referenced May 22, 2026
amavashev
added a commit
to runcycles/cycles-client-typescript
that referenced
this pull request
May 22, 2026
…ugh (0.3.3) (#104) Client-side companion to cycles-protocol-v0.yaml revision 2026-05-22 (runcycles/cycles-protocol#98) and runcycles/cycles-server#163. Closes the TypeScript-client side of issue #162. Follow-up to v0.3.2 with the same shape, just four more params. The existing `listReservations(params?: Record<string, string>)` signature already forwards arbitrary keys to the URL query string, so the four new ISO 8601 date-time params (expires_from / expires_to / finalized_from / finalized_to) work over the wire today without a code change. This commit adds a regression test that pins the URL-encoded passthrough. Call-site: await client.listReservations({ tenant: "acme", expires_from: "2026-05-22T00:00:00Z", expires_to: "2026-05-23T00:00:00Z", finalized_from: "2026-05-15T00:00:00Z", finalized_to: "2026-05-22T00:00:00Z", }); Colons URL-encoded to %3A per native fetch + URLSearchParams. No protocol or wire-format change; servers older than v0.1.25.21 silently ignore the new params per the additive-parameter guarantee. 317 tests pass at 98.4% statement / 99.62% line coverage. Bumped to 0.3.3, updated AUDIT.md and CHANGELOG.md.
amavashev
added a commit
to runcycles/cycles-client-rust
that referenced
this pull request
May 22, 2026
…_at_ms (0.2.6) Client-side companion to cycles-protocol-v0.yaml revision 2026-05-22 (runcycles/cycles-protocol#98) and runcycles/cycles-server#163. Closes the Rust-client side of issue #162. Follow-up to v0.2.5 with the same shape, just four more params on the request side plus one new optional response-side field. ListReservationsParams (request): Four new Option<String> fields, each with skip_serializing_if: expires_from, expires_to, finalized_from, finalized_to. Each pair binds to its target field independent of from/to and any sort_by. finalized_* excludes ACTIVE and EXPIRED rows (finalized_at_ms absent on those rows per the spec). ReservationSummary (response): New Option<u64> field finalized_at_ms with #[serde(default)]. Populated by servers on COMMITTED and RELEASED rows only; absent (None) on ACTIVE/EXPIRED rows and on pre-v0.1.25.21 servers regardless of status. Back-compat preserved both ways by Option + serde default. Regression tests in tests/client_test.rs: * list_reservations_forwards_expires_and_finalized_windows: wiremock query_param matchers assert all four request-side fields land on the wire under the spec names. * list_reservations_deserializes_finalized_at_ms_on_summary: server emits finalized_at_ms on a COMMITTED row, client deserializes to Some(value). * list_reservations_deserializes_absent_finalized_at_ms_as_none: server emits the row WITHOUT finalized_at_ms (pre-revision server or ACTIVE row), client deserializes to None. Locks the back-compat path. No protocol or wire-format change beyond the optional response field; servers older than v0.1.25.21 silently ignore the new request params and don't emit the new response field. 134 tests pass across integration + unit suites; doc-tests + clippy clean. Out of scope (intentionally narrow, same as v0.2.5): pre-existing drift on ListReservationsParams (missing workspace / workflow / toolset / sort_by / sort_dir / idempotency_key fields relative to full v0.1.25 spec parity) is not addressed here. Worth a follow-up. Bumped to 0.2.6, refreshed Cargo.lock, updated AUDIT.md and CHANGELOG.md.
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.
Closes #162. Implements cycles-protocol-v0.yaml revision 2026-05-22. Follow-up to v0.1.25.20 (#160) — same chain, addresses the operational use case that revision intentionally left out.
Summary
Four new optional query parameters on
GET /v1/reservations:expires_from/expires_to— inclusive bounds onexpires_at_ms.finalized_from/finalized_to— inclusive bounds onfinalized_at_ms.All ISO 8601 date-time. Each pair binds to its target field regardless of
sort_by. The three windows (from/to+expires_*+finalized_*) compose with AND semantics.ReservationSummarygains an optionalfinalized_at_msfield so clients filtering onfinalized_*can see the timestamp without a follow-upgetReservationcall.@JsonInclude(NON_NULL)for strict-schema back-compat.Why
The v0.1.25.20
from/towindow binds tocreated_at_ms, which is unhelpful for the operational use case the original issue thread also called out: cleanup sweepers that need to locate reservations expiring or already finalized within a window. A reservation created at T-7d that just expired this morning is invisible to a 24hcreated_atwindow — exactly what a sweeper is looking for.Implementation
Controller (
ReservationController.list)Four new params, each validated independently:
Invalid {param_name}message identifying which parameter failed.expires_from > expires_toandfinalized_from > finalized_to→ 400 before any repository call.Same
parseIsoToEpochMshelper from v0.1.25.20 reused for parsing.Repository (
RedisReservationRepository)Two new predicate helpers:
expiresAtInWindow(fields, fromMs, toMs)— readsexpires_athash field; defensive shape mirrorscreatedAtInWindow.finalizedAtInWindow(fields, fromMs, toMs)— resolves the timestamp fromcommitted_atORreleased_at, matchingbuildReservationSummary's projection. Both absent → row excluded per the normative ACTIVE/EXPIRED exclusion.Both applied in the legacy SCAN-cursor and sorted paths after the existing scope/status/tenant predicates and
createdAtInWindow.FilterHasher (cursor invalidation)
hash(...)gains four trailingLongarguments (10 → 14) with independent gated emission per pair. Each window pair emits its canonical block only when at least one of its bounds is non-null. This preserves byte-exact back-compat for both prior cursor generations:t=acme|i=|st=|ws=|ap=|wf=|ag=|ts=→ golden hash2f397ea0e8fb53b7(locked down in v0.1.25.20)t=acme|i=|st=|ws=|ap=|wf=|ag=|ts=|fr=100|to=200→ golden hashad7204d521cfd133(newly locked down here)A future refactor that drops the gating (e.g., unconditionally appending
|ef=|et=|ff=|ft=blocks) would break both goldens and fail the locked tests.Internal Java signature changes
RedisReservationRepository.listReservations(...)14 → 18 args (trailingLong expiresFromMs, Long expiresToMs, Long finalizedFromMs, Long finalizedToMs).listReservationsSorted(...)mirrors.FilterHasher.hash(...)10 → 14 args.ReservationSummarymodel gainsfinalizedAtMsfield;toSummary(...)projection updated.The only wire-format change is the optional
finalized_at_msfield onReservationSummary— covered by the optional-property guarantee in the spec.Validation matrix
expires_from=not-a-dateINVALID_REQUEST, messageInvalid expires_from: …expires_to=2026-13-99T99:99:99ZInvalid expires_to: …finalized_from=bogusInvalid finalized_from: …finalized_to=bogusInvalid finalized_to: …expires_from > expires_toexpires_from must be less than or equal to expires_to (...). No repository call.finalized_from > finalized_tofrom/to).committed_atand noreleased_at,finalized_*activereleased_at,finalized_*activefinalizedAtInWindowresolves fromreleased_at.sort_by=expires_at_mssort_by.Coverage
557 protocol-service tests pass (384 data + 173 api), +19 from v0.1.25.20's 538:
FilterHasherTest(+3 new): expires_* values differ from base/from-to, finalized_* values differ from from-to/expires_*, v0.1.25.20 from/to golden hash lockdown.RedisReservationQueryTest(+6 new) underExpiresAndFinalizedWindowFilternested class: legacyexpires_fromexcludes-below, legacyexpires_toexcludes-above, legacyfinalized_fromexcludes ACTIVE rows (no committed_at/released_at),finalized_atresolves fromreleased_atwhencommitted_atabsent, all-three AND composition (created + expires + finalized), cursor mismatch on expires window change rejected with 400.ReservationControllerTest(+10 new) underListReservationsnested class: 4 malformed-* tests, 2 reversed-window tests, expires propagation withverify(...)lock, finalized propagation withverify(...)lock, all-three combined with distinct epoch-ms per pair to catch slot mix-ups, blank-as-unset for new windows.JaCoCo 95% bundle gate met.
Out of scope (intentionally)
The
time_field-pivoted alternative parameter shape (onefrom/toplus atime_field=created_at|expires_at|finalized_atselector) was discussed in the spec PR and rejected — it would have been an awkward retroactive change to the v0.1.25.20 shape and split the family-widefrom/toconvention. Three parallel pairs is the cleaner expansion.Test plan
docker compose up(validation paths return 400, happy paths return 200; legacy SCAN cursor still resolves with new windows active)Option<String>fields onListReservationsParams