Print Queue Orchestration: The Badge-Job Acceptance & Ordering Layer

Print queue orchestration is the component that decides, job by job, which physical printer prints which badge and in what order, and it is one of the four stages that make up the On-Site Check-In & Print Routing section. Its scope is narrow and unforgiving: accept a rendered badge as a print job, give it a stable identity, place it on exactly one printer’s queue at the right priority, and refuse to let any single device’s backlog grow past the point where the scan-to-print budget collapses. Everything the floor promises — sub-second median latency, exactly-once printing, no idle printer next to a buried one — is either kept or lost here. This is the layer that a check-in scan hits after it has been turned into a trusted presence event, and it is the layer that feeds the spooler which drives the thermal head.

The reason to isolate this component is operational: queue behavior is where load actually bites, and it must be reasoned about independently of how scans are captured or how a dead printer is drained. This page defines the acceptance contract precisely — the PrintJob shape, the ordering and backpressure rules, the per-printer depth accounting, and the spooler-health signals the dispatcher reads — plus the structured observability an on-call engineer needs to clear a jammed queue in under fifteen minutes. Capturing and reconciling the scan itself belongs to Check-In Kiosk Sync; re-homing work off a failed device belongs to On-Site Print Failover; reissuing a badge on purpose belongs to Badge Reprint Handling. Orchestration owns only the queue.

The print-queue dispatcher: an accepted job passes an idempotency gate and priority ordering into a per-printer queue, then to the spooler An accepted PrintJob flows left to right through three stages. Stage one, the idempotency gate, claims a dedupe key with SET NX so a repeated scan enqueues only once; a duplicate drops along a dashed branch that returns an acknowledgment without enqueuing. Stage two, priority ordering, writes the job into a per-printer sorted set with a score derived from its priority and arrival time. Stage three is the per-printer queue, which tracks its depth against a high-water mark; a queue over its ceiling raises backpressure and sheds or redirects the job along a second dashed branch to a shed-and-dead-letter path. Jobs that pass all three stages are handed to the spooler, which drives the printer. enqueue · admit duplicate · backpressure shed PrintJob unique key scored member ready job Accepted job from check-in scan Idempotency gate SET NX · dedupe key duplicate → drop Priority ordering ZADD score priority + arrival Per-printer queue depth vs high-water over ceiling → shed Spooler → printer ZPOPMIN · print duplicate ack no second sheet shed → dead-letter rebalance / failover

Scope Boundary Link to this section

Orchestration stays fast and testable only because its job is bounded. Anything not in the left column is delegated to an adjacent stage and must never leak into the enqueue hot path.

In-Scope (this component owns) Out-of-Scope (delegated to adjacent stages)
Accepting a PrintJob and assigning it a queue position Turning a raw scan into a trusted check-in event — owned by Check-In Kiosk Sync
Ordering jobs by priority within a per-printer queue Rendering the badge PDF the job references — owned by PDF routing workflows
Enforcing per-printer queue depth and backpressure Detecting a dead device and draining its queue — owned by On-Site Print Failover
Structural dedupe so one scan enqueues exactly once Deliberately reissuing a badge with a reason code — owned by Badge Reprint Handling
Reading spooler health to pick a routing target Guaranteeing only paid records are printable — owned by async batch processing
Emitting correlation IDs and per-queue depth metrics Focused recovery runbooks — see clearing a stuck queue at peak check-in and load-balancing jobs across printers

The orchestrator holds no attendee state and renders nothing. It receives an accepted job and either places it on exactly one queue or diverts it — as a suppressed duplicate or a shed-under-backpressure job — with a structured reason, which is what lets a queue decision be replayed and audited after the rush is over.

Data Contract & Schema Link to this section

Every job crossing into a queue must satisfy a rigid contract. The PrintJob model below is enforced with Pydantic v2, coercion disabled, so a malformed job is rejected at acceptance rather than discovered when the spooler chokes on it.

PYTHON
from pydantic import BaseModel, ConfigDict, Field, field_validator

PRINTER_TARGET_RE = r"^printer-[0-9]{2,3}$"


