GPSS Example #2: two categories of mechanics share one tool clerk.
Category 2 has higher priority than Category 1 and should see
shorter waits despite arriving slightly more often. The reference
model for priority queueing (first non-FIFO Set use) and for
the dispatcher helper pattern.
Two independent Poisson-ish arrival streams feed a single clerk. Each mechanic belongs to a category that determines both its service-time distribution and its priority:
| Class | Inter-arrival (s) | Service (s) | GPSS priority |
|---|---|---|---|
| Cat 1 | Uniform(60..780) | Uniform(210..390) | 1 |
| Cat 2 | Uniform(120..600) | Uniform(70..130) | 2 |
Run for 8 hours (28,800 seconds). Expected: Cat 2's average wait is substantially lower than Cat 1's even though Cat 2 arrives slightly more often, because high-priority mechanics jump the queue.
GPSS reference: docs/gpss-examples.md
example #2. The original GPSS source is transcribed at the top of
tool_crib.odin.
Three puck types share a ^Crib pointer:
Mechanic (tool_crib.odin:121-182) — the
canonical four-state machine, identical in shape to the
barbershop's customer but with a category field and
per-category stats branching.Gen (tool_crib.odin:210-230) — the
generator. Two instances are added to the engine, one per
category, each with its own IA distribution, category tag, and
priority.dispatch (tool_crib.odin:184-194) — an
inline helper (not yet a reusable primitive) that pops the
highest-priority waiter off the wait list and hands it the
clerk. Called from two places: after a mechanic joins the wait
list, and after service completes.Crib (tool_crib.odin:65-88) holds:
clerk: sim.Facilitywait_list: sim.Set created with .Priority ordersvc_1, svc_2)Wait list is ordered by -priority. sim.Set sorts ascending
by rank, and GPSS priority convention is "higher number = higher
priority." The model reconciles this by placing at rank
-priority:
sim.set_place(&crib.wait_list, &m.puck, f64(-m.priority))
So Cat 2 (rank -2) sorts before Cat 1 (rank -1). Within a
priority class, insertion order is preserved (the Set's tie-break
is FIFO on insertion), giving GPSS-faithful "priority then FIFO"
semantics without any extra code.
A dispatch helper, called in two places. Unlike the barbershop
— where the state machine alone handles hand-off — priority
queueing means a newly arrived Cat 2 mechanic may need to jump
ahead of mechanics already suspended in the wait list. The state
machine can't handle that alone: the new arrival is in Arriving
state, and the queued mechanics are in Waiting state sleeping on
eng.suspend. So after every wait-list mutation or service
completion, dispatch runs: if the clerk is free and the wait list
non-empty, it pops the head, seizes the clerk on that head's
behalf, and eng.reactivates it. The woken mechanic already owns
the clerk and goes straight to service.
This is the first appearance of what pruning-status.md calls the
dispatcher pattern. It recurs in machine_inspector and
threshold_routing, and is the lead candidate for extraction into
sim.dispatch (see sim/dispatch.odin — a more general typed
version is already there for multi-facility cases; this one-facility
inline form is simple enough to stay inline).
Two generators, not one. Each category has independent arrival
timing, a separate priority value, and a different service stream,
so modeling them as two independent Gen pucks is the natural
shape. Shared code (mechanic creation, engine injection) lives in
gen_tick, which is parameterized on the generator's own fields
(category, priority).
Priority is stored on the mechanic, not re-derived at dispatch.
dispatch does not re-inspect category when choosing the next
mechanic — the priority ordering was established at set_place
time. This keeps the policy in one place (the rank passed in) and
makes it cheap to add more classes without touching the dispatcher.
Wait time and service time are measured at different points.
Wait-time stats are updated when the mechanic wakes in .Waiting
(i.e., the moment it starts service), not when it enters
.In_Service processing. This matches GPSS QUEUE/DEPART
semantics. See modeling guide §9.
sim.seize / the Facility's internal waiterssim.Facility has its own waiter Set that can be configured
.Priority. We could throw away crib.wait_list and let
sim.seize(&clerk, &m.puck, rank = -priority) handle ordering.
The reason we don't: the dispatch logic needs to walk the wait list
after any arrival (to check for priority inversions) and after any
release (to pick the next winner). Both operations are easier on a
model-owned Set that the code can set_take_first from
explicitly. Using the Facility's internal waiters would require
poking into its private state, which is exactly the coupling the
primitive is designed to hide. For a single-class FIFO shop
(barbershop-style) the tradeoff goes the other way — see the
barbershop README for the mirror case.
A Cat 1 queue and a Cat 2 queue, with dispatch picking Cat 2 first
whenever non-empty. This works and is what a GPSS modeler might
write by hand. It doesn't scale: a third priority class means a
third queue and a new dispatch clause. The single priority-ordered
Set scales with a new rank value and zero new code, which is
why it's the canonical shape here.
dispatch inside set_place rather than afterWe could tuck the dispatch call inside a wrapper (crib_enqueue)
so every enqueue atomically tries to hand the clerk to whoever is
now at the head. This is cleaner but hides the two call sites that
matter — arrival and service-completion — behind the same name.
Keeping dispatch explicit documents where priority inversions can
occur.
sim.dispatch from sim/dispatch.odinThe existing sim.dispatch is shaped for the multi-facility
routing case (threshold_routing uses it: pick the first eligible
facility for each waiter). Here there is one facility, so the
inline 10-line helper is simpler than configuring a single-target
Dispatch_Target array. When/if these two shapes converge into a
common helper, this is one of the call sites that will fold in.
This is the reference for:
sim.Set with .Priority order and
negative-rank placement to flip ascending→descending./tool_crib [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:
./tool_crib # default text run
./tool_crib --json # uniform envelope
To vary priorities, distributions, or run length, edit the constants
at the top of tool_crib.odin — those are not exposed as CLI flags.
odin run examples/tool_crib
Default output is a console table comparing per-category arrivals, serves, wait, and sojourn, plus a combined queue length summary.
Set primitive and order policiesbarbershop — single-class FIFO
analog; contrast the bypass pattern with the dispatcher herethreshold_routing — uses the
generic sim.dispatch for multi-facility routing; the shape to
which this model's inline dispatcher may eventually generalizemachine_inspector — same
dispatcher pattern applied to probabilistic routingpruning-status.md, commit
ddb64cb — the Phase B commit that ported this example