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 .
1919package process
2020
2121import (
@@ -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.
3940type 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).
7273func (c * Controller ) Process (ctx context.Context , delivery consumer.Delivery ) (retErr error ) {
7374 const opName = "process"
7475
@@ -105,16 +106,16 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) (r
105106 }
106107}
107108
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 .
109+ // processAccepted coalesces older heads against queue.latest_request_id, then admits
110+ // the latest head when a build slot is available .
110111func (c * Controller ) processAccepted (ctx context.Context , request entity.Request ) error {
111112 queueRow , err := c .loadQueue (ctx , request .Queue )
112113 if err != nil {
113114 return err
114115 }
115116
116117 if queueRow .LatestRequestID == "" {
117- c .logger .Infow ("latest head awaiting admit " ,
118+ c .logger .Infow ("latest head awaiting queue.latest_request_id stamp from ingest " ,
118119 "request_id" , request .ID ,
119120 "queue" , request .Queue ,
120121 "uri" , request .URI ,
@@ -144,23 +145,111 @@ func (c *Controller) processAccepted(ctx context.Context, request entity.Request
144145 }
145146
146147 if queueRow .InFlightCount >= cfg .MaxConcurrent {
148+ // TODO: re-enqueue the request via PublishAfter on the process topic with GateWaitDelayMs
147149 c .logger .Infow ("latest head awaiting build slot" ,
148150 "request_id" , request .ID ,
149151 "queue" , request .Queue ,
152+ "uri" , request .URI ,
150153 "in_flight_count" , queueRow .InFlightCount ,
151154 )
152155 return nil
153156 }
154157
155- c .logger .Infow ("latest head awaiting admit" ,
158+ return c .admitRequestToBuild (ctx , request , queueRow , cfg .MaxConcurrent )
159+ }
160+
161+ // admitRequestToBuild runs the admit workflow: claim a build slot on the queue row,
162+ // mark the request processing with build strategy, and publish the request to build.
163+ func (c * Controller ) admitRequestToBuild (ctx context.Context , request entity.Request , queueRow entity.Queue , maxConcurrent int32 ) error {
164+ for {
165+ err := c .claimBuildSlot (ctx , & queueRow )
166+ if err == nil {
167+ break
168+ }
169+ if errors .Is (err , storage .ErrVersionMismatch ) {
170+ // claimBuildSlot reloaded queueRow; another admit may have taken the last slot.
171+ if queueRow .InFlightCount >= maxConcurrent {
172+ return fmt .Errorf ("ProcessController gate closed for queue %s" , queueRow .Name )
173+ }
174+ continue
175+ }
176+ return err
177+ }
178+
179+ // TODO(build-strategy): derive from queue last_green_uri + SourceControl.IsAncestor.
180+ request .BuildStrategy = entity .BuildStrategyFull
181+ request .BaseURI = ""
182+
183+ if err := c .markProcessing (ctx , & request ); err != nil {
184+ return err
185+ }
186+
187+ // TODO(build-publish): publish BuildRequest to the build stage here.
188+
189+ c .logger .Infow ("admitted request to build" ,
156190 "request_id" , request .ID ,
157191 "queue" , request .Queue ,
158192 "uri" , request .URI ,
193+ "build_strategy" , string (request .BuildStrategy ),
159194 )
160195 return nil
161196}
162197
163- // supersedeRequest CAS-marks request accepted→superseded, retrying on version conflicts.
198+ // claimBuildSlot CAS-increments queue.in_flight_count by one. On version mismatch it
199+ // reloads queueRow and returns ErrVersionMismatch so the caller can retry.
200+ func (c * Controller ) claimBuildSlot (ctx context.Context , queueRow * entity.Queue ) error {
201+ queueStore := c .store .GetQueueStore ()
202+
203+ updated := * queueRow
204+ updated .InFlightCount = queueRow .InFlightCount + 1
205+ newVersion := queueRow .Version + 1
206+ if err := queueStore .Update (ctx , updated , queueRow .Version , newVersion ); err != nil {
207+ if errors .Is (err , storage .ErrVersionMismatch ) {
208+ got , getErr := queueStore .Get (ctx , queueRow .Name )
209+ if getErr != nil {
210+ return fmt .Errorf ("ProcessController failed to reload queue %s after version mismatch: %w" , queueRow .Name , getErr )
211+ }
212+ * queueRow = got
213+ return storage .ErrVersionMismatch
214+ }
215+ return fmt .Errorf ("ProcessController failed to claim build slot for queue %s: %w" , queueRow .Name , err )
216+ }
217+ updated .Version = newVersion
218+ * queueRow = updated
219+ return nil
220+ }
221+
222+ // markProcessing CAS-marks request accepted→processing, persisting BuildStrategy and BaseURI
223+ // already set on request by the admit workflow. Retries on version conflicts.
224+ func (c * Controller ) markProcessing (ctx context.Context , request * entity.Request ) error {
225+ reqStore := c .store .GetRequestStore ()
226+
227+ for {
228+ if request .State != entity .RequestStateAccepted {
229+ return nil
230+ }
231+
232+ updated := * request
233+ updated .State = entity .RequestStateProcessing
234+ newVersion := request .Version + 1
235+ if err := reqStore .Update (ctx , updated , request .Version , newVersion ); err != nil {
236+ if errors .Is (err , storage .ErrVersionMismatch ) {
237+ got , getErr := reqStore .Get (ctx , request .ID )
238+ if getErr != nil {
239+ return fmt .Errorf ("ProcessController failed to reload request %s after version mismatch: %w" , request .ID , getErr )
240+ }
241+ * request = got
242+ continue
243+ }
244+ return fmt .Errorf ("ProcessController failed to mark request %s processing: %w" , request .ID , err )
245+ }
246+ updated .Version = newVersion
247+ * request = updated
248+ return nil
249+ }
250+ }
251+
252+ // supersedeRequest transitions a request from accepted to superseded, retrying on version conflicts.
164253func (c * Controller ) supersedeRequest (ctx context.Context , request entity.Request ) error {
165254 reqStore := c .store .GetRequestStore ()
166255
0 commit comments