← QueueSim home  ·  All models

Gated Shop — README

Gated Shop

A barbershop with a capacity-limited shop floor: at most MAX_IN_SHOP customers may be inside (queue + chair) at once; the rest wait outside. The reference implementation of GPSS GATE SNF and the example that surfaced the release-then-owner-check gotcha during Phase A pruning.

Problem

Same arrival/service distributions as the plain barbershop (Exp(15) inter-arrival, Exp(12) service) but the shop holds only 3 customers total. Arrivals when the shop is full do not enter — they wait outside in the gate's wait set. When a served customer exits, the next outside-waiter is admitted automatically.

GPSS:

shop_capacity STORAGE   3
barber        FACILITY

 GENERATE   (Exponential 15)
 ENTER      shop_capacity     ; GATE SNF — wait outside if full
 SEIZE      barber
 ADVANCE    (Exponential 12)
 RELEASE    barber
 LEAVE      shop_capacity
 TERMINATE

The whole transaction lives in one customer puck whose state machine walks gate_enter → seize → advance → release → gate_exit.

Model in this directory

One puck type:

Shop (gated_shop.odin:47-56) holds a sim.Gate (capacity 3) and a sim.Facility (the barber). Stats track served, total_sojourn, and max_outside.

Why this shape

The customer tick is one state machine, not two coordinated pucks. GPSS lets a transaction flow through ENTER / SEIZE / ADVANCE / RELEASE / LEAVE linearly; the puck idiom collapses that into a state enum where each suspending operation (gate, facility, advance) is a state. This keeps the model's flow visible in one place rather than scattered across handlers.

Trying_Barber is the convergence state. Two distinct wakeups land here:

  1. gate_enter succeeded immediately — we fall through from Arriving and call try_barber.
  2. gate_enter returned false (shop was full) — we set state to Trying_Barber and return. When gate_exit later wakes us, tick is called with state already at Trying_Barber.
  3. try_barber's sim.seize returned false — we're queued on the facility's waiters. When release hands us the barber, we wake in state Trying_Barber.

Path (3) is the one that bit us. The fix is the ownership check at gated_shop.odin:104-108:

case .Trying_Barber:
    if shop.barber.owner == &c.puck {
        begin_service(c, e)        // release() already gave us ownership
    } else {
        try_barber(c, e)            // gate admit — we still need to seize
    }

Without that branch, a release-woken customer would call try_barbersim.seize, which would see busy=true and re-queue the customer behind everyone else. Modeling guide §4.2 calls this out; this is the example that produced the rule.

This model uses sim.seize/sim.release, not the bypass pattern that the barbershop uses. Because the gate already provides admission control, we don't need a separate model-owned wait list for the barber — the Facility's built-in waiters are correct. The contrast with examples/barbershop/ is intentional and worth reading both back-to-back.

The gate wraps a Storage. gate_enter is enter with the "already-counted-while-waiting" semantics; gate_exit is leave. The wait set ordering (FIFO by default) means outside-waiters are admitted in arrival order, matching GATE SNF semantics in GPSS.

Alternatives considered

Use Storage directly instead of Gate

Gate is currently a thin wrapper around Storage with renamed operations. We could write the model against sim.enter/sim.leave and the result would be identical bytecode. We use Gate because the intent is "shop capacity," not "pool of N servers" — the naming carries the GPSS mapping. If Gate ever grows policy that Storage lacks (priority admission, multi-class capacity), the distinction will start paying off.

Two pucks — a "doorman" and a "customer"

One could model the gate logic as a separate doorman puck that admits customers from an outside queue. We don't, because sim.gate_enter already encodes that interaction correctly and splitting it adds two pucks per arrival with no behavioral difference. If the admission policy were richer (VIP queue jumps, group entry), a doorman puck would start to make sense.

Drop the trace printfs

The model emits a line per state transition. This is fine for a demo but would dwarf any real run. A future cleanup might gate these behind sim.trace_enabled (already wired but currently unused here) so the same model can be run quiet for benchmarking.

What this example teaches

This is the reference for:

CLI

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

./gated_shop           # default text run
./gated_shop --json    # uniform envelope

To vary capacity or distributions, edit the constants at the top of gated_shop.odin — those are not exposed as CLI flags.

Running it

odin run examples/gated_shop

Default output is verbose by design — one line per state transition for the 8-hour run, then a summary block.

See also