Skip to content

Commit deb8d58

Browse files
committed
refactor: nest domain code under submitqueue/, extract shared layers
## Summary ### Why? SubmitQueue's domain code lived at the repo root (gateway/, orchestrator/, entity/, extension/) while a second service family, Stovepipe, already lived under stovepipe/. The layout was asymmetric and the line between domain-specific and genuinely shared code was implicit. This restructure gives every domain its own top-level folder and pulls shared code into clearly labeled shared layers, so "what's shared vs domain" is obvious from the directory tree. ### What? New layout — shared building blocks at the top level, one folder per domain: ``` core/ # SHARED cross-domain infra: errs, httpclient, metrics (no domain deps) entity/ # SHARED entities: queue/ extension/ # SHARED extensions: queue/ submitqueue/ # SubmitQueue domain ├── gateway/ ├── orchestrator/ ├── entity/ ├── extension/ └── core/ # consumer, request (shared between SubmitQueue's own services) stovepipe/ # Stovepipe domain (gateway/ orchestrator/ entity/ extension/ core/) example/ ├── submitqueue/ # servers + clients + docker-compose └── stovepipe/ test/ ├── integration/{core,submitqueue,stovepipe}/ └── e2e/submitqueue/ doc/rfc/ # sql-queue-rfc.md (shared) + submitqueue/{workflow,build-runner}.md ``` - Domain code nests per domain: submitqueue/{gateway,orchestrator,entity,extension,core}, mirroring the existing stovepipe/ layout. - Shared layers at the top level: core/ (cross-domain infra — errs, httpclient, metrics, now with zero domain deps), entity/ (shared entities: queue), extension/ (shared extensions: queue). - submitqueue/core/ holds infra shared only between SubmitQueue's own services (consumer, request), moved out of the top-level core/ so it is domain-free. stovepipe/core/ added as a placeholder for symmetry. - example/ and test/ mirror the split: example/submitqueue/..., test/integration/{core,submitqueue,stovepipe}/..., test/e2e/submitqueue/. - RFCs: SubmitQueue-specific RFCs moved under doc/rfc/submitqueue/; the shared sql-queue RFC stays at doc/rfc/. - Make targets made domain-explicit (build-submitqueue-*, integration-test-submitqueue-*, local-submitqueue-*), mirroring the existing *-stovepipe-* targets; CI updated to match. - Tests: domain-qualified docker-compose project names; integration/e2e run in parallel (--test_output=errors) and always execute a real pass (tags=["external"]) since they are non-hermetic. - Import paths rewritten throughout; protos regenerated; CLAUDE.md, READMEs, and doc/howto/TESTING.md updated to the new structure and naming convention. No behavior changes — this is a structural move. ## Test Plan - ✅ `make build` (bazel build //...) and `make test` (40/40 unit tests) - ✅ `make integration-test` — 8/8 pass; verified running in parallel (67s vs 275s serial) and always-run (uncached re-runs) - ✅ `make e2e-test` — passes; always-run - ✅ `make lint` (incl. license headers); gazelle / mocks / tidy / fmt all stable (CI gates clean)
1 parent 92cef12 commit deb8d58

296 files changed

Lines changed: 1183 additions & 1051 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ jobs:
9292
- uses: ./.github/actions/setup
9393

9494
- name: Run Gateway integration tests
95-
run: make integration-test-gateway
95+
run: make integration-test-submitqueue-gateway
9696

9797
orchestrator-integration-test:
9898
name: Orchestrator Integration Test
@@ -103,7 +103,7 @@ jobs:
103103
- uses: ./.github/actions/setup
104104

105105
- name: Run Orchestrator integration tests
106-
run: make integration-test-orchestrator
106+
run: make integration-test-submitqueue-orchestrator
107107

108108
# ---------------------------------------------------------------------------
109109
# EXTENSION TESTS
@@ -117,7 +117,7 @@ jobs:
117117
- uses: ./.github/actions/setup
118118
- uses: ./.github/actions/run-bazel-test
119119
with:
120-
target: //test/integration/extension/counter/...
120+
target: //test/integration/submitqueue/extension/counter/...
121121

