Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 24s
Booking stream carries :booking/:cancel events; live roster is the folded replay so cancelling frees a seat and capacity reopens. ev/cancel! (retrying append-expect), no-op on unbooked, cancelled actor may re-book. Capacity count is folded roster size. 110/110 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
146 lines
5.2 KiB
Plaintext
146 lines
5.2 KiB
Plaintext
;; lib/events/booking.sx — transactional, capacity-safe booking on persist.
|
|
;;
|
|
;; Each bookable occurrence has an append-only stream of :booking / :cancel
|
|
;; events. The live roster is the stream FOLDED in order — a booking adds an
|
|
;; actor, a cancel removes them — so a cancellation frees a seat and capacity
|
|
;; reopens. Capacity safety is the contract: two bookers racing for the last
|
|
;; seat must NEVER both succeed. The guarantee is delegated to persist's
|
|
;; optimistic concurrency — `persist/append-expect` appends only if the
|
|
;; stream's last-seq still equals what the writer observed; otherwise it returns
|
|
;; a conflict value and the writer retries against the advanced roster. So the
|
|
;; capacity check + append are atomic at the persist boundary, no lock.
|
|
;;
|
|
;; A booking/cancel decision is made against an OBSERVED snapshot (folded
|
|
;; roster + last-seq): two concurrent bookers each see the same free seat, both
|
|
;; attempt, and append-expect lets exactly one win — the loser gets a conflict
|
|
;; it retries.
|
|
|
|
(define ev-booking-stream (fn (occ-key) (str "booking:" occ-key)))
|
|
|
|
(define
|
|
ev-bk-member?
|
|
(fn
|
|
(x xs)
|
|
(cond
|
|
((empty? xs) false)
|
|
((= x (first xs)) true)
|
|
(else (ev-bk-member? x (rest xs))))))
|
|
|
|
(define
|
|
ev-bk-index
|
|
(fn
|
|
(xs x i)
|
|
(cond
|
|
((empty? xs) -1)
|
|
((= (first xs) x) i)
|
|
(else (ev-bk-index (rest xs) x (+ i 1))))))
|
|
|
|
(define ev-bk-remove (fn (xs a) (filter (fn (x) (not (= x a))) xs)))
|
|
(define ev-bk-append (fn (xs a) (append xs (list a))))
|
|
|
|
;; Fold a booking stream into the live roster (join order, cancels removed).
|
|
(define
|
|
ev-fold-roster
|
|
(fn
|
|
(events)
|
|
(reduce
|
|
(fn
|
|
(acc e)
|
|
(let
|
|
((typ (persist/event-type e))
|
|
(actor (get (persist/event-data e) :actor)))
|
|
(cond
|
|
((= typ :booking)
|
|
(if (ev-bk-member? actor acc) acc (ev-bk-append acc actor)))
|
|
((= typ :cancel) (ev-bk-remove acc actor))
|
|
(else acc))))
|
|
(list)
|
|
events)))
|
|
|
|
;; Live roster (actors currently holding a seat), oldest active first.
|
|
(define
|
|
ev-booked-actors
|
|
(fn
|
|
(b occ-key)
|
|
(ev-fold-roster (persist/read b (ev-booking-stream occ-key)))))
|
|
|
|
(define
|
|
ev-actor-booked?
|
|
(fn (b occ-key actor) (ev-bk-member? actor (ev-booked-actors b occ-key))))
|
|
|
|
;; Live seat count (folded roster size — not the physical event count).
|
|
(define
|
|
ev-booking-count
|
|
(fn (b occ-key) (len (ev-booked-actors b occ-key))))
|
|
|
|
;; 1-based seat number for an actor on the roster (0 if not booked).
|
|
(define
|
|
ev-seat-of
|
|
(fn
|
|
(actors actor)
|
|
(let
|
|
((i (ev-bk-index actors actor 0)))
|
|
(if (< i 0) 0 (+ i 1)))))
|
|
|
|
;; One booking attempt decided against an OBSERVED snapshot: `observed-actors`
|
|
;; (the roster the booker saw) and `expected` (the last-seq it saw). Returns
|
|
;; :already / :full / :booked / :conflict. :conflict means a concurrent append
|
|
;; landed since the snapshot — the caller must re-observe and retry.
|
|
(define
|
|
ev/book-with-observed
|
|
(fn
|
|
(b occ-key capacity actor observed-actors expected)
|
|
(cond
|
|
((ev-bk-member? actor observed-actors) {:seat (ev-seat-of observed-actors actor) :actor actor :status :already})
|
|
((>= (len observed-actors) capacity) {:actor actor :capacity capacity :status :full})
|
|
(else
|
|
(let
|
|
((r (persist/append-expect b (ev-booking-stream occ-key) expected :booking 0 {:actor actor})))
|
|
(if (persist/conflict? r) {:actual (persist/conflict-actual r) :actor actor :status :conflict} {:seat (+ (len observed-actors) 1) :actor actor :status :booked}))))))
|
|
|
|
;; Capacity-safe booking with retry. Observes a consistent (roster, last-seq)
|
|
;; snapshot, attempts, and retries on conflict (a concurrent booker won the
|
|
;; race) — bounded by capacity.
|
|
(define
|
|
ev/book!
|
|
(fn
|
|
(b occ-key capacity actor)
|
|
(let
|
|
((res (ev/book-with-observed b occ-key capacity actor (ev-booked-actors b occ-key) (persist/last-seq b (ev-booking-stream occ-key)))))
|
|
(if
|
|
(= (get res :status) :conflict)
|
|
(ev/book! b occ-key capacity actor)
|
|
res))))
|
|
|
|
;; One cancellation attempt against an observed snapshot. :not-booked when the
|
|
;; actor holds no seat; :conflict on a racing append (retry); else :cancelled.
|
|
(define
|
|
ev/cancel-with-observed
|
|
(fn
|
|
(b occ-key actor observed-actors expected)
|
|
(cond
|
|
((not (ev-bk-member? actor observed-actors)) {:actor actor :status :not-booked})
|
|
(else
|
|
(let
|
|
((r (persist/append-expect b (ev-booking-stream occ-key) expected :cancel 0 {:actor actor})))
|
|
(if (persist/conflict? r) {:actual (persist/conflict-actual r) :actor actor :status :conflict} {:actor actor :status :cancelled}))))))
|
|
|
|
;; Cancel an actor's seat, freeing capacity. Retries on conflict.
|
|
(define
|
|
ev/cancel!
|
|
(fn
|
|
(b occ-key actor)
|
|
(let
|
|
((res (ev/cancel-with-observed b occ-key actor (ev-booked-actors b occ-key) (persist/last-seq b (ev-booking-stream occ-key)))))
|
|
(if (= (get res :status) :conflict) (ev/cancel! b occ-key actor) res))))
|
|
|
|
;; The roster as a plain list of actors (oldest active first).
|
|
(define ev/roster (fn (b occ-key) (ev-booked-actors b occ-key)))
|
|
|
|
;; Seats remaining for an occurrence of the given capacity.
|
|
(define
|
|
ev/seats-left
|
|
(fn
|
|
(b occ-key capacity)
|
|
(max 0 (- capacity (ev-booking-count b occ-key)))))
|