diff --git a/doc/rfc/index.md b/doc/rfc/index.md index d3697190..12981bcb 100644 --- a/doc/rfc/index.md +++ b/doc/rfc/index.md @@ -10,6 +10,7 @@ Design documents and technical proposals, grouped by scope. Shared/cross-cutting - [Consumer Gate](consumer-gate.md) - Stopping and starting individual queue controllers at runtime via a consumer-side check: blocked deliveries are recorded as parked and postponed back to the queue (re-checked on redelivery), gate state as a separate extension with a file-based first implementation shared by tests and operators - [Consumer Hold](consumer-hold.md) - Fourth delivery outcome letting a controller postpone its delivery: the message becomes a partition barrier that pauses consumption for a chosen delay, redelivers in order, and does not count as a failure toward dead-lettering - [Change URIs](change-uri.md) - Identity of a code change: `scheme://{host[:port]}/{path}` per provider (GitHub PR, Phabricator Diff, git ref/commit) and canonical-form rules +- [Scoped Sequential Resource IDs](scoped-resource-ids.md) - Queue-scoped positive numeric IDs allocated by durable per-domain, per-kind counters, stored without queue/kind prefixes, and rendered directly in resource URL segments - [Hooks Framework](hook-framework.md) - Implemented fire-and-forget side effects: one shared `HookEvent` contract (`api/base/hook/`) on a durable per-domain hook topic, dispatched by `platform/hook` to `platform/extension/hook`. Stovepipe `process` and `record` publish repository events; the SubmitQueue orchestrator registers the stage and does not publish events yet - [Service-Scoped Extensions](service-scoped-extensions.md) - Implemented for SubmitQueue storage: gateway and orchestrator aggregates, schemas, and the core packages that serve one service have moved, while store contracts stay at `submitqueue/extension/storage`. Domain-level `buildrunner`, `conflict`, and `speculation` have not moved, and `changeset` still declares its own store slice diff --git a/doc/rfc/scoped-resource-ids.md b/doc/rfc/scoped-resource-ids.md new file mode 100644 index 00000000..99122361 --- /dev/null +++ b/doc/rfc/scoped-resource-ids.md @@ -0,0 +1,74 @@ +# Scoped Sequential Resource IDs + +## Status + +Proposed. + +## Decision + +A generated resource ID is the canonical decimal string for a positive value returned by a durable counter scoped to `(owner domain, queue, resource kind)`. + +| Resource | Current ID | Proposed ID | Complete identity | +|---|---:|---:|---| +| SubmitQueue request | `demo-queue/42` | `"42"` | `(submitqueue, demo-queue, request, "42")` | +| SubmitQueue batch | `demo-queue/batch/7` | `"7"` | `(submitqueue, demo-queue, batch, "7")` | +| Stovepipe request | `request/monorepo/main/42` | `"42"` | `(stovepipe, monorepo/main, request, "42")` | + +The decimal ID is unique only within its scope. The same value may appear in another queue, resource kind, or domain. APIs and messages therefore carry the queue separately; their typed field or message type supplies the resource kind. + +Do not embed scope into the ID. Forms such as `demo-queue/42`, `demo-queue/batch/7`, `request.42`, and ARN-like resource names are not stored or accepted as IDs. + +## Counter + +The counter backend persists one high-water mark per `(owner domain, queue, resource kind)`. For example: + +```text +(submitqueue, demo-queue, request) -> 42 +(submitqueue, demo-queue, batch) -> 7 +(stovepipe, demo-queue, request) -> 11 +``` + +Controllers allocate an ID before creating the resource; stores accept the caller-supplied ID and never generate one. + +- The first ID is `1`; `0` is the unset value. +- Allocation is atomic across replicas and durable across restarts. +- Allocated values are never reused. Failed writes may leave gaps. +- Overflow fails instead of wrapping. +- Numeric order is allocation order only within the same scope. + +The counter contract requires an atomic durable increment, not MySQL specifically. MySQL remains the initial implementation. + +## Storage and contracts + +Resource tables keep IDs as strings. Queue remains the leading key: + +```text +request(queue, id VARCHAR(...), ...) PRIMARY KEY (queue, id) +batch(queue, id VARCHAR(...), ...) PRIMARY KEY (queue, id) +``` + +Reference columns use the same string type. Domain entities may use distinct named string types such as `RequestID` and `BatchID`; protobuf resource fields remain `string`. The counter backend may store its high-water marks as integers, and controllers convert allocated values to canonical decimal strings before creating resources. + +This proposal applies only to counter-generated resources. Provider build IDs, message and hook IDs, change URIs, and content hashes keep their existing contracts. + +## URLs and display + +The decimal ID is used directly as one path segment: + +```text +/requests/42 +/batches/7 +``` + +The route supplies the resource kind; the request context supplies the queue. Queue URL design is separate. + +A UI may display `request.42`, `batch.7`, or `#42`, but those are derived labels, not identities. + +## Rejected alternatives + +- **Queue or kind prefixes:** duplicate explicit context, lengthen keys, require parsing, and introduce URL separators. +- **ARN-like names:** solve global lookup, which current APIs neither provide nor require. +- **UUIDs or a global counter:** provide global uniqueness at the cost of unnecessary encoding or coordination. +- **Integer resource fields:** couple the persisted and wire contracts to the current counter representation without adding identity semantics. +- **SQL auto-increment or `MAX(id) + 1`:** move allocation into one storage implementation or fail under concurrency. +- **Process-local counters:** reuse IDs after restart and collide across replicas.