Skip to content

feat(storage)!: resolve queue-scoped storage through a factory, split off global read-model stores - #535

Merged
behinddwalls merged 1 commit into
mainfrom
preetam/storage-factory
Aug 6, 2026
Merged

feat(storage)!: resolve queue-scoped storage through a factory, split off global read-model stores#535
behinddwalls merged 1 commit into
mainfrom
preetam/storage-factory

Conversation

@behinddwalls

@behinddwalls behinddwalls commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

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 fmtmake gazellemake mocks. Gateway/orchestrator integration suites fail locally on a docker-buildx sandbox permission unrelated to this diff; covered by CI.

@behinddwalls
behinddwalls force-pushed the preetam/queue-on-payloads branch from 229a29a to 71c44ed Compare August 6, 2026 16:46
@behinddwalls
behinddwalls force-pushed the preetam/storage-factory branch from a055dd3 to e0c1223 Compare August 6, 2026 16:46
@behinddwalls
behinddwalls marked this pull request as ready for review August 6, 2026 16:53
@behinddwalls
behinddwalls requested review from a team and sbalabanov as code owners August 6, 2026 16:53
@behinddwalls
behinddwalls marked this pull request as draft August 6, 2026 16:56
@behinddwalls
behinddwalls marked this pull request as ready for review August 6, 2026 17:01
@behinddwalls
behinddwalls changed the base branch from preetam/queue-on-payloads to main August 6, 2026 20:42
… 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
behinddwalls force-pushed the preetam/storage-factory branch from e0c1223 to d342ed4 Compare August 6, 2026 20:46
@behinddwalls
behinddwalls merged commit 7bda3d4 into main Aug 6, 2026
3 checks passed
@behinddwalls
behinddwalls deleted the preetam/storage-factory branch August 6, 2026 20:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants