Merge pull request #31 from towerops-app/fix/pool-shutdown-timeout

fix: add timeout to worker pool shutdown to prevent blocking reconnec…
This commit is contained in:
Graham McIntire 2026-03-22 16:52:25 -05:00 committed by GitHub
commit 160055e0c0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 52 additions and 4 deletions

View file

@ -251,10 +251,19 @@ func runSession(ctx context.Context, baseURL, token string) error {
startTime := time.Now() startTime := time.Now()
defer func() { defer func() {
pools.snmp.stop() const poolShutdownTimeout = 5 * time.Second
pools.mikrotik.stop() if !pools.snmp.stopWithTimeout(poolShutdownTimeout) {
pools.ping.stop() slog.Warn("snmp pool shutdown timed out, abandoning in-flight jobs")
pools.checks.stop() }
if !pools.mikrotik.stopWithTimeout(poolShutdownTimeout) {
slog.Warn("mikrotik pool shutdown timed out, abandoning in-flight jobs")
}
if !pools.ping.stopWithTimeout(poolShutdownTimeout) {
slog.Warn("ping pool shutdown timed out, abandoning in-flight jobs")
}
if !pools.checks.stopWithTimeout(poolShutdownTimeout) {
slog.Warn("checks pool shutdown timed out, abandoning in-flight jobs")
}
close(writeCh) close(writeCh)
writerWg.Wait() writerWg.Wait()
}() }()

View file

@ -5,6 +5,7 @@ import (
"log/slog" "log/slog"
"runtime/debug" "runtime/debug"
"sync" "sync"
"time"
) )
// workerPool is a fixed-size goroutine pool for executing tasks. // workerPool is a fixed-size goroutine pool for executing tasks.
@ -55,3 +56,20 @@ func (p *workerPool) stop() {
p.wg.Wait() 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
}
}

View file

@ -57,6 +57,27 @@ func TestWorkerPool(t *testing.T) {
pool.stop() pool.stop()
pool.stop() // should not panic pool.stop() // should not panic
}) })
t.Run("stopWithTimeout returns true when workers finish in time", func(t *testing.T) {
pool := newWorkerPool(2)
pool.submit(context.Background(), func() {
time.Sleep(10 * time.Millisecond)
})
if !pool.stopWithTimeout(time.Second) {
t.Error("expected stopWithTimeout to return true")
}
})
t.Run("stopWithTimeout returns false when workers exceed timeout", func(t *testing.T) {
pool := newWorkerPool(1)
blocker := make(chan struct{})
pool.submit(context.Background(), func() { <-blocker })
if pool.stopWithTimeout(50 * time.Millisecond) {
t.Error("expected stopWithTimeout to return false")
}
close(blocker) // unblock for cleanup
})
} }
func TestWorkerPoolRecoversPanic(t *testing.T) { func TestWorkerPoolRecoversPanic(t *testing.T) {