Skip to content

Commit 409830c

Browse files
mnoah1github-actions[bot]
authored andcommitted
feat(stovepipe): coalesce backlog in process controller
Compare accepted requests against queue.latest_request_id and mark older heads superseded. Latest heads remain accepted until admit lands in a follow-up PR.
1 parent 87edf75 commit 409830c

3 files changed

Lines changed: 276 additions & 57 deletions

File tree

stovepipe/controller/process/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ go_library(
1010
"//platform/errs:go_default_library",
1111
"//platform/metrics:go_default_library",
1212
"//stovepipe/core/messagequeue:go_default_library",
13+
"//stovepipe/entity:go_default_library",
1314
"//stovepipe/extension/storage:go_default_library",
1415
"@com_github_uber_go_tally//:go_default_library",
1516
"@org_uber_go_zap//:go_default_library",

stovepipe/controller/process/process.go

Lines changed: 109 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,10 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
// Package process holds the process-stage queue controller. It consumes the
16-
// request ids ingest publishes, reloads the Request from storage, and (in a
17-
// future change) decides the build strategy by asking SourceControl how the new
18-
// head relates to the queue's last-green URI. For now it is a thin consumer that
19-
// reloads and logs the request, establishing the stage and its wiring.
15+
// Package process holds the process-stage queue controller. It consumes request
16+
// ids from ingest, reloads the Request from storage, coalesces older heads, and
17+
// (in later changes) gates concurrency, decides build strategy, and admits
18+
// winners to build.
2019
package process
2120

2221
import (
@@ -29,12 +28,13 @@ import (
2928
"github.com/uber/submitqueue/platform/errs"
3029
"github.com/uber/submitqueue/platform/metrics"
3130
stovepipemq "github.com/uber/submitqueue/stovepipe/core/messagequeue"
31+
"github.com/uber/submitqueue/stovepipe/entity"
3232
"github.com/uber/submitqueue/stovepipe/extension/storage"
3333
"go.uber.org/zap"
3434
)
3535

3636
// Controller consumes ProcessRequest messages from the process stage, reloads the
37-
// referenced Request from storage, and logs it. Implements consumer.Controller.
37+
// referenced Request from storage, and coalesces older heads. Implements consumer.Controller.
3838
type Controller struct {
3939
logger *zap.SugaredLogger
4040
metricsScope tally.Scope
@@ -63,10 +63,8 @@ func NewController(
6363
}
6464
}
6565

66-
// Process reloads the request referenced by the delivery and logs it. Returns nil
67-
// to ack (success) or an error to nack (retry). A not-yet-visible request is
68-
// retryable: ingest persists and publishes, but a stale read may not see the row
69-
// yet, so redelivery converges.
66+
// Process reloads the request referenced by the delivery and coalesces older heads.
67+
// Returns nil to ack (success) or an error to nack (retry).
7068
func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) (retErr error) {
7169
const opName = "process"
7270

@@ -82,29 +80,119 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) (r
8280
return fmt.Errorf("failed to deserialize process request: %w", err)
8381
}
8482

85-
request, err := c.store.GetRequestStore().Get(ctx, pr.Id)
83+
request, err := c.loadRequest(ctx, pr.Id)
8684
if err != nil {
8785
metrics.NamedCounter(c.metricsScope, opName, "storage_errors", 1)
88-
if errors.Is(err, storage.ErrNotFound) {
89-
// Retryable: the request row may not be visible yet; redelivery converges.
90-
return errs.NewRetryableError(fmt.Errorf("request %s not found yet: %w", pr.Id, err))
86+
return err
87+
}
88+
89+
switch request.State {
90+
case entity.RequestStateSuperseded, entity.RequestStateProcessing:
91+
return nil
92+
case entity.RequestStateAccepted:
93+
return c.processAccepted(ctx, request)
94+
default:
95+
c.logger.Infow("ignored request in unexpected state",
96+
"request_id", request.ID,
97+
"queue", request.Queue,
98+
"state", string(request.State),
99+
)
100+
return nil
101+
}
102+
}
103+
104+
// processAccepted coalesces older heads against queue.latest_request_id. The latest
105+
// head is left in accepted until admit and the concurrency gate land in later PRs.
106+
func (c *Controller) processAccepted(ctx context.Context, request entity.Request) error {
107+
queueRow, err := c.loadQueue(ctx, request.Queue)
108+
if err != nil {
109+
return err
110+
}
111+
112+
if queueRow.LatestRequestID == "" {
113+
c.logger.Infow("latest head awaiting admit",
114+
"request_id", request.ID,
115+
"queue", request.Queue,
116+
"uri", request.URI,
117+
)
118+
return nil
119+
}
120+
121+
cmp, err := entity.CompareRequestID(request.Queue, request.ID, queueRow.LatestRequestID)
122+
if err != nil {
123+
return fmt.Errorf("ProcessController failed to compare request ids for queue %s: %w", request.Queue, err)
124+
}
125+
if cmp < 0 {
126+
if err := c.supersedeRequest(ctx, request); err != nil {
127+
return err
91128
}
92-
return fmt.Errorf("failed to load request %s: %w", pr.Id, err)
129+
c.logger.Infow("superseded request for newer head",
130+
"request_id", request.ID,
131+
"queue", request.Queue,
132+
"latest_request_id", queueRow.LatestRequestID,
133+
)
134+
return nil
93135
}
94136

95-
c.logger.Infow("processing request",
137+
c.logger.Infow("latest head awaiting admit",
96138
"request_id", request.ID,
97139
"queue", request.Queue,
98140
"uri", request.URI,
99-
"state", string(request.State),
100-
"version", request.Version,
101-
"attempt", delivery.Attempt(),
102-
"partition_key", msg.PartitionKey,
103141
)
104-
105142
return nil
106143
}
107144

145+
// supersedeRequest CAS-marks request accepted→superseded, retrying on version conflicts.
146+
func (c *Controller) supersedeRequest(ctx context.Context, request entity.Request) error {
147+
reqStore := c.store.GetRequestStore()
148+
149+
for {
150+
if request.State != entity.RequestStateAccepted {
151+
return nil
152+
}
153+
154+
updated := request
155+
updated.State = entity.RequestStateSuperseded
156+
newVersion := request.Version + 1
157+
if err := reqStore.Update(ctx, updated, request.Version, newVersion); err != nil {
158+
if errors.Is(err, storage.ErrVersionMismatch) {
159+
got, getErr := reqStore.Get(ctx, request.ID)
160+
if getErr != nil {
161+
return fmt.Errorf("ProcessController failed to reload request %s after version mismatch: %w", request.ID, getErr)
162+
}
163+
request = got
164+
continue
165+
}
166+
return fmt.Errorf("ProcessController failed to supersede request %s: %w", request.ID, err)
167+
}
168+
return nil
169+
}
170+
}
171+
172+
// loadRequest returns the request for id. A not-yet-visible row is retryable.
173+
func (c *Controller) loadRequest(ctx context.Context, id string) (entity.Request, error) {
174+
got, err := c.store.GetRequestStore().Get(ctx, id)
175+
if err == nil {
176+
return got, nil
177+
}
178+
if errors.Is(err, storage.ErrNotFound) {
179+
return entity.Request{}, errs.NewRetryableError(fmt.Errorf("request %s not found yet: %w", id, err))
180+
}
181+
return entity.Request{}, fmt.Errorf("ProcessController failed to load request %s: %w", id, err)
182+
}
183+
184+
// loadQueue returns the queue row for name. A not-yet-visible row is retryable.
185+
func (c *Controller) loadQueue(ctx context.Context, name string) (entity.Queue, error) {
186+
got, err := c.store.GetQueueStore().Get(ctx, name)
187+
if err == nil {
188+
return got, nil
189+
}
190+
if errors.Is(err, storage.ErrNotFound) {
191+
return entity.Queue{}, errs.NewRetryableError(fmt.Errorf("queue %s not found yet: %w", name, err))
192+
}
193+
return entity.Queue{}, fmt.Errorf("ProcessController failed to load queue %s: %w", name, err)
194+
}
195+
108196
// Name returns the controller name for logging and metrics.
109197
func (c *Controller) Name() string {
110198
return "process"

0 commit comments

Comments
 (0)