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.
2019package process
2120
2221import (
@@ -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.
3838type 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).
7068func (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.
109197func (c * Controller ) Name () string {
110198 return "process"
0 commit comments