towerops-agent/workerpool.go
Graham McIntire 51e9345f1f
fix: add timeout to worker pool shutdown to prevent blocking reconnection
When the WebSocket connection drops, session cleanup waits for all
worker pools to drain. SNMP/MikroTik operations against unresponsive
devices can block for 30+ seconds per worker, preventing the agent
from reconnecting during that time.

Add stopWithTimeout(5s) so pool shutdown is bounded. If workers don't
finish within the timeout, they're abandoned and reconnection proceeds
immediately.
2026-03-22 16:51:45 -05:00

75 lines
1.5 KiB
Go

package main
import (
"context"
"log/slog"
"runtime/debug"
"sync"
"time"
)
// workerPool is a fixed-size goroutine pool for executing tasks.
type workerPool struct {
tasks chan func()
wg sync.WaitGroup
once sync.Once
}
// newWorkerPool creates a pool with n worker goroutines.
func newWorkerPool(n int) *workerPool {
p := &workerPool{
tasks: make(chan func(), n*4),
}
p.wg.Add(n)
for range n {
go func() {
defer p.wg.Done()
for fn := range p.tasks {
func() {
defer func() {
if r := recover(); r != nil {
slog.Error("worker panic recovered", "error", r, "stack", string(debug.Stack()))
}
}()
fn()
}()
}
}()
}
return p
}
// submit enqueues a task. Returns false if the context is cancelled before the task can be queued.
func (p *workerPool) submit(ctx context.Context, fn func()) bool {
select {
case p.tasks <- fn:
return true
case <-ctx.Done():
return false
}
}
// stop closes the task channel and waits for all workers to finish.
func (p *workerPool) stop() {
p.once.Do(func() {
close(p.tasks)
p.wg.Wait()
})
}
// stopWithTimeout closes the task channel and waits up to timeout for workers
// to finish. Returns true if all workers completed, false if the timeout was
// reached and some workers were abandoned.
func (p *workerPool) stopWithTimeout(timeout time.Duration) bool {
done := make(chan struct{})
go func() {
p.stop()
close(done)
}()
select {
case <-done:
return true
case <-time.After(timeout):
return false
}
}