Skip to content

Commit 64f60a8

Browse files
committed
refactor(counter): keep domain as the sequence name
## Summary ### Why? Queue-prefixed resource IDs contain URL separators and duplicate scope already carried by the queue, route, and typed field. Resource IDs should remain flexible string contracts rather than forcing Go, protobuf, or SQL resource fields to integer types. ### What? - Store canonical positive decimal strings such as "42" for SubmitQueue request IDs, SubmitQueue batch IDs, and Stovepipe request IDs while keeping resource and reference fields as strings and VARCHAR columns. - Keep the existing counter schema and (queue, domain) key unchanged; domain names the sequence (request or batch), not an application. SubmitQueue and Stovepipe have separate storage backends, so no ownerDomain dimension is introduced. Stovepipe uses the durable MySQL counter. - Put the shared formatting, validation, and numeric comparison helpers in platform/base/id. Keep domain naming consistent across the counter contract, implementation, callers, mocks, tests, and docs. - Validate direct resource-ID inputs and retain the queue-scoped test fixes and cross-queue ID regression coverage from the prior review pass. Provider IDs, URIs, hashes, and derived event IDs keep their contracts. ## Test Plan - ✅ make build - ✅ make test (129 targets passed) - ✅ make check-gazelle, make check-mocks, make check-tidy, and make lint - ✅ git diff --check - ✅ git diff --quiet main -- ':(glob)**/schema/*.sql' (no schema differences from main) - ✅ Rebased onto main (`6884180e`), preserving the RFC → implementation stack and all seven commits. Runtime code is unchanged by the RFC table update. - ✅ Fresh MySQL counter integration run with test-result caching disabled. - ✅ The preceding comment pass also ran SubmitQueue gateway integration, Stovepipe integration, and Stovepipe e2e successfully (4/4 targets including the counter suite). - ⚠️ aifx verify could not complete its monorepo coverage, generic Go lint, and UReview API checks; repository-native checks above passed.
1 parent d4b0c5b commit 64f60a8

13 files changed

Lines changed: 76 additions & 75 deletions

File tree

‎doc/rfc/scoped-resource-ids.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,23 @@
22

33
## Decision
44

5-
A generated resource ID is the canonical decimal string for a positive value returned by a durable counter scoped to `(queue, resource type)` within an application's storage.
5+
A generated resource ID is the canonical decimal string for a positive value returned by a durable counter scoped to `(queue, domain)` within an application's storage.
66

7-
SubmitQueue and Stovepipe use separate storage backends. Resource type is `request` or `batch`; there is no additional application-domain key or schema change.
7+
The counter domain names the sequence (`request` or `batch`), not the application. SubmitQueue and Stovepipe use separate storage backends; there is no additional application-domain key or schema change.
88

99
| Resource | Current ID | Proposed ID | Identity within the application |
1010
|---|---:|---:|---|
1111
| SubmitQueue request | `demo-queue/42` | `"42"` | `(demo-queue, request, "42")` |
1212
| SubmitQueue batch | `demo-queue/batch/7` | `"7"` | `(demo-queue, batch, "7")` |
1313
| Stovepipe request | `request/monorepo/main/42` | `"42"` | `(monorepo/main, request, "42")` |
1414

15-
The decimal ID is unique only within its scope. The same value may appear in another queue, resource type, or application. APIs and messages therefore carry the queue separately; their typed field or message type supplies the resource type.
15+
The decimal ID is unique only within its scope. The same value may appear in another queue, counter domain, or application. APIs and messages therefore carry the queue separately; their typed field or message type supplies the counter domain.
1616

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

1919
## Counter
2020

21-
The counter backend persists one high-water mark per `(queue, resource type)`. MySQL keeps its existing `(queue, domain)` primary key; `domain` stores the resource type. For example:
21+
The counter backend persists one high-water mark per `(queue, domain)`. MySQL keeps its existing schema and primary key. For example:
2222