122122
queue-integration-test:
123123
name: Queue Extension Test
@@ -139,7 +139,7 @@ jobs:
139139
- uses: ./.github/actions/setup
140140
- uses: ./.github/actions/run-bazel-test
141141
with:
142-
target: //test/integration/extension/storage/...
142+
target: //test/integration/submitqueue/extension/storage/...
143143

144144
# ---------------------------------------------------------------------------
145145
# CORE TESTS
@@ -153,7 +153,7 @@ jobs:
153153
- uses: ./.github/actions/setup
154154
- uses: ./.github/actions/run-bazel-test
155155
with:
156-
target: //test/integration/core/consumer/...
156+
target: //test/integration/submitqueue/core/consumer/...
157157

158158
# ---------------------------------------------------------------------------
159159
# REQUIRED CHECKS GATE

BUILD.bazel

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ load("@gazelle//:def.bzl", "gazelle")
77
# gazelle:exclude .claude
88

99
# Resolve protobuf import ambiguities - use the actual protopb packages, not the proto aliases
10-
# gazelle:resolve go github.com/uber/submitqueue/gateway/protopb //gateway/protopb
11-
# gazelle:resolve go github.com/uber/submitqueue/orchestrator/protopb //orchestrator/protopb
10+
# gazelle:resolve go github.com/uber/submitqueue/submitqueue/gateway/protopb //submitqueue/gateway/protopb
11+
# gazelle:resolve go github.com/uber/submitqueue/submitqueue/orchestrator/protopb //submitqueue/orchestrator/protopb
1212
# gazelle:resolve go github.com/uber/submitqueue/stovepipe/gateway/protopb //stovepipe/gateway/protopb
1313
# gazelle:resolve go github.com/uber/submitqueue/stovepipe/orchestrator/protopb //stovepipe/orchestrator/protopb
1414

CLAUDE.md

Lines changed: 54 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ SubmitQueue is a distributed system for managing code submission workflows. It f
99
1. **Immutable entities** — once created, don't modify in place. Create new versions with updated fields.
1010
2. **Eventual consistency** — handle stale reads, idempotent operations, and convergence over time.
1111
3. **Event sourcing** — store events (what happened) rather than just current state for critical changes.
12-
4. **Optimistic locking** — use version numbers instead of pessimistic locks. Avoid transactions; prefer optimistic concurrency and retries. **Version arithmetic lives in the controller, not the storage layer.** Update methods take both `oldVersion` (the where-clause guard) and `newVersion` (the value to write); the store performs a pure conditional write. Controllers compute `newVersion = oldVersion + 1`, call the store, and only assign `entity.Version = newVersion` after the call succeeds. Pre-incrementing in memory before the call is a bug pattern — on error the in-memory version drifts ahead of the database. See [extension/storage/README.md](extension/storage/README.md).
12+
4. **Optimistic locking** — use version numbers instead of pessimistic locks. Avoid transactions; prefer optimistic concurrency and retries. **Version arithmetic lives in the controller, not the storage layer.** Update methods take both `oldVersion` (the where-clause guard) and `newVersion` (the value to write); the store performs a pure conditional write. Controllers compute `newVersion = oldVersion + 1`, call the store, and only assign `entity.Version = newVersion` after the call succeeds. Pre-incrementing in memory before the call is a bug pattern — on error the in-memory version drifts ahead of the database. See [submitqueue/extension/storage/README.md](submitqueue/extension/storage/README.md).
1313
5. **Idempotency keys** — include unique request IDs, check for duplicates before executing.
1414

