Skip to content

Commit 44606ba

Browse files
mnoah1github-actions[bot]
authored andcommitted
feat(stovepipe): admit latest head through processing transition
Increment in_flight_count and transition accepted→processing with a cold-start full build strategy when the concurrency gate is open. Build queue publish lands in a follow-up PR.
1 parent 98cef3f commit 44606ba

2 files changed

Lines changed: 117 additions & 16 deletions

File tree

stovepipe/controller/process/process.go

Lines changed: 96 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414

1515
// Package process holds the process-stage queue controller. It consumes request
1616
// 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.
17+
// admits the latest head when a build slot is open. Build queue publish lands in
18+
// a follow-up PR.
1919
package process
2020

2121
import (
@@ -35,7 +35,8 @@ import (
3535
)
3636

3737
// Controller consumes ProcessRequest messages from the process stage, reloads the
38-
// referenced Request from storage, and coalesces older heads. Implements consumer.Controller.
38+
// referenced Request from storage, coalesces older heads, and admits the latest when
39+
// a slot is open. Implements consumer.Controller.
3940
type Controller struct {
4041
logger *zap.SugaredLogger
4142
metricsScope tally.Scope
@@ -67,8 +68,8 @@ func NewController(
6768
}
6869
}
6970

70-
// Process reloads the request referenced by the delivery and coalesces older heads.
71-
// Returns nil to ack (success) or an error to nack (retry).
71+
// Process reloads the request referenced by the delivery, coalesces older heads,
72+
// and admits the latest when a slot is open. Returns nil to ack (success) or an error to nack (retry).
7273
func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) (retErr error) {
7374
const opName = "process"
7475

@@ -92,6 +93,7 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) (r
9293

9394
switch request.State {
9495
case entity.RequestStateSuperseded, entity.RequestStateProcessing:
96+
// Processing republish to build lands in a follow-up PR.
9597
return nil
9698
case entity.RequestStateAccepted:
9799
return c.processAccepted(ctx, request)
@@ -105,16 +107,16 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) (r
105107
}
106108
}
107109

