Skip to content

Internals

The shape of the system in one page: an axum router, a connection pool, the fusion layer, the log engine in PostgreSQL, the background jobs, and the mesh.

Updated View as Markdown

Queen is one stateless Rust binary (crate queen, binary queen) and one PostgreSQL database. The binary holds no durable state. Every offset, cursor, lease and segment lives in PostgreSQL, and every in-memory structure in the broker is a cache or a scheduler over it. That single fact is the key to reading this section: when an in-memory structure is stale, wrong, or lost to a restart, the outcome is extra work, not lost or misdelivered messages. Each page below states its own version of that argument, because each structure earns its place only by having one. The one deliberate exception is the ephemeral queue class, whose contents live in broker memory by contract: only the declared configuration is a PostgreSQL row, its hot verbs take neither an admission slot nor a pooled connection, and losing its contents on a restart is that class’s documented trade, not a defect. Everything else in this section is state PostgreSQL owns.

Read the concept pages first if you want to know what Queen promises. This section explains how it keeps those promises, and where it pays for them.

The request path

An HTTP request crosses the same five layers regardless of what it does.

  1. The axum router in server/src/main.rs. Eighty-four method-and-path pairs on one flat Router, plus a fallback that serves the dashboard SPA compiled into the binary with rust_embed from server/webapp/dist. There is no runtime static-directory override. There is no TLS listener and no CORS layer: the broker speaks plaintext HTTP on PORT (6632) and expects a proxy in front of it.
  2. Middleware. JWT validation (off by default) resolves an access level and an authenticated sub; tenant resolution reads x-queen-tenant when QUEEN_TENANCY_HEADER=1, otherwise stamps the fixed default tenant. Both run before any handler and both are transparent when disabled.
  3. An admission slot. Every write transaction passes through one process-wide arbiter that sizes itself from PostgreSQL’s commit-flush pipeline, with four lanes (push, pop, ack, maint) sharing the budget. See Flow control.
  4. A pooled connection. One deadpool-postgres pool, DB_POOL_SIZE connections (default 160), shared by handlers and background jobs. Hot statements go through prepare_cached, so a steady-state call re-uses a prepared statement on that connection.
  5. One stored-procedure call. Almost every route is a single SELECT queen.<sp>(...) whose JSON result the handler forwards, sometimes verbatim. The decision procedures (who gets which message, whether a push is a duplicate, what an ack commits) live in SQL, under row locks, not in Rust.

A pop that has to wait is the one exception worth knowing: it releases both the permit and the connection before it parks, and re-acquires them on wake. A parked long-poll costs the broker a timer and nothing else.

What lives in memory

Nothing in this table is durable. Every entry is followed by what its being wrong costs.

Structure Module Replaces Cost of being wrong
Push fusion shards fusion.rs one transaction per push request the bundle fails; participants are told error and spooled to disk
Hot-list rings hotlist.rs the SQL wildcard candidate scan one empty SKIP LOCKED claim that returns no rows
Dedup cache dedup.rs nothing: it only narrows the SQL probe the probe widens to the full window
Ack registry ack_registry.rs per-ack hash resolution the ack falls through to the SQL path
Ack fusion buffers ack_fusion.rs one transaction per ack the whole flush errors; leases expire and redeliver
Long-poll gates and hints notify.rs polling the parked pop waits out its own backoff
Disk spool file_buffer.rs losing pushes during a database outage pushes fail outright

The two things that decide what data exists (the deduplication verdict and the consumer cursor) are not in this table. They are in PostgreSQL, computed under a row lock, on every call.

The storage side

Storage is the “log engine”: four tables created by 001_log_schema.sql (log_partitions, log_segments, log_txns, log_consumers) plus queen.log_dlq from the ack file, all hanging off queen.queues, which is both a queue’s identity and its configuration (log_partitions.queue_id references it directly, ON DELETE CASCADE). A partition is an ordered lane; a message’s position is a single monotone BIGINT offset; a segment is one row holding many messages packed together and zstd-compressed. The storage model has the row-by-row detail.

Two further tables hang off nothing at all. queen.kv and queen.log_timers are created at every boot like the rest, and stay empty unless an operator switches their surfaces on. Neither names a partition, deliberately, which is what keeps them out of the per-partition maintenance scan; see KV internals and timer internals. The ephemeral class keeps two more (queen.ephemeral_queues, queen.ephemeral_quota): declared configuration and quota rows only, never message contents.

All of that DDL is include_str!-embedded in the binary and re-applied idempotently at every boot, under an advisory lock and with no migrations, because the deployment model is always-virgin; the schema catalogue has the apply order and its rules.

Background jobs

Several loops run alongside the HTTP server. Each is spawned before axum::serve, each swallows its own errors so a transient database failure cannot kill it. The cluster-singleton loops schedule through one durable claim row each in queen.maintenance_leases, so their cadence is the configured value whatever the replica count; the advisory locks stay inside those cycles as belt against old-image pods.