1515
```go
@@ -35,27 +35,37 @@ request.Version = newVersion
3535
### Project Layout
3636

3737
```
38-
submitqueue/
39-
├── gateway/ # Gateway service (port 8081) - entry point
40-
├── orchestrator/ # Orchestrator service (port 8082) - coordinates jobs
41-
├── entity/ # Domain entities (Request, Change, enums)
38+
submitqueue/ # repo root (Go module github.com/uber/submitqueue)
39+
├── core/ # SHARED cross-domain infrastructure (errs, httpclient, metrics) — no domain deps
40+
├── entity/ # SHARED domain entities
4241
│ └── queue/ # Queue-specific entities (Message)
43-
├── extension/ # Pluggable backend implementations
44-
│ ├── counter/ # Sequential number generation (interface + mysql/)
45-
│ ├── queue/ # Messaging queue abstraction (interface + sql/)
46-
│ └── storage/ # Storage abstraction (interface + mysql/)
47-
├── core/ # Shared infrastructure packages reused across services
48-
│ ├── consumer/ # Queue consumption framework (lifecycle, ack/nack, routing)
49-
│ └── errs/ # Error classification framework (user vs infra, retryability)
42+
├── extension/ # SHARED extensions
43+
│ └── queue/ # Messaging queue abstraction (interface + mysql/)
44+
├── submitqueue/ # SubmitQueue domain
45+
│ ├── gateway/ # Gateway service (port 8081) - entry point
46+
│ ├── orchestrator/ # Orchestrator service (port 8082) - coordinates jobs
47+
│ ├── entity/ # SubmitQueue-specific domain entities
48+
│ ├── extension/ # SubmitQueue-specific extension impls (storage, counter, mergechecker, ...)
49+
│ └── core/ # SubmitQueue-internal shared infra (consumer, request)
50+
├── stovepipe/ # Stovepipe domain
51+
│ ├── gateway/ # Gateway service: commit deployment verification entry point
52+
│ ├── orchestrator/ # Orchestrator service: commit verification pipeline
53+
│ ├── entity/ # Stovepipe-specific domain entities
54+
│ ├── extension/ # Stovepipe-specific extension impls
55+
│ └── core/ # Stovepipe-internal shared infra (placeholder; mirrors submitqueue/core)
5056
├── tool/ # Development and CI tooling
51-
├── example/server/ # Runnable servers with Docker Compose
57+
├── example/
58+
│ ├── submitqueue/ # Runnable SubmitQueue servers/clients + Docker Compose
59+
│ └── stovepipe/ # Runnable Stovepipe servers/clients
5260
├── test/
53-
│ ├── e2e/ # End-to-end tests (full stack)
54-
│ ├── integration/ # Integration tests (per-service + extensions)
61+
│ ├── e2e/submitqueue/ # End-to-end tests (full stack)
62+
│ ├── integration/ # Integration tests (core/, submitqueue/, stovepipe/)
5563
│ └── testutil/ # Test utilities (ComposeStack, MySQL helpers)
5664
└── doc/ # Documentation
5765
```
5866

67+
The repo hosts shared building blocks at the top level — cross-domain infrastructure in `core/`, shared entities in `entity/`, shared extensions in `extension/` — followed by one folder per **domain** (`submitqueue/`, `stovepipe/`). Each domain owns the same internal layout (`gateway/`, `orchestrator/`, `entity/`, `extension/`, `core/`); a domain's own `core/` (e.g. `submitqueue/core/`) holds infra shared only between that domain's services.
68+
5969
### Services
6070

6171
Each service follows the same layout:
@@ -122,13 +132,17 @@ When in doubt, ask: *"If the next implementation were DynamoDB / Kafka / Bigtabl
122132
123133
### Import Paths
124134
125-
- RPC Controllers: `github.com/uber/submitqueue/{service}/controller`
126-
- Queue Controllers: `github.com/uber/submitqueue/{service}/controller/{step}`
127-
- Consumer: `github.com/uber/submitqueue/core/consumer`
128-
- Proto (generated): `github.com/uber/submitqueue/{service}/protopb`
129-
- Extensions: `github.com/uber/submitqueue/extension/{extension}`
130-
- Extension impl: `github.com/uber/submitqueue/extension/{extension}/{impl}`
131-
- Entities: `github.com/uber/submitqueue/entity/{domain}`
135+
Paths follow the directory layout: shared code is top-level, domain code nests under the domain folder (`submitqueue/`, `stovepipe/`).
136+
137+
- RPC Controllers: `github.com/uber/submitqueue/{domain}/{service}/controller` (e.g. `.../submitqueue/gateway/controller`)
138+
- Queue Controllers: `github.com/uber/submitqueue/{domain}/{service}/controller/{step}`
139+
- Proto (generated): `github.com/uber/submitqueue/{domain}/{service}/protopb`
140+
- Domain entities: `github.com/uber/submitqueue/{domain}/entity` (e.g. `.../submitqueue/entity`)
141+
- Domain extensions: `github.com/uber/submitqueue/{domain}/extension/{ext}[/{impl}]` (e.g. `.../submitqueue/extension/storage/mysql`)
142+
- Domain-internal infra: `github.com/uber/submitqueue/{domain}/core/{pkg}` (e.g. `.../submitqueue/core/consumer`, `.../submitqueue/core/request`)
143+
- Shared entities: `github.com/uber/submitqueue/entity/{name}` (e.g. `.../entity/queue`)
144+
- Shared extensions: `github.com/uber/submitqueue/extension/{name}` (e.g. `.../extension/queue`)
145+
- Cross-domain infra: `github.com/uber/submitqueue/core/{pkg}` (e.g. `.../core/errs`, `.../core/metrics`)
132146
133147
## Development
134148
@@ -144,7 +158,7 @@ Bazel with Bzlmod (NOT WORKSPACE).
144158
### Proto Generation
145159
146160
Generated proto files are committed. When modifying `.proto` files:
147-
1. Edit in `{service}/proto/`
161+
1. Edit in `{domain}/{service}/proto/` (e.g. `submitqueue/gateway/proto/`)
148162
2. `make proto` (generates `*.pb.go`, `*_grpc.pb.go`, `*.pb.yarpc.go`)
149163
3. Commit all generated files
150164
@@ -153,6 +167,7 @@ Generated proto files are committed. When modifying `.proto` files:
153167
- **Directories**: singular (`mock/`, `entity/`, not `mocks/`, `entities/`)
154168
- **Files**: `{method}.go`, `{entity}.go`, `{file}_test.go`, `BUILD.bazel`
155169
- **Proto files**: `{service}.proto`
170+
- **Test compose contexts**: the `testContext` passed to `NewComposeStack` (and thus the `sq-test-{context}-…` Docker project/container names) must be **domain-qualified** — `{category}-{domain}-{name}` where `{category}` is `svc`/`ext`/`core`/`e2e` and `{domain}` is `submitqueue`/`stovepipe`/… (omit the domain only for shared/cross-domain suites, e.g. `ext-queue-sql`). This keeps containers unambiguous and lets suites run in parallel. See [doc/howto/TESTING.md](doc/howto/TESTING.md#container-naming).
156171
- **README files**: Do not duplicate interface or type definitions as code blocks in READMEs. Describe behavior in prose and let readers navigate to the source. Only include code samples when explicitly instructed.
157172
- **Markdown prose width**: Do not hard-wrap prose in Markdown docs (RFCs under `doc/`, READMEs). Write one line per paragraph and one line per list item, and let the editor soft-wrap — hard wrapping at a fixed column renders as a narrow fixed-width column regardless of window size. Code blocks, tables, and ASCII diagrams keep their own line breaks.
158173
@@ -180,36 +195,36 @@ make mocks # Generate mock files using mockgen
180195
make integration-test # Run all integration tests (Docker-based)
181196
make e2e-test # Run end-to-end tests
182197
make proto # Regenerate proto files
183-
make local-start # Start full stack with Docker Compose
184-
make local-ps # Show running containers and ports
185-
make local-logs # View logs from all services
198+
make local-submitqueue-start # Start full stack with Docker Compose
199+
make local-submitqueue-ps # Show running containers and ports
200+
make local-submitqueue-logs # View logs from all services
186201
make local-stop # Stop all services
187202
make clean # Clean Bazel cache
188203
```
189204

