Gated Shop: model notes
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:
Customer(gated_shop.odin:67-149) — three active states (Arriving,Trying_Barber,In_Service).Arrivingtries the gate.Trying_Barberis re-entered from two wakeup paths (gate admit, facility release) and discriminates between them by checking ownership.Gen(gated_shop.odin:153-181) — a plain generator; no graceful-close logic since the model runs toend_time = 8 hoursand reports whatever finished.
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:
gate_entersucceeded immediately — we fall through fromArrivingand calltry_barber.gate_enterreturned false (shop was full) — we set state toTrying_Barberandreturn. Whengate_exitlater wakes us,tickis called with state already atTrying_Barber.try_barber'ssim.seizereturned false — we're queued on the facility's waiters. Whenreleasehands us the barber, we wake in stateTrying_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_barber
→ sim.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:
- GPSS
GATE SNF→sim.Gate - Composing a Gate around a Facility (admission + service)
- The release-time ownership-check pattern (
facility.owner == &c.puck) - Convergent state design: one state, multiple wake-up sources
- Using
sim.seize/sim.releaseinstead of the manual-bypass pattern (contrast withbarbershop)
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
- Modeling guide §4.4 —
Gateprimitive - Modeling guide §4.2 — the
releaseownership-check rule that this model surfaced barbershop— same arrival/service distributions, no gate, and uses the model-owned-wait-list bypass pattern instead ofseize/release. Worth reading both to see when each pattern fits.pruning-status.md, commit465e400— the Phase A commit where this model was ported and the ownership-check bug was discovered.