feat(storage)!: resolve queue-scoped storage through a factory, split off global read-model stores - #535
Merged
Merged
Conversation
This was referenced Aug 6, 2026
behinddwalls
force-pushed
the
preetam/queue-on-payloads
branch
from
August 6, 2026 16:46
229a29a to
71c44ed
Compare
behinddwalls
force-pushed
the
preetam/storage-factory
branch
from
August 6, 2026 16:46
a055dd3 to
e0c1223
Compare
behinddwalls
marked this pull request as ready for review
August 6, 2026 16:53
behinddwalls
marked this pull request as draft
August 6, 2026 16:56
behinddwalls
marked this pull request as ready for review
August 6, 2026 17:01
mnoah1
approved these changes
Aug 6, 2026
… off global read-model stores
## Summary
### Why?
Storage was the last queue-blind seam: every decision/action extension already resolves per queue through `Config{QueueName}`/`Factory.For`, but storage was a singleton aggregate where queue identity leaked in as ad-hoc method arguments on three stores. Making the aggregate queue-resolved makes cross-queue access unrepresentable at the contract level and lets the host route different queues to different backends with zero library changes.
### What?
The storage extension gains `Config{QueueName}` and a `Factory` interface mirroring the extension contract. The `Storage` aggregate shrinks to the eight queue-scoped stores (request, request-batch, change, batch, batch-dependent, queue-batch-state, build, request-queue-summary); a resolved instance is bound to its queue — queue-keyed reads are implicitly scoped and writes whose entity queue disagrees with the binding are rejected. `ChangeStore.GetByURI`, `QueueBatchStateStore.List`/`Delete`, and `RequestQueueSummaryStore.Get`/`List` drop their explicit queue parameters.
The three cross-queue read-model stores (`RequestLogStore`, `RequestSummaryStore`, `RequestURIStore`) leave the aggregate and are injected individually: their lookups start from identifiers that arrive without queue context (a bare sqid or change URI at the status API), so they are global by classification. The materializer now takes the three globals plus the factory and resolves the queue-scoped summary projection from the queue on the authoritative summary; the changeset resolver takes the factory and resolves per batch.
Every consumer resolves its bound store set from the queue carried on the payload (PR: queue on every internal payload); an empty or unresolvable queue is a malformed message. Runway's `MergeResult` gains an additive `queue_name` echo so the merge-signal and merge-conflict-signal consumers (and their DLQs) can route without loading state. The MySQL backend keeps one shared pool: `For` binds a queue over the shared tables, the globals hang off the backend, and small wiring adapters in the gateway/orchestrator mains bridge it into the `storage.Factory` seam — per-queue backend routing stays a host decision.
## Test Plan
✅ `go test ./...` (only the pre-existing runway git-merger environment failures) ✅ storage integration contract suite via bazel (now factory-driven, including cross-queue record isolation) ✅ `make fmt` ✅ `make gazelle` ✅ `make mocks`. Gateway/orchestrator integration suites fail locally on a docker-buildx sandbox permission unrelated to this diff; covered by CI.
behinddwalls
force-pushed
the
preetam/storage-factory
branch
from
August 6, 2026 20:46
e0c1223 to
d342ed4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Why?
Storage was the last queue-blind seam: every decision/action extension already resolves per queue through
Config{QueueName}/Factory.For, but storage was a singleton aggregate where queue identity leaked in as ad-hoc method arguments on three stores. Making the aggregate queue-resolved makes cross-queue access unrepresentable at the contract level and lets the host route different queues to different backends with zero library changes.What?
The storage extension gains
Config{QueueName}and aFactoryinterface mirroring the extension contract. TheStorageaggregate shrinks to the eight queue-scoped stores (request, request-batch, change, batch, batch-dependent, queue-batch-state, build, request-queue-summary); a resolved instance is bound to its queue — queue-keyed reads are implicitly scoped and writes whose entity queue disagrees with the binding are rejected.ChangeStore.GetByURI,QueueBatchStateStore.List/Delete, andRequestQueueSummaryStore.Get/Listdrop their explicit queue parameters.The three cross-queue read-model stores (
RequestLogStore,RequestSummaryStore,RequestURIStore) leave the aggregate and are injected individually: their lookups start from identifiers that arrive without queue context (a bare sqid or change URI at the status API), so they are global by classification. The materializer now takes the three globals plus the factory and resolves the queue-scoped summary projection from the queue on the authoritative summary; the changeset resolver takes the factory and resolves per batch.Every consumer resolves its bound store set from the queue carried on the payload (PR: queue on every internal payload); an empty or unresolvable queue is a malformed message. Runway's
MergeResultgains an additivequeue_nameecho so the merge-signal and merge-conflict-signal consumers (and their DLQs) can route without loading state. The MySQL backend keeps one shared pool:Forbinds a queue over the shared tables, the globals hang off the backend, and small wiring adapters in the gateway/orchestrator mains bridge it into thestorage.Factoryseam — per-queue backend routing stays a host decision.Test Plan
✅
go test ./...(only the pre-existing runway git-merger environment failures) ✅ storage integration contract suite via bazel (now factory-driven, including cross-queue record isolation) ✅make fmt✅make gazelle✅make mocks. Gateway/orchestrator integration suites fail locally on a docker-buildx sandbox permission unrelated to this diff; covered by CI.