190205
### Common Workflows
191206

192207
**Add new RPC method:**
193-
1. Edit `{service}/proto/*.proto``make proto`
194-
2. Add controller in `{service}/controller/`
195-
3. Wire up in `example/server/{service}/main.go`
208+
1. Edit `{domain}/{service}/proto/*.proto``make proto`
209+
2. Add controller in `{domain}/{service}/controller/`
210+
3. Wire up in `example/{domain}/{service}/server/main.go`
196211

197212
**Add new queue message controller:**
198-
1. Create `{service}/controller/{step}/` implementing `consumer.Controller`
199-
2. Wire up in `example/server/{service}/main.go`
213+
1. Create `{domain}/{service}/controller/{step}/` implementing `consumer.Controller`
214+
2. Wire up in `example/{domain}/{service}/server/main.go`
200215

201216
**Add new extension:**
202-
1. Create `extension/{ext}/{impl}/` with factory and interfaces
217+
1. Create the extension under `{domain}/extension/{ext}/{impl}/` (domain-specific, e.g. `submitqueue/extension/...`) or top-level `extension/{ext}/{impl}/` (shared across domains) with factory and interfaces
203218
2. Add `BUILD.bazel`, tests, and README.md
204219

205220
**Add new entity:**
206-
1. Create `entity/{domain}/{entity}.go` with test file and `BUILD.bazel`
221+
1. Create `{domain}/entity/{entity}.go` (domain-specific) or top-level `entity/{name}/{entity}.go` (shared) with test file and `BUILD.bazel`
207222