2323
```text
2424
(demo-queue, request) -> 42
@@ -61,7 +61,7 @@ Both columns use the same queue prefix for comparison. The before batch/change r
6161

6262
## Rejected alternatives
6363

64-
- **Queue or resource-type prefixes:** duplicate explicit context, lengthen keys, require parsing, and introduce URL separators.
64+
- **Queue or domain prefixes:** duplicate explicit context, lengthen keys, require parsing, and introduce URL separators.
6565
- **ARN-like names:** solve global lookup, which current APIs neither provide nor require.
6666
- **UUIDs or a global counter:** provide global uniqueness at the cost of unnecessary encoding or coordination.
6767
- **Integer resource fields:** couple the persisted and wire contracts to the current counter representation without adding identity semantics.

‎platform/extension/counter/README.md‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Counter
22

3-
Vendor-agnostic interface for atomic sequential number generation, scoped by queue and resource type.
3+
Vendor-agnostic interface for atomic sequential number generation, scoped by queue and domain.
44

5-
SubmitQueue and Stovepipe use separate storage backends. The existing MySQL `domain` column names the resource type (`request` or `batch`); the schema and `(queue, domain)` key remain unchanged.
5+
The domain names a counter sequence (`request` or `batch`), not an application. SubmitQueue and Stovepipe use separate storage backends; the existing MySQL schema and `(queue, domain)` key remain unchanged.
66

77
## Interface
88

@@ -12,12 +12,12 @@ Resolves the Counter bound to one queue. The host wiring decides which backend s
1212

1313
### Counter
1414

15-
Generates unique, sequential values scoped to a resource type within the bound queue.
15+
Generates unique, sequential values scoped to a domain within the bound queue.
1616

17-
- **resource type**: A string key naming a sequence within the queue (max 255 characters). Each `(queue, resource type)` pair maintains its own independent sequence.
18-
- **Next**: Atomically increments and returns the next value. The first call for a new resource type returns 1. Safe for concurrent use; values are unique but ordering is not guaranteed.
17+
- **domain**: A string key naming a sequence within the queue (max 255 characters). Each `(queue, domain)` pair maintains its own independent sequence.
18+
- **Next**: Atomically increments and returns the next value. The first call for a new domain returns 1. Safe for concurrent use; values are unique but ordering is not guaranteed.
1919

20-
The resource type is a sequence name, not an ID prefix. Callers pass `"request"` or `"batch"`; the returned number is formatted as a decimal string without embedding any scope.
20+
The domain is not an ID prefix. Callers pass `"request"` or `"batch"`; the returned number is formatted as a decimal string without embedding any scope.
2121

2222
## Usage
2323

‎platform/extension/counter/counter.go‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,15 @@ type Factory interface {
3737
For(config Config) (Counter, error)
3838
}
3939

40-
// Counter provides atomic sequential number generation for a resource type within
40+
// Counter provides atomic sequential number generation for a domain within
4141
// the queue the instance is bound to.
42-
// Each call to Next returns the next value in the sequence for the specified resource type.
43-
// The value is unique within the (queue, resource type) pair.
42+
// Each call to Next returns the next value in the sequence for the specified domain.
43+
// The value is unique within the (queue, domain) pair.
44+
// A domain names a sequence, such as "request" or "batch", not an application.
4445
type Counter interface {
45-
// Next atomically increments the counter for the given resource type and returns the new value.
46-
// The first call for a new resource type returns 1.
47-
// The implementation should support resource types up to 255 bytes.
46+
// Next atomically increments the counter for the given domain and returns the new value.
47+
// The first call for a new domain returns 1.
48+
// The implementation should support domains up to 255 bytes.
4849
// The function is safe to be called concurrently and will give unique results, but the order of the values is not guaranteed.
49-
Next(ctx context.Context, resourceType string) (int64, error)
50+
Next(ctx context.Context, domain string) (int64, error)
5051
}

‎platform/extension/counter/mock/counter_mock.go‎

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎platform/extension/counter/mysql/counter.go‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,26 +37,26 @@ func NewCounter(db *sql.DB, scope tally.Scope, queue string) counter.Counter {
3737
return &mysqlCounter{db: db, scope: scope, queue: queue}
3838
}
3939

40-
// Next atomically increments the counter for the given resource type within the bound scope
40+
// Next atomically increments the counter for the given domain within the bound scope
4141
// and returns the new value.
4242
// Uses MySQL's LAST_INSERT_ID() to set the value atomically and read the incremented value.
43-
func (c *mysqlCounter) Next(ctx context.Context, resourceType string) (ret int64, retErr error) {
43+
func (c *mysqlCounter) Next(ctx context.Context, domain string) (ret int64, retErr error) {
4444
op := metrics.Begin(c.scope, "next", metrics.StorageLatencyBuckets)
4545
defer func() { op.Complete(retErr) }()
4646
result, err := c.db.ExecContext(ctx,
4747
"INSERT INTO counter (queue, domain, value) VALUES (?, ?, LAST_INSERT_ID(1)) ON DUPLICATE KEY UPDATE value = LAST_INSERT_ID(value + 1)",
48-
c.queue, resourceType,
48+
c.queue, domain,
4949
)
5050
if err != nil {
51-
return 0, fmt.Errorf("failed to increment counter for queue=%s resource_type=%s: %w", c.queue, resourceType, err)
51+
return 0, fmt.Errorf("failed to increment counter for queue=%s domain=%s: %w", c.queue, domain, err)
5252
}
5353

5454
value, err := result.LastInsertId()
5555
if err != nil {
56-
return 0, fmt.Errorf("failed to get counter value for queue=%s resource_type=%s: %w", c.queue, resourceType, err)
56+
return 0, fmt.Errorf("failed to get counter value for queue=%s domain=%s: %w", c.queue, domain, err)
5757
}
5858
if value <= 0 {
59-
return 0, fmt.Errorf("counter returned non-positive value for queue=%s resource_type=%s", c.queue, resourceType)
59+
return 0, fmt.Errorf("counter returned non-positive value for queue=%s domain=%s", c.queue, domain)
6060
}
6161

6262
return value, nil

‎stovepipe/controller/ingest.go‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ import (
3333
"go.uber.org/zap"
3434
)
3535

36-
// counterResourceTypeRequest names the per-queue sequence that mints request IDs.
37-
const counterResourceTypeRequest = "request"
36+
// counterDomainRequest names the per-queue sequence that mints request IDs.
37+
const counterDomainRequest = "request"
3838

3939
// IngestController handles ingest business logic for stovepipe: it admits a queue's newly
4040
// observed commit into the validation pipeline.
@@ -187,7 +187,7 @@ func (c *IngestController) resolveID(ctx context.Context, store storage.Storage,
187187
if err != nil {
188188
return "", fmt.Errorf("failed to resolve counter for queue=%s: %w", queue, err)
189189
}
190-
seq, err := queueCounter.Next(ctx, counterResourceTypeRequest)
190+
seq, err := queueCounter.Next(ctx, counterDomainRequest)
191191
if err != nil {
192192
return "", fmt.Errorf("failed to generate request ID for queue=%s: %w", queue, err)
193193
}

‎stovepipe/controller/ingest_test.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ func TestIngestController_Ingest(t *testing.T) {
174174
setup: func(m ingestMocks) {
175175
expectResolve(m)
176176
m.uriStore.EXPECT().GetIDByURI(gomock.Any(), testURI).Return("", storage.ErrNotFound)
177-
m.counter.EXPECT().Next(gomock.Any(), counterResourceTypeRequest).Return(int64(7), nil)
177+
m.counter.EXPECT().Next(gomock.Any(), counterDomainRequest).Return(int64(7), nil)
178178
m.uriStore.EXPECT().Create(gomock.Any(), testURI, "7").Return(nil)
179179
m.reqStore.EXPECT().Get(gomock.Any(), "7").Return(entity.Request{}, storage.ErrNotFound)
180180
m.reqStore.EXPECT().Create(gomock.Any(), gomock.Any()).Return(nil)
@@ -217,7 +217,7 @@ func TestIngestController_Ingest(t *testing.T) {
217217
setup: func(m ingestMocks) {
218218
expectResolve(m)
219219
m.uriStore.EXPECT().GetIDByURI(gomock.Any(), testURI).Return("", storage.ErrNotFound)
220-
m.counter.EXPECT().Next(gomock.Any(), counterResourceTypeRequest).Return(int64(7), nil)
220+
m.counter.EXPECT().Next(gomock.Any(), counterDomainRequest).Return(int64(7), nil)
221221
m.uriStore.EXPECT().Create(gomock.Any(), testURI, "7").Return(storage.ErrAlreadyExists)
222222
m.uriStore.EXPECT().GetIDByURI(gomock.Any(), testURI).Return("3", nil)
223223
m.reqStore.EXPECT().Get(gomock.Any(), "3").Return(acceptedRequest("3"), nil)

‎submitqueue/gateway/controller/land.go‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ func IsUnrecognizedQueue(err error) bool {
6363
return errors.As(err, &target)
6464
}
6565

66-
// counterResourceTypeRequest names the per-queue sequence that mints request IDs.
67-
const counterResourceTypeRequest = "request"
66+
// counterDomainRequest names the per-queue sequence that mints request IDs.
67+
const counterDomainRequest = "request"
6868

6969
// LandController handles land business logic for the gateway
7070
type LandController interface {
@@ -131,7 +131,7 @@ func (c *landController) Land(ctx context.Context, req entity.LandRequest) (resu
131131
if err != nil {
132132
return entity.LandResult{}, fmt.Errorf("failed to resolve counter for queue=%s: %w", queue, err)
133133
}
134-
seq, err := queueCounter.Next(ctx, counterResourceTypeRequest)
134+
seq, err := queueCounter.Next(ctx, counterDomainRequest)
135135
if err != nil {
136136
return entity.LandResult{}, fmt.Errorf("failed to generate request ID for queue=%s: %w", queue, err)
137137
}

‎submitqueue/gateway/controller/land_test.go‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,17 +157,17 @@ func TestLand_ReturnsErrorOnNonPositiveCounterValue(t *testing.T) {
157157
}
158158

159159
// TestLand_ResolvesCounterForRequestQueue pins that the queue reaches the counter
160-
// through the factory binding rather than through the resource-type string: the value is a
160+
// through the factory binding rather than through the domain name: the value is a
161161
// bare sequence name, and the sqid is the decimal counter value.
162162
func TestLand_ResolvesCounterForRequestQueue(t *testing.T) {
163-
var capturedResourceType, capturedQueue string
163+
var capturedDomain, capturedQueue string
164164

165165
ctrl := gomock.NewController(t)
166166

167167
cnt := countermock.NewMockCounter(ctrl)
168168
cnt.EXPECT().Next(gomock.Any(), gomock.Any()).DoAndReturn(
169-
func(ctx context.Context, resourceType string) (int64, error) {
170-
capturedResourceType = resourceType
169+
func(ctx context.Context, domain string) (int64, error) {
170+
capturedDomain = domain
171171
return 1, nil
172172
},
173173
)
@@ -186,7 +186,7 @@ func TestLand_ResolvesCounterForRequestQueue(t *testing.T) {
186186
result, err := controller.Land(ctx, testLandRequest("my-queue"))
187187

188188
require.NoError(t, err)
189-
assert.Equal(t, "request", capturedResourceType, "the resource type is a sequence name, not a queue-qualified key")
189+
assert.Equal(t, "request", capturedDomain, "the domain is a sequence name, not a queue-qualified key")
190190
assert.Equal(t, "my-queue", capturedQueue, "the queue reaches the counter through the factory binding")
191191
assert.Equal(t, "1", result.ID)
192192
}

‎submitqueue/orchestrator/controller/batch/batch.go‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ var _ consumer.Controller = (*Controller)(nil)
5252

5353
const opName = "process"
5454

55-
// counterResourceTypeBatch names the per-queue sequence that mints batch IDs.
56-
const counterResourceTypeBatch = "batch"
55+
// counterDomainBatch names the per-queue sequence that mints batch IDs.
56+
const counterDomainBatch = "batch"
5757

5858
// NewController creates a new batch controller for the orchestrator.
5959
func NewController(
@@ -143,7 +143,7 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er
143143
metrics.NamedCounter(c.metricsScope, opName, "counter_errors", 1)
144144
return fmt.Errorf("failed to resolve counter for queue=%s: %w", request.Queue, err)
145145
}
146-
seq, err := queueCounter.Next(ctx, counterResourceTypeBatch)
146+
seq, err := queueCounter.Next(ctx, counterDomainBatch)
147147
if err != nil {
148148
metrics.NamedCounter(c.metricsScope, opName, "counter_errors", 1)
149149
return fmt.Errorf("failed to generate batch ID for queue=%s: %w", request.Queue, err)

0 commit comments

Comments
 (0)