class PrintJob(BaseModel):
    """The unit accepted by the print-queue dispatcher on the event floor."""

    model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)

    attendee_id: str = Field(min_length=4, max_length=48)
    badge_pdf_ref: str = Field(min_length=1, max_length=512)
    printer_target: str = Field(pattern=PRINTER_TARGET_RE)
    priority: int = Field(default=100, ge=0, le=1000)
    idempotency_key: str = Field(min_length=8, max_length=80)

    @field_validator("badge_pdf_ref", mode="before")
    @classmethod
    def require_pdf(cls, v: str) -> str:
        # The floor prints rendered artifacts only; anything else is a bug upstream.
        if not str(v).lower().endswith(".pdf"):
            raise ValueError("badge_pdf_ref must reference a rendered .pdf artifact")
        return v

Each field earns its place. attendee_id is the correlation key threaded through every log line and the dead-letter envelope, bounded to a stable alphanumeric shape and never defaulted. badge_pdf_ref points at the already-rendered artifact from the upstream routing stage; the mode="before" validator runs prior to type checking and rejects anything that is not a PDF, closing the door on a half-formed job reaching a printer. printer_target is pinned to a closed pattern because it selects a specific physical device — an unknown target must fail loudly, not silently pick a default. priority is a bounded integer where a lower number prints sooner, which lets a VIP or a re-drive jump the line deterministically. idempotency_key is the structural guarantee against double-printing: it is derived from the check-in scan upstream and is the exact value the dedupe gate claims, so one scan can occupy a queue exactly once. extra="forbid" ensures an upstream shape change adds a stray field here rather than being silently carried into the spooler.

Deterministic Implementation Link to this section

The dispatcher is Redis-backed for a reason: one printer maps to one sorted set, ordering is the set’s score, depth is its cardinality, and the whole thing survives a dispatcher restart because state lives in Redis, not in process memory. Acceptance is a fixed sequence — check depth for backpressure, claim the idempotency key, then write the scored member — and the spooler pops the lowest score to print next. Every decision emits a structured line keyed by a correlation ID so a job can be traced from acceptance to the thermal head.

PYTHON
from __future__ import annotations

import json
import logging
import time
from typing import Optional

import redis
from pydantic import BaseModel, ConfigDict, Field, field_validator, ValidationError

logger = logging.getLogger("print_queue")

PRINTER_TARGET_RE = r"^printer-[0-9]{2,3}$"


class PrintJob(BaseModel):
    model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)

    attendee_id: str = Field(min_length=4, max_length=48)
    badge_pdf_ref: str = Field(min_length=1, max_length=512)
    printer_target: str = Field(pattern=PRINTER_TARGET_RE)
    priority: int = Field(default=100, ge=0, le=1000)
    idempotency_key: str = Field(min_length=8, max_length=80)

    @field_validator("badge_pdf_ref", mode="before")
    @classmethod
    def require_pdf(cls, v: str) -> str:
        if not str(v).lower().endswith(".pdf"):
            raise ValueError("badge_pdf_ref must reference a rendered .pdf artifact")
        return v


class QueueBackpressure(Exception):
    """Raised when a printer's queue depth is at or above its high-water mark."""


