Broker Queues
The RabbitMQ/LavinMQ broker (see broker failover, Src/lib/queue.php) holds
12 queues — 4 logical destinations, each provisioned as a {name} / {name}-retry / {name}-error
triple. All 12 are pre-provisioned on the production broker; declareQueueAndDLX() only
declares/binds them when running against the test/CI broker.
The retry/error pattern (shared by all 4)
Every logical queue follows the same dead-letter cycle, built once in declareQueueAndDLX() and
driven by nackOrDeadLetter() (both Src/lib/queue.php):
- A consumer
nack()s a message it failed to process for a transient reason (e.g. a DB connection blip). RabbitMQ dead-letters it to{name}-retry. {name}-retryholds the message for a 10-minute TTL (x-message-ttl), then dead-letters it back to{name}for another attempt. Nothing ever consumes-retryqueues directly — the TTL is the only thing that moves messages out of them.- RabbitMQ’s own
x-deathheader tracks how many times a given message has cycled through{name}.getDeathCount()reads it; once it reaches 10,nackOrDeadLetter()callssendToErrorQueue()instead of nacking again, publishing the message to{name}-errorand acking it off the retry cycle for good. {name}-erroris terminal — nothing consumes it automatically. It exists for manual inspection and (if needed) manual replay.
A permanent failure (a duplicate delivery, a payload with no body) skips this cycle entirely —
the consumer just ack()s and drops the message, since retrying it would never produce a different
outcome.
The 4 logical queues
github / github-retry / github-error
- Producer:
Src/handlers/github.php, when the request’s source IP is inside theSrc/meta.jsonCIDR allow list. Publishes to thegithub-exchangefanout exchange, which is bound to bothgithubandgithub-database— one publish reaches both queues. Thehooksworker also republishes here (seegithub-hooksbelow) when replaying a previously-undelivered hook. - Consumer: the
servicedaemon worker (runServiceJob()→receiveQueue(..., "github", "processMessage", "github-exchange"),Src/services/consumer.github.php). For every message:saveGithubToDatabase()writes the raw event to thegithublog table and upserts itsgithub_statistics_6hbucket (see ADR-0004); thenhandleGitHub()(Src/lib/github.handler.php) composes and sends the outbound email/push Alert — it does not touch any entity table, despite the name. - This queue is output-facing: persist the raw log, notify a human. The entity-table dispatch lives
on
github-databasebelow, not here.
github-database / github-database-retry / github-database-error
- Producer: the same
github-exchangefanout publish asgithubabove — every valid GitHub delivery lands in both queues from a single publish, not two separatesendQueue()calls. - Consumer: the
database-servicedaemon worker (runDatabaseServiceJob()→receiveQueue(..., "github-database", "processDatabaseMessage", "github-exchange"),Src/services/consumer.github.database.php). CallssaveGithubValidatedToDatabase(), which:- unconditionally runs
ReleaseTrackingHandlerfirst, watchingrelease/workflow_runevents across every repository this service sees (not just GStraccini-bot’s) intogithub_releases/github_release_workflow_runs(see ADR-0006); - then, scoped to the GStraccini-bot GitHub App only (
isGitHubXToBot()/validateEventAndAction()), dispatches to the OOP pipeline (GitHubEventHandler→*Repositoryclasses, forcreate/delete,discussion,issues,pull_request) or the older proceduralsave*GitHubToDatabase()calls (check_run, comments, installation, etc.), writing into whichever typedgithub_*table the event maps to.
- unconditionally runs
- Kept as a separate queue/worker from
githubso a slow or failing entity-table write here can’t stall or drop the raw log + Alert ongithub, and vice versa.
github-hooks / github-hooks-retry / github-hooks-error
- Producer: the
hooksworker itself. Before it drains this queue each cycle,checkHooks()(Src/services/check.hooks.github.php) polls the GStraccini-bot GitHub App’s/app/hook/deliveriesAPI for deliveries that didn’t get a202the first time, and publishes one message per undelivered hook ontogithub-hooks. - Consumer: the
hooksdaemon worker (runHooksJob()→receiveQueue(..., "github-hooks", "processMessageHook"),Src/services/consumer.github.php).retryGitHubHook()re-fetches the original request (headers + payload) for that hook delivery from GitHub’s API and republishes it onto thegithubqueue — i.e. this queue’s job is to feedgithub, not to process events itself. - This is the self-healing loop for deliveries that arrived while this app (or the broker) was down or erroring: GitHub already recorded them as failed deliveries, and this queue is what re-injects them.
github-forbidden / github-forbidden-retry / github-forbidden-error
- Producer:
Src/handlers/github.php, when the request’s source IP is not in theSrc/meta.jsonCIDR allow list (getQueueName()returnsgithub-forbiddeninstead ofgithub). Published to the default exchange directly — not throughgithub-exchange, since this path is meant for one queue only. - Consumer: the
cleanupworker, once per run (runCleanupJob()→receiveQueue(60 * 1, "github-forbidden", "processMessageForbidden"),Src/services/consumer.github.php). Runs the samehandleGitHub()pipeline as thegithubqueue, just tagged"Forbidden"— the event is still processed, this queue only exists to keep IP-disallowed traffic out of the primarygithubqueue’s throughput/latency budget.
Related decisions
- Architecture — how this queue traffic fits into the app’s four overall data flows.
- ADR-0004 —
github_statistics_6hbucketed statistics - ADR-0006 — Release + workflow tracking for
projects-monitor - ADR-0008 — Fan out GitHub events via
github-exchange— whygithubandgithub-databaseare two independent queues fed by one publish. - ADR-0009 — Queue retry cycle via RabbitMQ TTL/DLX — why the retry/error mechanism is built on broker-native TTL + dead-lettering rather than app-managed state.
- CONTEXT.md — glossary entries for Broker, Queue, Exchange, Retry Cycle, and Queue Message.
- Full ADR log