@@ -13,6 +13,7 @@ import (
1313
1414 "github.com/distribution/reference"
1515 cliconfig "github.com/docker/cli/cli/config"
16+ "golang.org/x/sync/semaphore"
1617 "github.com/docker/docker/api/types/container"
1718 "github.com/docker/docker/api/types/image"
1819 "github.com/docker/docker/api/types/volume"
@@ -58,7 +59,7 @@ type Worker struct {
5859 sendChan chan []byte
5960 activeTasks map [string ]context.CancelFunc
6061 tasksMutex sync.Mutex
61- taskSemaphore chan struct {} // nil when unlimited; buffered channel used as counting semaphore
62+ taskSemaphore * semaphore. Weighted // nil when unlimited
6263 dockerClient * client.Client
6364 platform string // Docker daemon platform (e.g., "linux/amd64" or "linux/arm64")
6465}
@@ -107,9 +108,9 @@ func New(ctx context.Context, config Config) (*Worker, error) {
107108
108109 log .Debugf (ctx , "Docker daemon is reachable, platform: %s" , platform )
109110
110- var taskSemaphore chan struct {}
111+ var taskSemaphore * semaphore. Weighted
111112 if config .MaxConcurrentTasks > 0 {
112- taskSemaphore = make ( chan struct {}, config .MaxConcurrentTasks )
113+ taskSemaphore = semaphore . NewWeighted ( int64 ( config .MaxConcurrentTasks ) )
113114 log .Infof (ctx , "Concurrency limit set to %d" , config .MaxConcurrentTasks )
114115 }
115116
@@ -334,6 +335,18 @@ func (w *Worker) handleMessage(message []byte) {
334335func (w * Worker ) handleTaskAssignment (assignment * types.TaskAssignmentMessage ) {
335336 log .Infof (w .ctx , "Received task assignment: taskID=%s, title=%s" , assignment .TaskID , assignment .Task .Title )
336337
338+ // If a concurrency limit is configured, try to acquire a slot without blocking.
339+ // Failing fast lets the server reroute the task to another worker.
340+ if w .taskSemaphore != nil {
341+ if ! w .taskSemaphore .TryAcquire (1 ) {
342+ log .Warnf (w .ctx , "At max concurrency (%d), rejecting task: taskID=%s" , w .config .MaxConcurrentTasks , assignment .TaskID )
343+ if err := w .sendTaskFailed (assignment .TaskID , "worker at maximum concurrency" ); err != nil {
344+ log .Errorf (w .ctx , "Failed to send task failed message: %v" , err )
345+ }
346+ return
347+ }
348+ }
349+
337350 // It's important to update the task state to claimed as the task lifecycle treats this as a dependency to advance to further states.
338351 if err := w .sendTaskClaimed (assignment .TaskID ); err != nil {
339352 log .Errorf (w .ctx , "Failed to send task claimed message: %v" , err )
@@ -349,29 +362,17 @@ func (w *Worker) handleTaskAssignment(assignment *types.TaskAssignmentMessage) {
349362}
350363
351364func (w * Worker ) executeTask (ctx context.Context , assignment * types.TaskAssignmentMessage ) {
352- acquiredSlot := false
353365 defer func () {
354366 w .tasksMutex .Lock ()
355367 delete (w .activeTasks , assignment .TaskID )
356368 w .tasksMutex .Unlock ()
357369
358- // Release the semaphore slot if we acquired one .
359- if w .taskSemaphore != nil && acquiredSlot {
360- <- w .taskSemaphore
370+ // Release the semaphore slot if concurrency is limited .
371+ if w .taskSemaphore != nil {
372+ w .taskSemaphore . Release ( 1 )
361373 }
362374 }()
363375
364- // If a concurrency limit is configured, wait for an available slot.
365- if w .taskSemaphore != nil {
366- log .Infof (ctx , "Waiting for concurrency slot: taskID=%s" , assignment .TaskID )
367- select {
368- case w .taskSemaphore <- struct {}{}:
369- acquiredSlot = true
370- case <- ctx .Done ():
371- return
372- }
373- }
374-
375376 taskID := assignment .TaskID
376377 log .Infof (ctx , "Starting task execution: taskID=%s, title=%s" , taskID , assignment .Task .Title )
377378
0 commit comments