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.
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.
One puck type:
Customer (gated_shop.odin:67-149) — three
active states (Arriving, Trying_Barber, In_Service).
Arriving tries the gate. Trying_Barber is 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 to
end_time = 8 hours and 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.
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_enter succeeded immediately — we fall through from
Arriving and call try_barber.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.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_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.
Storage directly instead of GateGate 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.
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.
printfsThe 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.
This is the reference for:
GATE SNF → sim.Gatefacility.owner == &c.puck)sim.seize/sim.release instead of the
manual-bypass pattern (contrast with barbershop)./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.
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.
Gate primitiverelease ownership-check rule that
this model surfacedbarbershop — same arrival/service
distributions, no gate, and uses the model-owned-wait-list
bypass pattern instead of seize/release. Worth reading both
to see when each pattern fits.pruning-status.md, commit
465e400 — the Phase A commit where this model was ported and
the ownership-check bug was discovered.