A coffee shop with one barista and one espresso machine. Drip orders need only the barista; espresso orders need both, with the barista held throughout. The reference model for the compound hold pattern (holding one resource while waiting for another), multi-resource serial coordination, and the richest composition of primitives in the example set: two Facilities, a Priority wait set, forked patience timers, and a deeper state machine.
Arrivals: Exp(inter-arrival from the barbershop model file, ~3 min cadence). Each customer independently wants:
Espresso orders take priority at the barista (rank 0 vs 1). All customers have a 5-minute patience; they renege if not served in time. Simulate an 8-hour day and report per-order-type served counts, reneges, and wait times.
This is not a GPSS-book example — it's a puck-idiom showpiece composing the primitives. No single GPSS block maps cleanly because GPSS doesn't have a native "hold resource A while seizing resource B" idiom; the transaction would typically be expressed as two nested SEIZE blocks, which is what this model does, just as puck state transitions.
Customer (coffee_shop.odin:77-151)
— a six-state machine (Arriving → Waiting_For_Barista → {Making_Drip | Waiting_For_Machine → Pulling_Shot → Finishing_Espresso} → Done). The branching is on c.order.Patience_Puck (coffee_shop.odin:208-232)
— the fork offspring, identical in shape to
reneging's timeout puck.Gen (coffee_shop.odin:236-272) —
generator that classifies each arrival as drip vs espresso from
a deterministic hash of the customer id.Shop (coffee_shop.odin:41-48) holds:
barista: sim.Facility — created with .Priority order so
espresso orders jump the drip queue.espresso_machine: sim.Facility — plain FIFO.customer_pool and patience_pool.The compound hold. When an espresso customer reaches
customer_start_order, it already owns the barista. It then
calls sim.seize(&shop.espresso_machine, &c.puck) — if that
succeeds, pull the shot immediately; if it fails, the customer
suspends on the machine's wait set while still holding the
barista. This is intentional backpressure: a queued espresso
order blocks the barista from helping anyone else until the
machine frees up. The model depends on this behavior — it's the
whole point.
case .Espresso:
c.state = .Waiting_For_Machine
if sim.seize(&c.shop.espresso_machine, &c.puck) {
// Machine was free — straight into Pulling_Shot
} else {
// Holding barista, waiting for machine.
}
The released wakeup lands the customer back in
.Waiting_For_Machine; the tick proc transitions it to
.Pulling_Shot and advances 4 minutes. When the shot is done,
the machine is released first (.Pulling_Shot → .Finishing_Espresso)
but the barista stays held for the finishing minute. This
sequential release is what separates "resource A held throughout
operation X" from "resource A handed off when operation Y
starts" — it's all in the order of sim.release calls across
states.
Priority on the Facility, not a model-owned Set. Unlike
tool_crib — which manages priority in a model-owned Set with
-priority ranks — the coffee shop uses
sim.facility_create(.Priority) and passes rank: f64 = c.order == .Espresso ? 0 : 1 to sim.seize. This is the "cheap priority"
form: policy lives on the Facility, callers pass a rank at seize
time, no dispatcher needed. It works here because:
sim.seize directly, which either wins
immediately or joins the correctly-sorted queue).Contrast with tool_crib, where two separate generators feed a
shared list and a dispatcher is needed because priority inversions
happen between independent arrivals — the Facility's internal
ordering alone isn't enough. Here, the simpler form suffices.
Six states, not four. The compound hold adds two extra
waypoints: Waiting_For_Machine (arrived at barista, blocked on
machine) and Pulling_Shot (holding both). This is where puck
state machines stretch: each new concurrent resource adds a state,
each new sequential phase adds a state. Five or six states per
entity is around the ceiling for readable puck code; beyond that,
a sub-machine or a helper proc per-phase starts to earn its keep.
Fork-with-flags, not wait_until. Reneging here uses the
same pattern as the reneging example:
a Patience_Puck that fires once, checks being_served, and
either stands down or flips reneged and reactivates the
customer. The renege path explicitly removes the customer from
barista.waiters (coffee_shop.odin:110) —
the customer can only be waiting on the barista at this point
(the renege check happens in .Waiting_For_Barista state), not
on the machine. A customer already holding the barista but
queued on the machine cannot renege in this model; their
patience flag would have been suppressed by being_served = true
in customer_start_order. That's a modeling choice: once you
have the barista, you're committed.
Order type by deterministic hash, not RNG stream. The mix is
60/40 drip/espresso, selected by hash := u64(id) * 2654435761; hash % 100 < 40. This is a deterministic pseudo-RNG that doesn't
consume the model's db.RV streams — a note-to-self placeholder,
same as the patience field in reneging. A clean version would
use a dedicated type_rng stream with db.random_real.
wait_untilSLX's compound-hold could read:
seize barista;
wait until (machine free); // predicate over shop state
seize machine;
advance 4; release machine;
advance 1; release barista;
In odin-des terms, that would be a Notify_Var on machine
availability and a sim.wait_until in place of the
seize-with-enqueue. It would work, but it's strictly more
machinery than sim.seize already provides (seize handles
"wait until free" correctly via its waiters set). wait_until
earns its keep when the predicate is compound — "machine free
AND barista break not active AND supplies above threshold" —
where no single Facility can answer. For a single-resource wait,
sim.seize is the right tool.
Storage with capacity 1Semantically identical, notationally heavier. Facility with
FIFO waiters is the tighter fit for single-unit equipment.
The six states could flatten into a single body switching on
(order, state) pairs. It would be shorter in line count,
harder to follow. The current split by state is more maintainable
— each case is one event, one transition, one advance or seize.
Patience_Puck code with renegingPatience_Puck here and Timeout_Puck in reneging are
essentially the same thing. Extracting sim.patience(customer, duration, &reneged_flag, &being_served_flag) would collapse both
— but once it's extracted, the naming will inform the next
example that needs it. Deferred, same discipline as
pruning-status.md §"Patterns
emerging" calls for: two occurrences is not enough, and the flag
protocol between parent and child is subtle enough that a helper
needs a careful API (which fields does it write? what happens if
the customer forgets to set being_served?). Leaving both
inlined keeps the code honest for now.
If drip and espresso had the same priority, espresso customers
would regularly get stuck behind drip orders and see higher
average wait despite needing the barista longer. The model's
.Priority ordering is the policy lever — worth toggling in a
sensitivity analysis to see the effect.
This is the reference for:
sim.facility_create(.Priority) + seize-rank
as the light-weight alternative to dispatcher-managed priority
(contrast with tool_crib)./coffee_shop [options]
Add --json to emit the uniform envelope (metadata, execution_stats,
metrics, details) instead of the default text output.
| Flag | Type | Default | Description |
|---|---|---|---|
--json |
bool | false |
Emit uniform JSON envelope instead of text. |
Example runs:
./coffee_shop # default text run
./coffee_shop --json # uniform envelope
Edit constants in coffee_shop.odin to vary order mix, service
times, patience, or run length — those are not exposed as CLI flags.
odin run examples/coffee_shop
Default output is a verbose trace (customer-by-customer event log) plus an end-of-day summary by order type.
Facility with .Priority order and
seize-time rankreneging — source of the patience
fork pattern this model reuses; read it first for the fork
mechanicstool_crib — the alternative
priority implementation (model-owned Set + dispatcher);
contrast with the Facility-native approach here to see when
each fitsgated_shop — the two-resource
shape in a different composition (Gate + Facility, serial
admission); this example is the "parallel hold" counterpartdocs/wait-until-and-helpers.md — the deeper alternative to
sim.seize with compound predicates