Skip to content

docs(rfc): propose service-scoped extensions, splitting SubmitQueue storage per service - #740

Open
roychying wants to merge 1 commit into
mainfrom
chenghan.ying/service-scoped-extensions-rfc
Open

roychying wants to merge 1 commit into
mainfrom
chenghan.ying/service-scoped-extensions-rfc

Conversation

@roychying

Copy link
Copy Markdown
Contributor

Why?

The gateway and orchestrator own disjoint halves of the storage extension, but nothing expresses that. Both depend on one 13-store Storage aggregate, so a gateway controller can resolve BatchStore and nothing objects, and the schema is one filegroup — a deployment giving each service its own database provisions all thirteen tables in both.

What?

An RFC for the layout rule: an extension used by exactly one service of a multi-service domain lives at {domain}/{service}/extension/{ext}/, owning its implementations, mocks and schema. SubmitQueue storage goes first; the rule is meant to apply to every extension after it.

Test Plan

Issue


**Promote the contract package to `platform/`.** Rejected under Decision 5.

## Worth considering afterwards: relocating `submitqueue/core/`

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could take the same approach for 'core/'. not necessary now, but might worth considering once this lands.

@roychying
roychying force-pushed the chenghan.ying/service-scoped-extensions-rfc branch from fcd6c7f to ead85cc Compare September 22, 2026 22:48
@roychying
roychying added this pull request to stack #742 September 22, 2026 23:11

@behinddwalls behinddwalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agent-written.

4/9 split is right. Split Factory/Storage by service; keep store interfaces at submitqueue/extension/storage/, or the core/ inversion is in this RFC.

@@ -0,0 +1,89 @@
# Service-Scoped Extensions

Moving an extension used by exactly one service of a multi-service domain out of `{domain}/extension/` and into `{domain}/{service}/extension/`, so the service and the thing it depends on map one to one. SubmitQueue's storage extension goes first; the layout rule is meant to apply to every extension after it.

@behinddwalls behinddwalls Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agent-written.

Decision 1 already covers more than storage: changeprovider, validator, conflict, buildrunner, and speculation are orchestrator-only today. Say whether they stay at {domain}/extension/ until a later RFC, or only extensions with a schema split move. Implementation also needs the new path in AGENTS.md.

queueconfig is gateway-only now but expected in both services — leave it at submitqueue/extension/queueconfig/.


> The gateway owns the append-only request log and three new logical read models. The orchestrator's request and change stores are pipeline working state with different retention semantics, so neither API reads them.

Nothing enforces it. A gateway controller can resolve `BatchStore` and nothing objects. Deployments feel it too: the MySQL schema is one filegroup, so a deployment that gives each service its own database provisions all thirteen tables in both, including the nine or four the service never reads.

@behinddwalls behinddwalls Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agent-written.

Separate DBs already work: orchestrator publishes logs on the queue; gateway persists them. The four gateway tables are not a shared store.


1. An extension used by exactly one service of a multi-service domain lives at `{domain}/{service}/extension/{ext}/`, with its implementations, mocks and schema underneath it, exactly as a domain-level extension does today.
2. An extension genuinely shared between a domain's services stays at `{domain}/extension/{ext}/`. A single-service domain is unaffected: its domain root is its service root, so nothing moves.
3. SubmitQueue's storage extension splits into `submitqueue/gateway/extension/storage/` and `submitqueue/orchestrator/extension/storage/`. Each declares its own `Factory` and its own `Storage` aggregate covering only its stores.

@behinddwalls behinddwalls Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agent-written.

Split Factory/Storage by service. Keep store interfaces in submitqueue/extension/storage/. Moving *_store.go forces core/ to import a service package in this refactor, not later.

