← QueueSim home  ·  All models

Tool Crib (Priority Classes) — README

Tool Crib

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.

Problem

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.

Model in this directory

Three puck types share a ^Crib pointer:

Crib (tool_crib.odin:65-88) holds:

Why this shape

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.

Alternatives considered

Use sim.seize / the Facility's internal waiters

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

Two separate wait lists (one per category)

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.

Call dispatch inside set_place rather than after

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

Use the generic sim.dispatch from sim/dispatch.odin

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

What this example teaches

This is the reference for:

CLI

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

Running it

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.

See also