108-
// processAccepted coalesces older heads against queue.latest_request_id, then resolves
109-
// per-queue config for the concurrency gate. Admit lands in a follow-up PR.
110+
// processAccepted coalesces older heads against queue.latest_request_id, then admits
111+
// the latest head when a build slot is available.
110112
func (c *Controller) processAccepted(ctx context.Context, request entity.Request) error {
111113
queueRow, err := c.loadQueue(ctx, request.Queue)
112114
if err != nil {
113115
return err
114116
}
115117

116118
if queueRow.LatestRequestID == "" {
117-
c.logger.Infow("latest head awaiting admit",
119+
c.logger.Infow("latest head awaiting ingest pointer",
118120
"request_id", request.ID,
119121
"queue", request.Queue,
120122
"uri", request.URI,
@@ -152,15 +154,98 @@ func (c *Controller) processAccepted(ctx context.Context, request entity.Request
152154
return nil
153155
}
154156

155-
c.logger.Infow("latest head awaiting admit",
157+
return c.admitRequest(ctx, request, queueRow, cfg.MaxConcurrent)
158+
}
159+
160+
// admitRequest admits the latest head: cold-start full build, increment in_flight_count,
161+
// and transition accepted→processing.
162+
func (c *Controller) admitRequest(ctx context.Context, request entity.Request, queueRow entity.Queue, maxConcurrent int32) error {
163+
if err := c.incrementInFlightCount(ctx, &queueRow, maxConcurrent); err != nil {
164+
return err
165+
}
166+
167+
strategy := entity.BuildStrategyFull
168+
if err := c.transitionToProcessing(ctx, &request, strategy, ""); err != nil {
169+
return err
170+
}
171+
172+
// TODO(build-publish): publish BuildRequest to the build stage here.
173+
174+
c.logger.Infow("admitted request",
156175
"request_id", request.ID,
157176
"queue", request.Queue,
158-
"uri", request.URI,
177+
"build_strategy", string(strategy),
159178
)
160179
return nil
161180
}
162181

163-
// supersedeRequest CAS-marks request accepted→superseded, retrying on version conflicts.
182+
// incrementInFlightCount CAS-increments queue.in_flight_count, retrying on version conflicts.
183+
func (c *Controller) incrementInFlightCount(ctx context.Context, queueRow *entity.Queue, maxConcurrent int32) error {
184+
queueStore := c.store.GetQueueStore()
185+
186+
for {
187+
if queueRow.InFlightCount >= maxConcurrent {
188+
return fmt.Errorf("ProcessController gate closed for queue %s", queueRow.Name)
189+
}
190+
191+
updated := *queueRow
192+
updated.InFlightCount = queueRow.InFlightCount + 1
193+
newVersion := queueRow.Version + 1
194+
if err := queueStore.Update(ctx, updated, queueRow.Version, newVersion); err != nil {
195+
if errors.Is(err, storage.ErrVersionMismatch) {
196+
got, getErr := queueStore.Get(ctx, queueRow.Name)
197+
if getErr != nil {
198+
return fmt.Errorf("ProcessController failed to reload queue %s after version mismatch: %w", queueRow.Name, getErr)
199+
}
200+
*queueRow = got
201+
continue
202+
}
203+
return fmt.Errorf("ProcessController failed to increment in_flight_count for queue %s: %w", queueRow.Name, err)
204+
}
205+
*queueRow = updated
206+
queueRow.Version = newVersion
207+
return nil
208+
}
209+
}
210+
211+
// transitionToProcessing CAS-marks request accepted→processing with strategy fields,
212+
// retrying on version conflicts.
213+
func (c *Controller) transitionToProcessing(
214+
ctx context.Context,
215+
request *entity.Request,
216+
strategy entity.BuildStrategy,
217+
baseURI string,
218+
) error {
219+
reqStore := c.store.GetRequestStore()
220+
221+
for {
222+
if request.State != entity.RequestStateAccepted {
223+
return nil
224+
}
225+
226+
updated := *request
227+
updated.State = entity.RequestStateProcessing
228+
updated.BuildStrategy = strategy
229+
updated.BaseURI = baseURI
230+
newVersion := request.Version + 1
231+
if err := reqStore.Update(ctx, updated, request.Version, newVersion); err != nil {
232+
if errors.Is(err, storage.ErrVersionMismatch) {
233+
got, getErr := reqStore.Get(ctx, request.ID)
234+
if getErr != nil {
235+
return fmt.Errorf("ProcessController failed to reload request %s after version mismatch: %w", request.ID, getErr)
236+
}
237+
*request = got
238+
continue
239+
}
240+
return fmt.Errorf("ProcessController failed to transition request %s to processing: %w", request.ID, err)
241+
}
242+
*request = updated
243+
request.Version = newVersion
244+
return nil
245+
}
246+
}
247+
248+
// supersedeRequest transitions a request from accepted to superseded, retrying on version conflicts.
164249
func (c *Controller) supersedeRequest(ctx context.Context, request entity.Request) error {
165250
reqStore := c.store.GetRequestStore()
166251

stovepipe/controller/process/process_test.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import (
2525
entityqueue "github.com/uber/submitqueue/platform/base/messagequeue"
2626
"github.com/uber/submitqueue/platform/consumer"
2727
"github.com/uber/submitqueue/platform/errs"
28-
queuemock "github.com/uber/submitqueue/platform/extension/messagequeue/mock"
28+
mqmock "github.com/uber/submitqueue/platform/extension/messagequeue/mock"
2929
stovepipemq "github.com/uber/submitqueue/stovepipe/core/messagequeue"
3030
"github.com/uber/submitqueue/stovepipe/entity"
3131
queueconfigdefault "github.com/uber/submitqueue/stovepipe/extension/queueconfig/default"
@@ -64,7 +64,7 @@ func newController(t *testing.T, ctrl *gomock.Controller) (*Controller, processM
6464

6565
func delivery(t *testing.T, ctrl *gomock.Controller, payload []byte) consumer.Delivery {
6666
t.Helper()
67-
d := queuemock.NewMockDelivery(ctrl)
67+
d := mqmock.NewMockDelivery(ctrl)
6868
d.EXPECT().Message().Return(entityqueue.NewMessage(testID, payload, testQueue, nil)).AnyTimes()
6969
d.EXPECT().Attempt().Return(1).AnyTimes()
7070
return d
@@ -87,6 +87,21 @@ func acceptedRequest(id string) entity.Request {
8787
}
8888
}
8989

90+
func expectAdmit(m processMocks, id string) {
91+
updatedQueue := entity.Queue{
92+
Name: testQueue,
93+
LatestRequestID: id,
94+
InFlightCount: 1,
95+
Version: 1,
96+
}
97+
m.queueStore.EXPECT().Update(gomock.Any(), updatedQueue, int32(1), int32(2)).Return(nil)
98+
99+
updatedReq := acceptedRequest(id)
100+
updatedReq.State = entity.RequestStateProcessing
101+
updatedReq.BuildStrategy = entity.BuildStrategyFull
102+
m.reqStore.EXPECT().Update(gomock.Any(), updatedReq, int32(1), int32(2)).Return(nil)
103+
}
104+
90105
func TestProcess(t *testing.T) {
91106
tests := []struct {
92107
name string
@@ -104,26 +119,27 @@ func TestProcess(t *testing.T) {
104119
},
105120
},
106121
{
107-
name: "processing is no-op until republish lands",
122+
name: "processing is no-op until build publish lands",
108123
setup: func(m processMocks) {
109124
m.reqStore.EXPECT().Get(gomock.Any(), testID).Return(entity.Request{
110125
ID: testID, Queue: testQueue, State: entity.RequestStateProcessing, Version: 2,
111126
}, nil)
112127
},
113128
},
114129
{
115-
name: "latest accepted head awaits admit",
130+
name: "latest accepted head is admitted",
116131
setup: func(m processMocks) {
117132
m.reqStore.EXPECT().Get(gomock.Any(), testID).Return(acceptedRequest(testID), nil)
118133
m.queueStore.EXPECT().Get(gomock.Any(), testQueue).Return(entity.Queue{
119134
Name: testQueue,
120135
LatestRequestID: testID,
121136
Version: 1,
122137
}, nil)
138+
expectAdmit(m, testID)
123139
},
124140
},
125141
{
126-
name: "accepted with empty latest pointer awaits admit",
142+
name: "accepted with empty latest pointer awaits ingest stamp",
127143
setup: func(m processMocks) {
128144
m.reqStore.EXPECT().Get(gomock.Any(), testID).Return(acceptedRequest(testID), nil)
129145
m.queueStore.EXPECT().Get(gomock.Any(), testQueue).Return(entity.Queue{

0 commit comments

Comments
 (0)