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
@@ -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 .
110112func (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.
164249func (c * Controller ) supersedeRequest (ctx context.Context , request entity.Request ) error {
165250 reqStore := c .store .GetRequestStore ()
166251
0 commit comments