class RedisPrintQueueDispatcher:
    """One sorted set per printer: score orders, cardinality is depth."""

    HIGH_WATER = 200        # per-printer queue-depth ceiling
    DEDUPE_TTL = 21_600     # 6h — covers a full check-in day

    def __init__(self, client: "redis.Redis") -> None:
        self.r = client

    def _queue_key(self, printer_target: str) -> str:
        return f"printq:{printer_target}"

    def _dedupe_key(self, idempotency_key: str) -> str:
        return f"printq:dedupe:{idempotency_key}"

    def accept(self, job: PrintJob, correlation_id: str) -> bool:
        """Enqueue a job; return False if it was a suppressed duplicate."""
        key = self._queue_key(job.printer_target)
        depth = self.r.zcard(key)
        if depth >= self.HIGH_WATER:
            logger.warning(json.dumps({
                "event": "queue_backpressure", "printer_target": job.printer_target,
                "depth": depth, "correlation_id": correlation_id,
                "idempotency_key": job.idempotency_key,
            }))
            raise QueueBackpressure(job.printer_target)

        # Idempotency: one badge per key, even under scanner double-taps.
        if not self.r.set(self._dedupe_key(job.idempotency_key), correlation_id,
                          nx=True, ex=self.DEDUPE_TTL):
            logger.info(json.dumps({
                "event": "duplicate_suppressed", "idempotency_key": job.idempotency_key,
                "correlation_id": correlation_id,
            }))
            return False

        # Lower priority number prints sooner; the fractional arrival time
        # breaks ties in FIFO order within a priority band.
        score = job.priority + (time.time() % 1)
        self.r.zadd(key, {job.model_dump_json(): score})
        logger.info(json.dumps({
            "event": "job_enqueued", "printer_target": job.printer_target,
            "priority": job.priority, "depth": depth + 1,
            "correlation_id": correlation_id, "idempotency_key": job.idempotency_key,
        }))
        return True

    def next_job(self, printer_target: str) -> Optional[PrintJob]:
        """Spooler pull: pop the lowest-score (highest-priority) job."""
        popped = self.r.zpopmin(self._queue_key(printer_target), 1)
        if not popped:
            return None
        raw_member, _score = popped[0]
        return PrintJob.model_validate_json(raw_member)


if __name__ == "__main__":
    # Verification runs without a live Redis: it proves the contract, not the wire.
    job = PrintJob.model_validate({
        "attendee_id": "att_44817",
        "badge_pdf_ref": "s3://badges/att_44817.pdf",
        "printer_target": "printer-02",
        "priority": 5,
        "idempotency_key": "scan-att_44817-88231",
    })
    assert job.printer_target == "printer-02"
    # The JSON round-trip is what a sorted-set member relies on.
    assert PrintJob.model_validate_json(job.model_dump_json()).idempotency_key == job.idempotency_key

    # A non-PDF artifact is rejected before it can ever reach a printer queue.
    try:
        PrintJob.model_validate({
            "attendee_id": "att_00001",
            "badge_pdf_ref": "s3://badges/att_00001.png",
            "printer_target": "printer-01",
            "idempotency_key": "scan-att_00001-00001",
        })
        raise AssertionError("expected a validation error for a non-pdf ref")
    except ValidationError:
        pass
    print("OK:", job.idempotency_key, "priority", job.priority)

Three properties make this safe under floor concurrency. First, the depth check and the dedupe claim happen before the write, so a backpressured printer never accepts and a duplicate never enqueues. Second, the sorted-set score is priority + fractional_arrival, giving a total order that is priority-first and FIFO within a band, so a re-drive at priority=0 jumps ahead deterministically instead of racing. Third, the queue member is the job’s own JSON, so next_job re-validates on the way out — a corrupted member fails at the spooler boundary rather than printing garbage.

Production Debugging & Observability Link to this section

Because acceptance decisions are made and forgotten in milliseconds, the log line is the only forensic record. Every path emits a structured JSON event keyed by correlation_id, which ties back to the originating check-in scan and forward to the print receipt. An enqueued job logs job_enqueued at info with the live depth; a suppressed retry logs duplicate_suppressed; a printer over its ceiling logs queue_backpressure at warning immediately before the dispatcher sheds the job.

JSON
{
  "event": "queue_backpressure",
  "printer_target": "printer-02",
  "depth": 200,
  "correlation_id": "scan-7f3a9c2e",
  "idempotency_key": "scan-att_44817-88231"
}

In a Datadog or ELK pipeline, index these fields by name: correlation_id as the cross-stage trace facet, event as the monitored dimension (a rising queue_backpressure rate means one printer is falling behind the floor), printer_target to attribute the pressure to a specific device, depth as the gauge that drives the backpressure alert, and idempotency_key to prove a duplicate was suppressed rather than double-printed. Jobs shed under sustained backpressure are wrapped with their reason and routed to a dead-letter list for controlled recovery — never dropped — so they can be rebalanced or handed to failover. A saturation alert on depth crossing the high-water mark on any single printer while peers sit low is the earliest signal that load is skewed and a rebalance is due, the exact condition worked in load-balancing jobs across printers.

