Commit bd31064
committed
feat(reservations): support sort_by + sort_dir on GET /v1/reservations
Implements cycles-protocol-v0.yaml revision 2026-04-16: clients can now
request server-side ordering of list-reservations results by passing
sort_by (one of reservation_id, tenant, scope_path, status, reserved,
created_at_ms, expires_at_ms) and sort_dir (asc | desc, default desc).
Invalid enum values return HTTP 400 INVALID_REQUEST.
Design: dual-path. When both params are omitted the repository keeps
its existing Redis-SCAN cursor loop byte-for-byte unchanged, so clients
that never send the new params see zero wire or behaviour difference.
When sort_by or sort_dir is present (or the incoming cursor is a sorted-
path cursor), the repository runs a full SCAN pass with the existing
filter predicates, sorts in memory via ReservationComparators with a
stable reservation_id ASC tiebreaker, and emits an opaque slice cursor
that binds the (sort_by, sort_dir, filters) tuple via an 8-byte SHA-256
filter hash. Reusing a cursor under a mismatched tuple returns 400.
New support utilities under .repository.support:
- SortedListCursor (base64url-no-pad Jackson JSON, legacy digit-only
cursor falls through to the SCAN path by design)
- FilterHasher (SHA-256 of canonical k=v|k=v|... over 8 filter fields,
first 16 hex chars — collision-safe for server-side tuple detection)
- ReservationComparators (per-key extractors with nullsLast, plus an
extractSortValue helper used for cursor lsv encoding)
Repository signature: RedisReservationRepository.listReservations now
takes trailing sortBy, sortDir parameters (10 -> 12 args). The 10-arg
overload was removed intentionally — keeping both caused Mockito stubs
defined over 10 args to silently miss 12-arg call sites from the
updated controller, producing null responses under test.
Tests:
- ReservationControllerTest: 4 new cases (invalid sort_by/sort_dir
reject with 400, propagation to repo, all 7 spec enum values
accepted). Pre-existing Mockito stubs updated to 12-arg signature.
- ReservationComparatorsTest (new): all 7 primary sort keys, ASC/DESC,
reservation_id tiebreaker, nullsLast across every key, null-safe
extractSortValue, default-to-created_at_ms fallback.
- SortedListCursorTest (new): round-trip, malformed/digit-only inputs
(legacy SCAN passthrough), filter-hash stability across key ordering.
- FilterHasherTest (new): determinism, null/empty equivalence, 16-hex
shape.
- RedisReservationQueryTest Testcontainers: new @nested sorted-path
class covering 3-page pagination by created_at_ms ASC, cross-tuple
cursor rejection (400), legacy numeric-cursor compatibility,
scope_path lexicographic ordering.
Wire-format invariance: omitting both params produces byte-identical
responses to v0.1.25.11 (SCAN cursor, same has_more semantics). The
sorted path is strictly additive.1 parent 1f5909c commit bd31064
11 files changed
Lines changed: 1083 additions & 20 deletions
File tree
- cycles-protocol-service
- cycles-protocol-service-api/src
- main/java/io/runcycles/protocol/api/controller
- test/java/io/runcycles/protocol/api/controller
- cycles-protocol-service-data/src
- main/java/io/runcycles/protocol/data/repository
- support
- test/java/io/runcycles/protocol/data/repository
- support
- cycles-protocol-service-model/src/main/java/io/runcycles/protocol/model
Lines changed: 27 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
241 | 241 | | |
242 | 242 | | |
243 | 243 | | |
244 | | - | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
245 | 247 | | |
246 | 248 | | |
247 | 249 | | |
| |||
251 | 253 | | |
252 | 254 | | |
253 | 255 | | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
254 | 277 | | |
255 | 278 | | |
256 | 279 | | |
| |||
271 | 294 | | |
272 | 295 | | |
273 | 296 | | |
274 | | - | |
| 297 | + | |
| 298 | + | |
275 | 299 | | |
276 | | - | |
| 300 | + | |
277 | 301 | | |
278 | 302 | | |
Lines changed: 56 additions & 6 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
498 | 498 | | |
499 | 499 | | |
500 | 500 | | |
501 | | - | |
| 501 | + | |
502 | 502 | | |
503 | 503 | | |
504 | 504 | | |
| |||
517 | 517 | | |
518 | 518 | | |
519 | 519 | | |
520 | | - | |
| 520 | + | |
521 | 521 | | |
522 | 522 | | |
523 | 523 | | |
| |||
529 | 529 | | |
530 | 530 | | |
531 | 531 | | |
532 | | - | |
| 532 | + | |
533 | 533 | | |
534 | 534 | | |
535 | 535 | | |
| |||
540 | 540 | | |
541 | 541 | | |
542 | 542 | | |
543 | | - | |
| 543 | + | |
544 | 544 | | |
545 | 545 | | |
546 | 546 | | |
| |||
558 | 558 | | |
559 | 559 | | |
560 | 560 | | |
561 | | - | |
| 561 | + | |
562 | 562 | | |
563 | 563 | | |
564 | 564 | | |
565 | 565 | | |
566 | 566 | | |
567 | 567 | | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
568 | 618 | | |
569 | 619 | | |
570 | 620 | | |
| |||
583 | 633 | | |
584 | 634 | | |
585 | 635 | | |
586 | | - | |
| 636 | + | |
587 | 637 | | |
588 | 638 | | |
589 | 639 | | |
| |||
0 commit comments