Keep pool running through worker failures until explicit close
This commit is contained in:
96
pool.go
96
pool.go
@@ -69,8 +69,13 @@ func (p *Pool) Submit(task func()) error {
|
||||
if n := len(p.idle); n > 0 {
|
||||
w := p.idle[n-1]
|
||||
p.idle = p.idle[:n-1]
|
||||
w.inbox <- task
|
||||
return nil
|
||||
if trySend(w, task) {
|
||||
return nil
|
||||
}
|
||||
// A worker can have published itself idle immediately before its
|
||||
// closed inbox is observed. Keep task and let the normal state
|
||||
// machine assign it again after that worker is accounted for.
|
||||
continue
|
||||
}
|
||||
if p.total < p.max {
|
||||
p.startWorkerLocked(task, true)
|
||||
@@ -85,21 +90,20 @@ func (p *Pool) Submit(task func()) error {
|
||||
}
|
||||
|
||||
// Close stops accepting tasks and waits for workers to exit. Tasks accepted
|
||||
// before Close normally finish before it returns. If runtime.Goexit or a
|
||||
// worker panic exits the last worker, queued tasks are discarded. It is safe
|
||||
// to call Close concurrently or more than once.
|
||||
// before Close finish before it returns, even if workers exit abnormally and
|
||||
// need replacement. It is safe to call Close concurrently or more than once.
|
||||
func (p *Pool) Close() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.closed = true
|
||||
for _, w := range p.idle {
|
||||
close(w.inbox)
|
||||
safeClose(w)
|
||||
}
|
||||
p.idle = nil
|
||||
p.submitCond.Broadcast()
|
||||
for p.total != 0 {
|
||||
p.closeCond.Wait()
|
||||
}
|
||||
p.mu.Unlock()
|
||||
}
|
||||
|
||||
// All pool state (worker count, idle set, queue indexes and contents, and
|
||||
@@ -117,43 +121,49 @@ func (p *Pool) startWorkerLocked(task func(), withTask bool) {
|
||||
go p.run(w)
|
||||
}
|
||||
|
||||
func trySend(w *worker, task func()) (sent bool) {
|
||||
defer func() {
|
||||
if recover() != nil {
|
||||
sent = false
|
||||
}
|
||||
}()
|
||||
w.inbox <- task
|
||||
return true
|
||||
}
|
||||
|
||||
func safeClose(w *worker) {
|
||||
defer func() { _ = recover() }()
|
||||
close(w.inbox)
|
||||
}
|
||||
|
||||
func (p *Pool) run(w *worker) {
|
||||
normalExit := false
|
||||
exitAccounted := false
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
safeLogf("worker panic: %v", recovered)
|
||||
}
|
||||
if !normalExit {
|
||||
p.workerExitedAbnormally()
|
||||
if !exitAccounted {
|
||||
p.accountWorkerExit(w, &exitAccounted)
|
||||
}
|
||||
}()
|
||||
for {
|
||||
task, ok := <-w.inbox
|
||||
if !ok {
|
||||
p.finishClosedWorker(&normalExit)
|
||||
p.accountWorkerExit(w, &exitAccounted)
|
||||
return
|
||||
}
|
||||
runTask(task)
|
||||
if p.finishTask(w, &normalExit) {
|
||||
if p.finishTask(w, &exitAccounted) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// finishClosedWorker accounts for a normal Close of an idle worker.
|
||||
func (p *Pool) finishClosedWorker(normalExit *bool) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.total--
|
||||
*normalExit = true
|
||||
p.workerExitedLocked()
|
||||
}
|
||||
|
||||
// finishTask commits the post-task transition under mu. If queue work exists,
|
||||
// it first hands off the head item and only then removes it from the ring, so
|
||||
// a closed inbox panic cannot lose an accepted task. The deferred unlock runs
|
||||
// before run's panic cleanup acquires the mutex.
|
||||
func (p *Pool) finishTask(w *worker, normalExit *bool) bool {
|
||||
func (p *Pool) finishTask(w *worker, exitAccounted *bool) bool {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if p.queueLen > 0 {
|
||||
@@ -166,7 +176,7 @@ func (p *Pool) finishTask(w *worker, normalExit *bool) bool {
|
||||
otherIdle := len(p.idle)
|
||||
if p.closed || (p.total > p.min && otherIdle > p.min/2) {
|
||||
p.total--
|
||||
*normalExit = true
|
||||
*exitAccounted = true
|
||||
if !p.closed {
|
||||
p.submitCond.Signal()
|
||||
} else {
|
||||
@@ -207,26 +217,34 @@ func (p *Pool) workerExitedLocked() {
|
||||
}
|
||||
}
|
||||
|
||||
// workerExitedAbnormally accounts for a worker terminated by runtime.Goexit.
|
||||
// If other workers remain, they retain the FIFO queue. The last worker clears
|
||||
// the queue because no worker remains to consume it; all of this is protected
|
||||
// by mu so Submit and Close observe the same state transition.
|
||||
func (p *Pool) workerExitedAbnormally() {
|
||||
// accountWorkerExit decrements a worker and, while still holding mu, starts a
|
||||
// replacement for the oldest accepted task or restores the minimum worker count.
|
||||
// This applies to panic, Goexit, and a closed inbox. It never retries the task
|
||||
// that caused the worker to exit and never discards queued tasks.
|
||||
func (p *Pool) accountWorkerExit(w *worker, exitAccounted *bool) {
|
||||
p.mu.Lock()
|
||||
p.total--
|
||||
if p.total == 0 {
|
||||
for i := range p.queue {
|
||||
p.queue[i] = nil
|
||||
defer p.mu.Unlock()
|
||||
for i, idleWorker := range p.idle {
|
||||
if idleWorker == w {
|
||||
copy(p.idle[i:], p.idle[i+1:])
|
||||
last := len(p.idle) - 1
|
||||
p.idle[last] = nil
|
||||
p.idle = p.idle[:last]
|
||||
break
|
||||
}
|
||||
p.queueHead = 0
|
||||
p.queueTail = 0
|
||||
p.queueLen = 0
|
||||
p.submitCond.Broadcast()
|
||||
} else {
|
||||
p.submitCond.Signal()
|
||||
}
|
||||
p.total--
|
||||
*exitAccounted = true
|
||||
if p.queueLen > 0 {
|
||||
task := p.dequeueLocked()
|
||||
p.startWorkerLocked(task, true)
|
||||
} else {
|
||||
if !p.closed && p.total < p.min {
|
||||
p.startWorkerLocked(nil, false)
|
||||
}
|
||||
}
|
||||
p.submitCond.Signal()
|
||||
p.workerExitedLocked()
|
||||
p.mu.Unlock()
|
||||
}
|
||||
|
||||
func safeLogf(format string, args ...any) {
|
||||
|
||||
Reference in New Issue
Block a user