1. An extension used by exactly one service of a multi-service domain lives at `{domain}/{service}/extension/{ext}/`, with its implementations, mocks and schema underneath it, exactly as a domain-level extension does today.
2. An extension genuinely shared between a domain's services stays at `{domain}/extension/{ext}/`. A single-service domain is unaffected: its domain root is its service root, so nothing moves.
3. SubmitQueue's storage extension splits into `submitqueue/gateway/extension/storage/` and `submitqueue/orchestrator/extension/storage/`. Each declares its own `Factory` and its own `Storage` aggregate covering only its stores.
4. `submitqueue/extension/storage/` survives as a contract-only package: the error vocabulary and `Config`, which both halves and `submitqueue/core/` depend on. It keeps its import path, so nothing that uses only those symbols changes.

@behinddwalls behinddwalls Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agent-written.

ErrNotFound/Config callers stay. Materializer and TerminateRequest take Factory/Storage and will change. The storage README belongs with the interfaces.

3. SubmitQueue's storage extension splits into `submitqueue/gateway/extension/storage/` and `submitqueue/orchestrator/extension/storage/`. Each declares its own `Factory` and its own `Storage` aggregate covering only its stores.
4. `submitqueue/extension/storage/` survives as a contract-only package: the error vocabulary and `Config`, which both halves and `submitqueue/core/` depend on. It keeps its import path, so nothing that uses only those symbols changes.
5. The contract package is not promoted to `platform/`. It is not cross-domain — `stovepipe/extension/storage/storage.go` already declares its own verbatim copy of the same symbols, so a per-domain error vocabulary is the existing pattern rather than something this RFC introduces.
6. Each half owns its own MySQL schema directory, so the tables a service creates are the tables it reads.

@behinddwalls behinddwalls Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agent-written.

Keep a union schema for colocated DBs (e2e, local compose), as #737 did. Split schemas are the opt-in for separate DBs.


## Worth considering afterwards: relocating `submitqueue/core/`

Not part of this refactor, and nothing here depends on it. Raising it because the split makes an existing oddity visible, and the cheapest moment to act on it is while the surrounding code is already being touched.

@behinddwalls behinddwalls Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agent-written.

If interfaces move with Decision 3, core/ has no domain Storage to import. That is in scope, not a follow-on.


So the suggestion is to move `core/batch` and `core/changeset` under the orchestrator, and split `core/request` by file. It would be mechanical: the files already divide along the line, and rebinding them has already proven where each one belongs.

A smaller observation in passing. `core/request/request.go` holds `CurrentState` and `GetCurrentStateFromRequestLog`, and neither has a caller outside that file's own tests. It is an exported symbol in a domain-internal package, so it is not API anyone outside the repository depends on. If that is genuinely dead, deleting it removes the third file from the `core/request` question and leaves two going opposite directions. Worth confirming rather than assuming.

@behinddwalls behinddwalls Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agent-written.

Confirmed unused. Drop from this RFC; delete in its own PR.

@behinddwalls

behinddwalls commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Agent-written.

Layout

platform/extension/counter/              # shared; mysql impl; each service has its own table
submitqueue/
├── extension/
│   ├── storage/                         # errors, Config, store interfaces
│   └── queueconfig/                     # shared — stays here
├── gateway/extension/storage/
│   ├── storage.go                       # Factory/Storage (4)
│   └── mysql/schema/                    # 4 tables
└── orchestrator/extension/storage/
    ├── storage.go                       # Factory/Storage (9)
    └── mysql/schema/                    # 9 tables

No store is shared today. RequestLogStore is gateway-owned (orchestrator publishes logs; gateway persists).

A later shared store: interface in submitqueue/extension/storage/, accessor on both aggregates, explicit table ownership. Do not reassemble the 13-way Storage.

queueconfig stays at submitqueue/extension/queueconfig/. It will be used by both services; do not move it under gateway.

@behinddwalls behinddwalls left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agent-written.

Requesting changes: split Factory/Storage by service, but keep store interfaces in submitqueue/extension/storage/. Moving *_store.go inverts core/ and is in scope for this RFC. queueconfig stays domain-level (shared). Preserve a colocated schema union.

@behinddwalls
behinddwalls force-pushed the chenghan.ying/service-scoped-extensions-rfc branch from ead85cc to ce8de6d Compare September 23, 2026 15:02

This branch has not been deployed

No deployments
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