Loop Cadence Leader gate What it does
Retention RETENTION_INTERVAL, 5000 ms claim row retention + transaction lock 737001 as belt bounded autocommitting delete steps (Retention)
Stats reconciler STATS_INTERVAL_MS, 10000 ms claim row stats_refresh + transaction lock 737002 as belt recomputes queen.stats from the log tables (Stats)
Retained-bytes lane RETAINED_BYTES_INTERVAL_MS, 600000 ms claim row retained_bytes + transaction lock 737003 as belt recomputes queen.stats.retained_bytes from live segments (Stats)
Metrics collector METRICS_FLUSH_MS, 60000 ms none, by design per-replica rows in worker_metrics and system_metrics
Hot-list wheel tick 50 ms none promotes due deferred and lease-parked ring entries
Hot-list wake tick 5 ms none one coalesced wake per queue that received pushes
Hot-list reseed floor 2 s tick, per ring QUEEN_HOTLIST_RESEED_MS none re-derives ring contents from PostgreSQL, bounded to recently written partitions
Hot-list full reseed per ring, QUEEN_HOTLIST_RESEED_FULL_MS, 300000 ms none the same over every partition of the queue, the floor for pendingness no write explains
Reconcile QUEEN_CACHE_REFRESH_INTERVAL_MS, 60000 ms none re-reads maintenance flags, drops per-queue caches, applies the hot-list repair markers a seek or a group delete published
Idle sweep QUEEN_HOTLIST_IDLE_SWEEP_MS, 300000 ms none drops rings and wake gates for queues nobody touched
Unserved-ring trim QUEEN_HOTLIST_UNSERVED_TRIM_MS, 30000 ms none drops rings this broker serves no pops for, the standby-broker bound on ring memory
Spool drain FILE_BUFFER_FLUSH_MS, 100 ms none replays disk-spooled pushes once the database is reachable
Sweeper due-driven, 5 ms to 1000 ms none, by design fires due timers into the log and prunes expired KV rows (the sweeper)

The metrics collector is deliberately un-gated: every replica records its own rows, keyed by hostname and worker id, and the dashboard aggregates across them.

The sweeper is un-gated for a stronger reason, and it is the opposite of retention’s. Any ownership scheme orphans the work of a broker that dies, and an orphaned timer never fires, which is the worst failure this component can have. So every replica drains in parallel and the sharing mechanism is FOR UPDATE ... SKIP LOCKED rather than an election. It is spawned on every replica, because the two surfaces it serves are on every replica; QUEEN_SWEEPER=false is the only thing that stops it.

The mesh

With QUEEN_MESH_PEERS set, replicas form a full mesh over framed TCP. It carries wakes for parked pops when a push commits on another replica, hot-list pending marks, maintenance-mode flips, queue-config cache invalidation, and the admin fan-out for ephemeral queues (whose data verbs travel broker-to-broker over plain HTTP instead, to the owner the HELLO handshake advertised). Nothing it carries is authoritative: PostgreSQL is re-read every QUEEN_CACHE_REFRESH_INTERVAL_MS and a parked pop re-polls on its own backoff, so a dropped frame costs latency only. It also has a security boundary you must enforce yourself: see The mesh.

Working on Queen

The four pages under Contributing are the same tier pointed at the repository rather than at the running system. They are the answer to “where does this claim come from”: the workspace and the build order, the suites each page names in its verifiedBy frontmatter, the generators that derive this site’s fact tables from server/, and what a release actually publishes.

Where to go next

The storage model

Segments, offsets, the log tables and the queue row they hang off: what each row is, and the invariants that make offset arithmetic safe.

Life of a push

From HTTP body to committed rows: grouping, intra-request dedup, and how N partitions cost one commit.

Life of a pop

Candidate selection, the claim under FOR UPDATE SKIP LOCKED, the head probe, and lease versus auto-ack.

The hot-list

The in-memory ring that replaced the SQL candidate scan: tri-state verdicts, epoch-CAS clears, and the revisit wheel.

Deduplication

How exactness is achieved in SQL, and why the broker-side cache can never change a verdict.

Ack internals

The ack registry, ack fusion, and how a transactionId resolves to an offset through the hash sidecar.

Flow control

The adaptive concurrency limiter: what it measures, how the limit moves, and why raising the maximum does nothing.

Retention internals

Bounded autocommitting steps, one partition lock at a time, and the three eviction watermarks.

Counters, rates and lag

Where every number on the dashboard comes from, and why the manual refresh route does nothing useful.

The mesh

Framed TCP, the HMAC HELLO handshake, the frame set, and the security reality.

Timer internals

Why a scheduled message cannot live in the log, the staging table keyed by names, and the two lease columns.

KV internals

The identity with no partition id, a version that is not monotonic, and one predicate as the only definition of existence.

The sweeper

One component with two clocks, leaderless, and the livelock a claim-then-fire loop walks into.

Schema and procedure catalogue

Every log table with its columns, and every log-engine stored procedure with its signature and role.

Retired objects

The engines that came before, and why nothing of them survives: deployments start from an empty database.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close