@@ -41,7 +41,7 @@ type Planner struct {
4141 bc plannerclient.BatchClient
4242 prov provisioner.Provisioner
4343
44- submit chan string // buffered FIFO of pending JobIDs
44+ queue pendingQueue
4545
4646 baseCtx context.Context
4747 baseCancel context.CancelFunc
@@ -60,7 +60,7 @@ type Planner struct {
6060// it to "pending" or "provisioning". After submission, status comes from
6161// MDS.
6262//
63- // Pending → buffered in submit channel , no worker yet.
63+ // Pending → buffered in the queue , no worker yet.
6464// Provisioning → worker holds the job; Provision + CreateBatch in flight.
6565// Submitted → CreateBatch returned; MDS owns the lifecycle from here.
6666// Failed → Provision or CreateBatch errored.
@@ -79,13 +79,14 @@ const (
7979)
8080
8181type queuedJob struct {
82- req * plannerapi.EnqueueRequest
83- state jobState
84- batchID string // populated when state == jobStateSubmitted
85- err error // populated when state == jobStateFailed
86- enqueuedAt time.Time
87- failedAt time.Time // populated when state == jobStateFailed
88- canceledAt time.Time // populated when state == jobStateCanceled
82+ req * plannerapi.EnqueueRequest
83+ state jobState
84+ provisionID string // populated once Provision returns accepted
85+ batchID string // populated when state == jobStateSubmitted
86+ err error // populated when state == jobStateFailed
87+ enqueuedAt time.Time
88+ failedAt time.Time // populated when state == jobStateFailed
89+ canceledAt time.Time // populated when state == jobStateCanceled
8990}
9091
9192// terminalTime returns the timestamp at which the job transitioned into a
@@ -101,10 +102,6 @@ func terminalTime(j *queuedJob) time.Time {
101102 return time.Time {}
102103}
103104
104- // queueCapacity caps the submit channel. When full, Enqueue blocks on the
105- // caller's context.
106- const queueCapacity = 256
107-
108105// DefaultWorkerCount sizes the worker pool.
109106const DefaultWorkerCount = 8
110107
@@ -127,7 +124,7 @@ func NewPlanner(bc plannerclient.BatchClient, prov provisioner.Provisioner, work
127124 q := & Planner {
128125 bc : bc ,
129126 prov : prov ,
130- submit : make ( chan string , queueCapacity ),
127+ queue : newFIFOPendingQueue ( queueCapacity ),
131128 baseCtx : ctx ,
132129 baseCancel : cancel ,
133130 jobs : make (map [string ]* queuedJob ),
@@ -146,6 +143,7 @@ var _ plannerapi.Planner = (*Planner)(nil)
146143
147144// Close cancels in-flight work and waits for workers to exit.
148145func (q * Planner ) Close () error {
146+ q .queue .Close ()
149147 q .baseCancel ()
150148 q .wg .Wait ()
151149 return nil
@@ -154,12 +152,11 @@ func (q *Planner) Close() error {
154152func (q * Planner ) run () {
155153 defer q .wg .Done ()
156154 for {
157- select {
158- case <- q . baseCtx . Done ():
155+ jobID , err := q . queue . Pop ( q . baseCtx )
156+ if err != nil {
159157 return
160- case jobID := <- q .submit :
161- q .process (jobID )
162158 }
159+ q .process (jobID )
163160 }
164161}
165162
@@ -171,11 +168,12 @@ func (q *Planner) process(jobID string) {
171168 q .mu .Unlock ()
172169 return
173170 }
171+ // Example: a pending job is canceled before a worker picks it up, so the
172+ // worker later observes a non-pending state here and skips provisioning.
174173 if job .state != jobStatePending {
175- // Cancel raced ahead; drop without provisioning.
176174 state := job .state
177175 q .mu .Unlock ()
178- klog .Infof ("[planner] skip job_id=%q state=%d" , jobID , state )
176+ klog .Infof ("[planner] invalid state before provisioning job_id=%q state=%d" , jobID , state )
179177 return
180178 }
181179 job .state = jobStateProvisioning
@@ -193,6 +191,9 @@ func (q *Planner) process(jobID string) {
193191 q .markFailed (jobID , errors .Join (plannerapi .ErrInsufficientResources , err ))
194192 return
195193 }
194+ q .mu .Lock ()
195+ q .jobs [jobID ].provisionID = provResult .ProvisionID
196+ q .mu .Unlock ()
196197
197198 // Provision returns when the request is accepted, not when the resource
198199 // is ready. Wait for Running before submitting to MDS, which rejects
@@ -284,8 +285,9 @@ func (q *Planner) waitForProvisionReady(provisionID string) error {
284285}
285286
286287// releaseAfter performs a best-effort RM release and logs failures. The
287- // reason string ("wait failure", "CreateBatch failure", "cancel-race")
288- // appears in the log line so each call site is self-identifying.
288+ // reason string ("wait failure", "CreateBatch failure", "cancel-race",
289+ // "cancel submitted") appears in the log line so each call site is
290+ // self-identifying.
289291func (q * Planner ) releaseAfter (jobID , provisionID , reason string ) {
290292 if err := q .prov .Release (q .baseCtx , provisionID ); err != nil {
291293 klog .Warningf ("[planner] release after %s job_id=%q provision_id=%q: %v" ,
@@ -295,16 +297,15 @@ func (q *Planner) releaseAfter(jobID, provisionID, reason string) {
295297
296298func (q * Planner ) markFailed (jobID string , err error ) {
297299 q .mu .Lock ()
298- if job , ok := q .jobs [jobID ]; ok {
299- job .state = jobStateFailed
300- job .err = err
301- job .failedAt = time .Now ()
302- }
300+ job := q .jobs [jobID ]
301+ job .state = jobStateFailed
302+ job .err = err
303+ job .failedAt = time .Now ()
303304 q .mu .Unlock ()
304305 klog .Warningf ("[planner] job_id=%q failed: %v" , jobID , err )
305306}
306307
307- // Enqueue records the job, pushes it onto the worker channel , and returns
308+ // Enqueue records the job, pushes it onto the queue , and returns
308309// a placeholder batch in "pending" status.
309310func (q * Planner ) Enqueue (ctx context.Context , req * plannerapi.EnqueueRequest ) (* plannerapi.Job , error ) {
310311 if req == nil {
@@ -322,6 +323,9 @@ func (q *Planner) Enqueue(ctx context.Context, req *plannerapi.EnqueueRequest) (
322323 if q .prov == nil {
323324 return nil , fmt .Errorf ("%w: missing provisioner" , plannerapi .ErrInsufficientResources )
324325 }
326+ if err := q .baseCtx .Err (); err != nil {
327+ return nil , fmt .Errorf ("planner closed: %w" , err )
328+ }
325329
326330 now := time .Now ()
327331 q .mu .Lock ()
@@ -336,17 +340,14 @@ func (q *Planner) Enqueue(ctx context.Context, req *plannerapi.EnqueueRequest) (
336340 }
337341 q .mu .Unlock ()
338342
339- select {
340- case q .submit <- req .JobID :
341- // Happy path: a worker will dequeue and drive the entry; nothing to roll back.
342- case <- ctx .Done ():
343- // Caller gave up while q.submit was full; the bookkeeping insert is orphaned.
343+ if err := q .queue .Push (ctx , req .JobID ); err != nil {
344344 q .rollbackEnqueue (req .JobID )
345- return nil , ctx .Err ()
346- case <- q .baseCtx .Done ():
347- // Planner shutting down while q.submit was full; roll back the orphaned insert.
348- q .rollbackEnqueue (req .JobID )
349- return nil , fmt .Errorf ("planner closed: %w" , q .baseCtx .Err ())
345+ if errors .Is (err , errQueueClosed ) {
346+ // Planner shutting down while the queue was full; roll back the orphaned insert.
347+ return nil , fmt .Errorf ("planner closed: %w" , q .baseCtx .Err ())
348+ }
349+ // Caller gave up while the queue was full; the bookkeeping insert is orphaned.
350+ return nil , err
350351 }
351352
352353 klog .Infof ("[planner] enqueue job_id=%q" , req .JobID )
@@ -405,6 +406,7 @@ func (q *Planner) Cancel(ctx context.Context, jobID string) (*plannerapi.Job, er
405406 }
406407 state := job .state
407408 batchID := job .batchID
409+ provisionID := job .provisionID
408410 req := job .req
409411 enqueuedAt := job .enqueuedAt
410412 var terminalAt time.Time
@@ -428,6 +430,7 @@ func (q *Planner) Cancel(ctx context.Context, jobID string) (*plannerapi.Job, er
428430 if err != nil {
429431 return nil , err
430432 }
433+ q .releaseAfter (jobID , provisionID , "cancel submitted" )
431434 return & plannerapi.Job {JobID : jobID , Batch : batch }, nil
432435 }
433436 // Already terminal (failed/canceled) — return current view, no double-cancel side effects.
0 commit comments