Performance & Memory Constraints Link to this section

The dispatcher is I/O-bound on Redis, not CPU-bound; the failure modes are queue-depth blow-out and memory growth in the sorted sets, not compute.

Component Constraint Mitigation
Per-printer sorted set Unbounded depth blows the scan-to-print budget for every job behind it Enforce HIGH_WATER at acceptance; raise QueueBackpressure and shed or rebalance rather than grow
Redis memory Each queued member holds the full job JSON; a stuck queue inflates memory Keep members lean (a badge_pdf_ref, not the PDF); alert on used_memory; drain stuck queues promptly
Dedupe key space One SET NX key per scan for the whole day accumulates Bound with a DEDUPE_TTL that just covers the event; let Redis auto-evict on expiry
Spooler pull rate ZPOPMIN throughput caps how fast a printer drains its band Pull in small batches per printer; scale printers horizontally, not the queue depth
Structured log volume A duplicate_suppressed storm during a retry loop inflates ingestion Sample repeated identical suppressions; keep one exemplar per (printer_target, key) window

The one most often gotten wrong is treating depth as cosmetic. A queue that grows without a ceiling does not fail loudly — it silently converts a one-second SLA into a ten-second one, and the line backs up before any alarm fires. The high-water mark is what turns that slow failure into a fast, visible one.

Incident Triage Checklist Link to this section

When a printer’s queue stalls or the backpressure alarm fires, work these steps in order. Target MTTR is under fifteen minutes; every step before the last is non-destructive.

  1. Confirm the symptom class. Check which printer is pressured: redis-cli --scan --pattern "printq:printer-*" | while read k; do echo "$k $(redis-cli zcard "$k")"; done. One queue far above its peers is skew; every queue climbing is a floor-wide throughput problem.
  2. Peek the stuck head. redis-cli ZRANGE printq:printer-02 0 0 WITHSCORES shows the lowest-score job and its priority band; a wedged head at an unexpected score usually means a poisoned member or a spooler that stopped pulling.
  3. Check spooler liveness. Confirm the printer is actually draining: compare zcard now versus thirty seconds ago. A flat, non-zero depth with no pops is a zombie printer — hand it to On-Site Print Failover rather than clearing the queue.
  4. Rebalance, don’t flush. If the device is healthy but skewed, drain the excess to a peer: pop from the hot queue and re-accept with the peer as printer_target, preserving the idempotency_key. The full procedure is in clearing a stuck queue at peak check-in.
  5. Validate recovery. Confirm depth returns below the high-water mark and the queue_backpressure rate falls to zero: redis-cli ZCARD printq:printer-02. Only then re-enable normal routing to that target.

Frequently Asked Questions Link to this section

Why order with a sorted set instead of a plain Redis list? Because a list gives you FIFO and nothing else, and the floor needs priority. A re-driven badge, a VIP, or a failover job must be able to jump ahead of routine traffic deterministically. Encoding priority + fractional_arrival as a sorted-set score gives a total order that is priority-first and FIFO within each band, and ZPOPMIN pulls the next job in that order in one round trip — a plain list would force a separate priority structure and a race between them.

Where exactly does the double-print get stopped? At the idempotency gate, before the write. The dispatcher claims printq:dedupe:{idempotency_key} with SET NX, and if the key already exists it returns without enqueuing. Since the key is derived from the check-in scan upstream, a double-tapped barcode or a retried dispatch resolves to the same key and the second attempt is suppressed. A genuine reprint is a different, logged action handled by Badge Reprint Handling, never an accidental duplicate here.

What happens to a job shed under backpressure? It is not dropped. When a queue is at its high-water mark the dispatcher raises QueueBackpressure and the job is routed to a dead-letter list with its reason and correlation ID, from which it is rebalanced to a healthier printer or handed to failover. Backpressure is a redirect signal, not a discard — the SLA is protected by moving the job, never by losing it.


Up: On-Site Check-In & Print Routing — the section this queue-orchestration stage sits inside.