Skip to content

Commit 5fb143c

Browse files
committed
feat(requestlog): persist queue and change metadata
Summary: Request logs now carry queue and change metadata so downstream read models can build queue-scoped views without reloading the original request. Existing SQID parsing remains as a fallback for older or minimal log producers. Test Plan: ✅ `make fmt && make build && make test && make check-mocks && make e2e-test`
1 parent 3a6057c commit 5fb143c

7 files changed

Lines changed: 172 additions & 7 deletions

File tree

submitqueue/entity/request_log.go

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package entity
1616

1717
import (
1818
"encoding/json"
19+
"strings"
1920
"time"
2021
)
2122

@@ -96,6 +97,10 @@ const (
9697
type RequestLog struct {
9798
// RequestID is the ID of the request this log entry belongs to. References entity.Request.ID.
9899
RequestID string `json:"request_id"`
100+
// Queue is the queue this request belongs to. New log producers should set it explicitly.
101+
Queue string `json:"queue"`
102+
// ChangeURIs are the original change URIs submitted with the request. They are populated by gateway-originated accepted logs.
103+
ChangeURIs []string `json:"change_uris"`
99104
// TimestampMs is the time this log entry was created, in milliseconds since Unix epoch.
100105
TimestampMs int64 `json:"timestamp_ms"`
101106
// Status is the request status at the time this log entry was created. It may contain requests states from the state machine and also display-friendly intermediate statuses.
@@ -117,11 +122,24 @@ type RequestLog struct {
117122
// lastError is the last error message associated with the status at the time of this log entry, empty string if no error.
118123
// metadata is a set of key-value pairs providing additional context for this log entry. Not constrained to any specific format or schema, used for display or debugging purposes.
119124
func NewRequestLog(requestID string, status RequestStatus, requestVersion int32, lastError string, metadata map[string]string) RequestLog {
125+
return NewRequestLogWithDetails(requestID, QueueFromRequestID(requestID), nil, status, requestVersion, lastError, metadata)
126+
}
127+
128+
// NewRequestLogWithDetails creates a new RequestLog with queue and change information for list summaries.
129+
func NewRequestLogWithDetails(requestID, queue string, changeURIs []string, status RequestStatus, requestVersion int32, lastError string, metadata map[string]string) RequestLog {
120130
if metadata == nil {
121131
metadata = make(map[string]string)
122132
}
133+
if queue == "" {
134+
queue = QueueFromRequestID(requestID)
135+
}
136+
if changeURIs == nil {
137+
changeURIs = []string{}
138+
}
123139
return RequestLog{
124140
RequestID: requestID,
141+
Queue: queue,
142+
ChangeURIs: changeURIs,
125143
TimestampMs: time.Now().UnixMilli(),
126144
Status: status,
127145
RequestVersion: requestVersion,
@@ -135,8 +153,9 @@ func (r RequestLog) ToBytes() ([]byte, error) {
135153
return json.Marshal(r)
136154
}
137155

138-
// RequestLogFromBytes deserializes a RequestLog from JSON bytes.
139-
// If metadata is absent from the JSON, it will be initialized as an empty map.
156+
// RequestLogFromBytes deserializes a RequestLog from JSON bytes. If metadata or change URIs are
157+
// absent from the JSON, they will be initialized as empty collections. If queue is absent, it will
158+
// be derived from the request ID when possible.
140159
func RequestLogFromBytes(data []byte) (RequestLog, error) {
141160
var log RequestLog
142161
err := json.Unmarshal(data, &log)
@@ -146,5 +165,53 @@ func RequestLogFromBytes(data []byte) (RequestLog, error) {
146165
if log.Metadata == nil {
147166
log.Metadata = make(map[string]string)
148167
}
168+
if log.ChangeURIs == nil {
169+
log.ChangeURIs = []string{}
170+
}
171+
if log.Queue == "" {
172+
log.Queue = QueueFromRequestID(log.RequestID)
173+
}
149174
return log, nil
150175
}
176+
177+
// QueueFromRequestID extracts the queue from the current "<queue>/<number>" request ID format.
178+
// It strips only a trailing numeric path segment so queue names may contain slashes.
179+
func QueueFromRequestID(requestID string) string {
180+
idx := strings.LastIndex(requestID, "/")
181+
if idx <= 0 || idx == len(requestID)-1 {
182+
return ""
183+
}
184+
for _, r := range requestID[idx+1:] {
185+
if r < '0' || r > '9' {
186+
return ""
187+
}
188+
}
189+
return requestID[:idx]
190+
}
191+
192+
// IsKnownRequestStatus returns true if status is a public request status emitted by SubmitQueue.
193+
func IsKnownRequestStatus(status RequestStatus) bool {
194+
switch status {
195+
case RequestStatusAccepted,
196+
RequestStatusStarted,
197+
RequestStatusValidating,
198+
RequestStatusValidated,
199+
RequestStatusBatching,
200+
RequestStatusBatched,
201+
RequestStatusScored,
202+
RequestStatusSpeculating,
203+
RequestStatusSpeculated,
204+
RequestStatusBuilding,
205+
RequestStatusBuilt,
206+
RequestStatusWaitingPath,
207+
RequestStatusLanding,
208+
RequestStatusProcessing,
209+
RequestStatusLanded,
210+
RequestStatusError,
211+
RequestStatusCancelling,
212+
RequestStatusCancelled:
213+
return true
214+
default:
215+
return false
216+
}
217+
}

submitqueue/entity/request_log_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ func TestRequestLog_ToBytes(t *testing.T) {
5151
func TestRequestLogFromBytes(t *testing.T) {
5252
original := RequestLog{
5353
RequestID: "my-queue/999",
54+
Queue: "my-queue",
55+
ChangeURIs: []string{"github://uber/repo/pull/1/abcdef"},
5456
TimestampMs: 1709568000000,
5557
Status: RequestStatusProcessing,
5658
RequestVersion: 3,
@@ -65,6 +67,8 @@ func TestRequestLogFromBytes(t *testing.T) {
6567
require.NoError(t, err)
6668

6769
assert.Equal(t, original.RequestID, deserialized.RequestID)
70+
assert.Equal(t, original.Queue, deserialized.Queue)
71+
assert.Equal(t, original.ChangeURIs, deserialized.ChangeURIs)
6872
assert.Equal(t, original.TimestampMs, deserialized.TimestampMs)
6973
assert.Equal(t, original.Status, deserialized.Status)
7074
assert.Equal(t, original.RequestVersion, deserialized.RequestVersion)
@@ -103,6 +107,8 @@ func TestRequestLog_SerializationRoundTrip(t *testing.T) {
103107
name: "with all fields populated",
104108
log: RequestLog{
105109
RequestID: "queue1/100",
110+
Queue: "queue1",
111+
ChangeURIs: []string{},
106112
TimestampMs: 1709568000000,
107113
Status: RequestStatusLanded,
108114
RequestVersion: 5,
@@ -114,6 +120,8 @@ func TestRequestLog_SerializationRoundTrip(t *testing.T) {
114120
name: "with error",
115121
log: RequestLog{
116122
RequestID: "queue2/200",
123+
Queue: "queue2",
124+
ChangeURIs: []string{},
117125
TimestampMs: 1709568001000,
118126
Status: RequestStatusError,
119127
RequestVersion: 2,
@@ -125,6 +133,8 @@ func TestRequestLog_SerializationRoundTrip(t *testing.T) {
125133
name: "with zero version",
126134
log: RequestLog{
127135
RequestID: "queue3/300",
136+
Queue: "queue3",
137+
ChangeURIs: []string{},
128138
TimestampMs: 1709568002000,
129139
Status: RequestStatusStarted,
130140
RequestVersion: 0,
@@ -146,3 +156,10 @@ func TestRequestLog_SerializationRoundTrip(t *testing.T) {
146156
})
147157
}
148158
}
159+
160+
func TestQueueFromRequestID(t *testing.T) {
161+
assert.Equal(t, "queue", QueueFromRequestID("queue/100"))
162+
assert.Equal(t, "org/queue", QueueFromRequestID("org/queue/100"))
163+
assert.Empty(t, QueueFromRequestID("queue/not-a-number"))
164+
assert.Empty(t, QueueFromRequestID("queue"))
165+
}

submitqueue/extension/storage/mysql/BUILD.bazel

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
load("@rules_go//go:def.bzl", "go_library")
1+
load("@rules_go//go:def.bzl", "go_library", "go_test")
22

33
go_library(
44
name = "mysql",
@@ -22,3 +22,14 @@ go_library(
2222
"@com_github_uber_go_tally//:tally",
2323
],
2424
)
25+
26+
go_test(
27+
name = "mysql_test",
28+
srcs = ["request_log_store_test.go"],
29+
embed = [":mysql"],
30+
deps = [
31+
"//submitqueue/entity",
32+
"@com_github_stretchr_testify//require",
33+
"@com_github_uber_go_tally//:tally",
34+
],
35+
)

submitqueue/extension/storage/mysql/request_log_store.go

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,24 @@ func (r *requestLogStore) Insert(ctx context.Context, log entity.RequestLog) (re
4646
op := metrics.Begin(r.scope, "insert")
4747
defer func() { op.Complete(retErr) }()
4848

49+
if log.Queue == "" {
50+
log.Queue = entity.QueueFromRequestID(log.RequestID)
51+
}
52+
if log.Queue == "" {
53+
return fmt.Errorf("request log insert requires queue for request_id=%s", log.RequestID)
54+
}
55+
if log.ChangeURIs == nil {
56+
log.ChangeURIs = []string{}
57+
}
58+
if log.Metadata == nil {
59+
log.Metadata = map[string]string{}
60+
}
61+
62+
changeURIsJSON, err := json.Marshal(log.ChangeURIs)
63+
if err != nil {
64+
return fmt.Errorf("failed to marshal change URIs for request log request_id=%s: %w", log.RequestID, err)
65+
}
66+
4967
metadataJSON, err := json.Marshal(log.Metadata)
5068
if err != nil {
5169
return fmt.Errorf("failed to marshal metadata for request log request_id=%s: %w", log.RequestID, err)
@@ -57,8 +75,8 @@ func (r *requestLogStore) Insert(ctx context.Context, log entity.RequestLog) (re
5775
salt := rand.Int64()
5876

5977
_, err = r.db.ExecContext(ctx,
60-
"INSERT INTO request_log (request_id, timestamp_ms, salt, status, request_version, last_error, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)",
61-
log.RequestID, log.TimestampMs, salt, log.Status, log.RequestVersion, log.LastError, metadataJSON,
78+
"INSERT INTO request_log (request_id, queue, change_uri, timestamp_ms, salt, status, request_version, last_error, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
79+
log.RequestID, log.Queue, changeURIsJSON, log.TimestampMs, salt, log.Status, log.RequestVersion, log.LastError, metadataJSON,
6280
)
6381
if err != nil {
6482
return fmt.Errorf("failed to insert request log for request_id=%s timestamp_ms=%d: %w", log.RequestID, log.TimestampMs, err)
@@ -75,7 +93,7 @@ func (r *requestLogStore) List(ctx context.Context, requestID string) (ret []ent
7593
defer func() { op.Complete(retErr) }()
7694

7795
rows, err := r.db.QueryContext(ctx,
78-
"SELECT request_id, timestamp_ms, status, request_version, last_error, metadata FROM request_log WHERE request_id = ? ORDER BY timestamp_ms ASC, salt ASC",
96+
"SELECT request_id, queue, change_uri, timestamp_ms, status, request_version, last_error, metadata FROM request_log WHERE request_id = ? ORDER BY timestamp_ms ASC, salt ASC",
7997
requestID,
8098
)
8199
if err != nil {
@@ -86,16 +104,27 @@ func (r *requestLogStore) List(ctx context.Context, requestID string) (ret []ent
86104
var logs []entity.RequestLog
87105
for rows.Next() {
88106
var log entity.RequestLog
107+
var changeURIsJSON []byte
89108
var metadataJSON []byte
90109

91-
err := rows.Scan(&log.RequestID, &log.TimestampMs, &log.Status, &log.RequestVersion, &log.LastError, &metadataJSON)
110+
err := rows.Scan(&log.RequestID, &log.Queue, &changeURIsJSON, &log.TimestampMs, &log.Status, &log.RequestVersion, &log.LastError, &metadataJSON)
92111
if err != nil {
93112
return nil, fmt.Errorf("failed to scan request log row for request_id=%s: %w", requestID, err)
94113
}
95114

115+
if err := json.Unmarshal(changeURIsJSON, &log.ChangeURIs); err != nil {
116+
return nil, fmt.Errorf("failed to unmarshal change URIs for request log request_id=%s: %w", requestID, err)
117+
}
118+
if log.ChangeURIs == nil {
119+
log.ChangeURIs = []string{}
120+
}
121+
96122
if err := json.Unmarshal(metadataJSON, &log.Metadata); err != nil {
97123
return nil, fmt.Errorf("failed to unmarshal metadata for request log request_id=%s: %w", requestID, err)
98124
}
125+
if log.Metadata == nil {
126+
log.Metadata = map[string]string{}
127+
}
99128

100129
logs = append(logs, log)
101130
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) 2025 Uber Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package mysql
16+
17+
import (
18+
"context"
19+
"testing"
20+
21+
"github.com/stretchr/testify/require"
22+
"github.com/uber-go/tally"
23+
"github.com/uber/submitqueue/submitqueue/entity"
24+
)
25+
26+
func TestRequestLogStoreInsert_RejectsMissingQueue(t *testing.T) {
27+
store := NewRequestLogStore(nil, tally.NoopScope)
28+
29+
err := store.Insert(context.Background(), entity.RequestLog{
30+
RequestID: "not-an-sqid",
31+
TimestampMs: 100,
32+
Status: entity.RequestStatusAccepted,
33+
})
34+
35+
require.Error(t, err)
36+
}

submitqueue/extension/storage/mysql/schema/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@ As the `batch` table grows, the secondary index will grow with it, increasing st
2222

2323
The `change` table records per-URI claims by in-flight requests. `request_id` is part of the primary key so that concurrent claims on the same URI by different requests coexist as distinct rows — a same-request retry collides on the PK and is a no-op (`INSERT IGNORE`), while a different-request claim is a new row that `GetByURI` surfaces for overlap detection. `queue` leads the key so queue-scoped lookups are primary-key-prefix scans and the table is shardable by queue.
2424

25+
## request_log table
26+
27+
`request_log` stores immutable request status records. Schema application uses `CREATE TABLE IF NOT EXISTS` files, so existing local databases created before the `queue` and `change_uri` columns were added must be recreated or manually altered before running newer binaries.

submitqueue/extension/storage/mysql/schema/request_log.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
CREATE TABLE IF NOT EXISTS request_log (
22
request_id VARCHAR(255) NOT NULL,
3+
queue VARCHAR(255) NOT NULL,
4+
change_uri JSON NOT NULL,
35
timestamp_ms BIGINT NOT NULL,
46
salt BIGINT NOT NULL,
57
status VARCHAR(64) NOT NULL,

0 commit comments

Comments
 (0)