Runnable wiring for the Stovepipe domain — a single-service domain (the domain is the service). The server exposes four RPCs and runs the internal pipeline stages as queue consumers:
Ping— health check.Ingest— resolves a queue's head commit, persists aRequest(and its head URI) to storage, and publishes the request to the process stage.GetRequestHistoryByID— returns the retained request log for one request ID.GetRequestHistoryByURI— returns retained histories selected by an exact commit URI.- process consumer (
TopicKeyProcess) — reloads the persistedRequestfrom storage and runs the process stage (stovepipe/controller/process). - build consumer (
TopicKeyBuild) — reloads the persistedRequestand triggers the build-runner, then publishes tobuildsignal. - buildsignal consumer (
TopicKeyBuildSignal) — polls/records the build's terminal status and releases the queue's in-flight slot, then publishes torecord. - record consumer (
TopicKeyRecord) — writes the whole-repository validation fact and, for a green fact, advances the queue's last-green bookmark and promotion ref. - hook consumer (
TopicKeyHook) — receivesvalidation.repository.startedfrom process andvalidation.repository.recordedorvalidation.repository.cancelledfrom record. The current resolver invokes onlynoop, so these events have no external side effect. Its topic name is domain-qualified (stovepipe-hook) because the key is shared across domains. - DLQ consumers — registered for process, build, buildsignal, record, and hook, with stage-specific reconciliation behavior.
The ingest → process → build → buildsignal → record flow stays inside one service and one store, so messages carry thin identifiers: request IDs on process, build, and record; a build ID on buildsignal. Consumers reload the full entity from storage, which keeps messages small and redelivery idempotent. The topic keys and internal wire contract are owned by the domain under stovepipe/core/messagequeue/.
Stovepipe therefore needs two MySQL databases: a storage database (the queue, request, request_uri, request_log, build, and validation_fact tables) and a queue database (messaging infrastructure).
server/main.go is the composition root and supplies the concrete extension implementations. Two are deliberately demo-only and must be replaced for any real deployment:
inMemoryCounter— a process-localcounter.Counterfor sequence numbers; not durable. A real deployment uses a persistent implementation (e.g.platform/extension/counter/mysql).fakeSourceControlFactory— seeds each queue with a deterministic single-commit history so ingest resolves a stable head URI (and re-ingesting the same queue exercises the dedup path). A real deployment supplies a VCS-backedsourcecontrol.Factory, which is also where a queue's promotion ref is resolved. The fake has no ref to move, so a promotion locally shows up only in the record consumer's logs.
stovepipe/
├── docker-compose.yml # Stovepipe service + storage MySQL + queue MySQL
├── docker-compose.debug.yml # Debug variant with delve
├── server/
│ ├── main.go # gRPC server (Ping, Ingest) + pipeline consumer wiring
│ └── Dockerfile
└── client/
└── main.go # Ping client (default :8083)
The Stovepipe controllers live under stovepipe/controller/: ingest.go contains the RPC controller, while process/, build/, buildsignal/, record/, and dlq/ contain queue controllers. Its extensions live under stovepipe/extension/; this directory only contains runnable wiring and a Docker Compose stack for manual testing.
| Variable | Required | Description | Default |
|---|---|---|---|
STORAGE_MYSQL_DSN |
yes | Storage database DSN | — |
QUEUE_MYSQL_DSN |
yes | Queue database DSN | — |
QUEUE_LOG_LEVEL |
no | Message-queue logger level | info |
PORT |
no | gRPC listen address | :8083 |
HOSTNAME |
no | Subscriber name for the queue consumers | stovepipe-<unix_ts> |
make local-stovepipe-start # builds the Linux binary, starts the service + both MySQL DBs, applies storage + queue schemas
make local-stovepipe-stop # tears the stack down
make local-stovepipe-logs # follow logsThe compose service key is stovepipe-service, so under the default project stovepipe the container is stovepipe-stovepipe-service-1. Inside the container the server listens on :8080, published on a random ephemeral host port.
make local-stovepipe-debug-startAttach with .vscode/launch.json (Debug: attach (dlv in docker)), then send a request using the gRPC port from the make output.
# Ingest example
grpcurl -plaintext -d '{"queue":"monorepo/main"}' localhost:PORT uber.submitqueue.stovepipe.Stovepipe/Ingest
# Retained history by request ID
grpcurl -plaintext -d '{"queue":"monorepo/main","request_id":"1"}' localhost:PORT uber.submitqueue.stovepipe.Stovepipe/GetRequestHistoryByID
# Retained history by exact commit URI
grpcurl -plaintext -d '{"queue":"monorepo/main","uri":"git://monorepo/main/HEAD"}' localhost:PORT uber.submitqueue.stovepipe.Stovepipe/GetRequestHistoryByURIHistory lookup is defined by retained request_log rows. A request with no retained rows is not discoverable through these RPCs, even if operational request data still exists.
bazel build //service/stovepipe/server:stovepipe
bazel build //service/stovepipe/client:stovepipe
go run ./service/stovepipe/servergo run ./service/stovepipe/client -addr localhost:8083 -message "hello"
# or
make run-client-stovepipe SERVER_ADDR=localhost:8083 MESSAGE=hello
# grpcurl
grpcurl -plaintext -d '{"message": "hello"}' localhost:8083 uber.submitqueue.stovepipe.Stovepipe/PingThe server handles SIGINT / SIGTERM gracefully: it drains in-flight RPCs, then stops the primary pipeline consumer followed by the DLQ consumer (30-second limit for each). It exits 0 on clean shutdown, 143 (128 + SIGTERM) when stopped by signal, and 1 on startup/runtime errors (details on stderr). Shutdown errors override the signal exit code.