← QueueSim home  ·  All models

Coffee Shop (Compound Hold) — README

Coffee Shop

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.

Problem

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.

Model in this directory

Shop (coffee_shop.odin:41-48) holds:

Why this shape

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:

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.

Alternatives considered

Express the espresso path with wait_until

SLX'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.

Model the machine as a Storage with capacity 1

Semantically identical, notationally heavier. Facility with FIFO waiters is the tighter fit for single-unit equipment.

One giant tick proc with nested switches

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.

Share Patience_Puck code with reneging

Patience_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.

Drop the priority on the barista

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.

What this example teaches

This is the reference for:

CLI

./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.

Running it

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.

See also