Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ jobs:
- uses: ./.github/actions/setup

- name: Run Gateway integration tests
run: make integration-test-gateway
run: make integration-test-submitqueue-gateway

orchestrator-integration-test:
name: Orchestrator Integration Test
Expand All @@ -103,7 +103,7 @@ jobs:
- uses: ./.github/actions/setup

- name: Run Orchestrator integration tests
run: make integration-test-orchestrator
run: make integration-test-submitqueue-orchestrator

# ---------------------------------------------------------------------------
# EXTENSION TESTS
Expand All @@ -117,7 +117,7 @@ jobs:
- uses: ./.github/actions/setup
- uses: ./.github/actions/run-bazel-test
with:
target: //test/integration/extension/counter/...
target: //test/integration/submitqueue/extension/counter/...

queue-integration-test:
name: Queue Extension Test
Expand All @@ -139,7 +139,7 @@ jobs:
- uses: ./.github/actions/setup
- uses: ./.github/actions/run-bazel-test
with:
target: //test/integration/extension/storage/...
target: //test/integration/submitqueue/extension/storage/...

# ---------------------------------------------------------------------------
# CORE TESTS
Expand All @@ -153,7 +153,7 @@ jobs:
- uses: ./.github/actions/setup
- uses: ./.github/actions/run-bazel-test
with:
target: //test/integration/core/consumer/...
target: //test/integration/submitqueue/core/consumer/...

# ---------------------------------------------------------------------------
# REQUIRED CHECKS GATE
Expand Down
4 changes: 2 additions & 2 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ load("@gazelle//:def.bzl", "gazelle")
# gazelle:exclude .claude

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

Expand Down
93 changes: 54 additions & 39 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ SubmitQueue is a distributed system for managing code submission workflows. It f
1. **Immutable entities** — once created, don't modify in place. Create new versions with updated fields.
2. **Eventual consistency** — handle stale reads, idempotent operations, and convergence over time.
3. **Event sourcing** — store events (what happened) rather than just current state for critical changes.
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).
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).
5. **Idempotency keys** — include unique request IDs, check for duplicates before executing.

```go
Expand All @@ -35,27 +35,37 @@ request.Version = newVersion
### Project Layout

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

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.

### Services

Each service follows the same layout:
Expand Down Expand Up @@ -122,13 +132,17 @@ When in doubt, ask: *"If the next implementation were DynamoDB / Kafka / Bigtabl

### Import Paths

- RPC Controllers: `github.com/uber/submitqueue/{service}/controller`
- Queue Controllers: `github.com/uber/submitqueue/{service}/controller/{step}`
- Consumer: `github.com/uber/submitqueue/core/consumer`
- Proto (generated): `github.com/uber/submitqueue/{service}/protopb`
- Extensions: `github.com/uber/submitqueue/extension/{extension}`
- Extension impl: `github.com/uber/submitqueue/extension/{extension}/{impl}`
- Entities: `github.com/uber/submitqueue/entity/{domain}`
Paths follow the directory layout: shared code is top-level, domain code nests under the domain folder (`submitqueue/`, `stovepipe/`).

- RPC Controllers: `github.com/uber/submitqueue/{domain}/{service}/controller` (e.g. `.../submitqueue/gateway/controller`)
- Queue Controllers: `github.com/uber/submitqueue/{domain}/{service}/controller/{step}`
- Proto (generated): `github.com/uber/submitqueue/{domain}/{service}/protopb`
- Domain entities: `github.com/uber/submitqueue/{domain}/entity` (e.g. `.../submitqueue/entity`)
- Domain extensions: `github.com/uber/submitqueue/{domain}/extension/{ext}[/{impl}]` (e.g. `.../submitqueue/extension/storage/mysql`)
- Domain-internal infra: `github.com/uber/submitqueue/{domain}/core/{pkg}` (e.g. `.../submitqueue/core/consumer`, `.../submitqueue/core/request`)
- Shared entities: `github.com/uber/submitqueue/entity/{name}` (e.g. `.../entity/queue`)
- Shared extensions: `github.com/uber/submitqueue/extension/{name}` (e.g. `.../extension/queue`)
- Cross-domain infra: `github.com/uber/submitqueue/core/{pkg}` (e.g. `.../core/errs`, `.../core/metrics`)

## Development

Expand All @@ -144,7 +158,7 @@ Bazel with Bzlmod (NOT WORKSPACE).
### Proto Generation

Generated proto files are committed. When modifying `.proto` files:
1. Edit in `{service}/proto/`
1. Edit in `{domain}/{service}/proto/` (e.g. `submitqueue/gateway/proto/`)
2. `make proto` (generates `*.pb.go`, `*_grpc.pb.go`, `*.pb.yarpc.go`)
3. Commit all generated files

Expand All @@ -153,6 +167,7 @@ Generated proto files are committed. When modifying `.proto` files:
- **Directories**: singular (`mock/`, `entity/`, not `mocks/`, `entities/`)
- **Files**: `{method}.go`, `{entity}.go`, `{file}_test.go`, `BUILD.bazel`
- **Proto files**: `{service}.proto`
- **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).
- **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.
- **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.

Expand Down Expand Up @@ -180,36 +195,36 @@ make mocks # Generate mock files using mockgen
make integration-test # Run all integration tests (Docker-based)
make e2e-test # Run end-to-end tests
make proto # Regenerate proto files
make local-start # Start full stack with Docker Compose
make local-ps # Show running containers and ports
make local-logs # View logs from all services
make local-submitqueue-start # Start full stack with Docker Compose
make local-submitqueue-ps # Show running containers and ports
make local-submitqueue-logs # View logs from all services
make local-stop # Stop all services
make clean # Clean Bazel cache
```

### Common Workflows

**Add new RPC method:**
1. Edit `{service}/proto/*.proto` → `make proto`
2. Add controller in `{service}/controller/`
3. Wire up in `example/server/{service}/main.go`
1. Edit `{domain}/{service}/proto/*.proto` → `make proto`
2. Add controller in `{domain}/{service}/controller/`
3. Wire up in `example/{domain}/{service}/server/main.go`

**Add new queue message controller:**
1. Create `{service}/controller/{step}/` implementing `consumer.Controller`
2. Wire up in `example/server/{service}/main.go`
1. Create `{domain}/{service}/controller/{step}/` implementing `consumer.Controller`
2. Wire up in `example/{domain}/{service}/server/main.go`

**Add new extension:**
1. Create `extension/{ext}/{impl}/` with factory and interfaces
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
2. Add `BUILD.bazel`, tests, and README.md

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

**Add gomock for an extension interface:**

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.
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.

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

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

To create a mock package for a new extension (e.g., `extension/newext/mock/`):
To create a mock package for a new extension (e.g., `submitqueue/extension/newext/mock/`):

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

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

**Using mocks in tests:**
```go
import storagemock "github.com/uber/submitqueue/extension/storage/mock"
import storagemock "github.com/uber/submitqueue/submitqueue/extension/storage/mock"

ctrl := gomock.NewController(t)
mockStore := storagemock.NewMockRequestStore(ctrl)
Expand All @@ -243,7 +258,7 @@ mockStore.EXPECT().Create(gomock.Any(), gomock.Any()).Return(nil)
Test `BUILD.bazel` deps:
```starlark
deps = [
"//extension/storage/mock",
"//submitqueue/extension/storage/mock",
"@org_uber_go_mock//gomock",
]
```
Expand Down
Loading
Loading