208223
**Add gomock for an extension interface:**
209224

210-
Mocks are checked-in files generated by [mockgen](https://github.com/uber-go/mock). Run `make mocks` to regenerate, then `make gazelle` to update BUILD files. See `extension/storage/mock/` for the canonical example.
225+
Mocks are checked-in files generated by [mockgen](https://github.com/uber-go/mock). Run `make mocks` to regenerate, then `make gazelle` to update BUILD files. See `submitqueue/extension/storage/mock/` for the canonical example.
211226

212-
To add a mock for a new interface file in an existing mock package (e.g., `extension/storage/new_store.go`):
227+
To add a mock for a new interface file in an existing mock package (e.g., `submitqueue/extension/storage/new_store.go`):
213228

214229
1. Add a `//go:generate` directive to the interface file:
215230
```go
@@ -219,10 +234,10 @@ To add a mock for a new interface file in an existing mock package (e.g., `exten
219234
3. Run `make gazelle` to update `BUILD.bazel` files.
220235
4. Commit the generated mock file.
221236

222-
To create a mock package for a new extension (e.g., `extension/newext/mock/`):
237+
To create a mock package for a new extension (e.g., `submitqueue/extension/newext/mock/`):
223238

224239
1. Add `//go:generate` directives to each interface file (same pattern as above).
225-
2. Create the `mock/` directory: `mkdir extension/newext/mock/`.
240+
2. Create the `mock/` directory: `mkdir submitqueue/extension/newext/mock/`.
226241
3. Run `make mocks` to generate mock files into the new directory.
227242
4. Run `make gazelle` to create the `BUILD.bazel` file automatically.
228243

@@ -233,7 +248,7 @@ For inline mocks (mock in the same package, e.g., `extension/queue/mysql/mock_st
233248

234249
**Using mocks in tests:**
235250
```go
236-
import storagemock "github.com/uber/submitqueue/extension/storage/mock"
251+
import storagemock "github.com/uber/submitqueue/submitqueue/extension/storage/mock"
237252

238253
ctrl := gomock.NewController(t)
239254
mockStore := storagemock.NewMockRequestStore(ctrl)
@@ -243,7 +258,7 @@ mockStore.EXPECT().Create(gomock.Any(), gomock.Any()).Return(nil)
243258
Test `BUILD.bazel` deps:
244259
```starlark
245260
deps = [
246-
"//extension/storage/mock",
261+
"//submitqueue/extension/storage/mock",
247262
"@org_uber_go_mock//gomock",
248263
]
249264
```

0 commit comments

Comments
 (0)