monitoring: add poll watchdog to prevent worker leaks (refs #696)
This commit is contained in:
parent
6e9a62f663
commit
349f5627e5
2 changed files with 118 additions and 1 deletions
|
|
@ -41,6 +41,12 @@ import (
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultTaskTimeout = 90 * time.Second
|
||||||
|
minTaskTimeout = 30 * time.Second
|
||||||
|
maxTaskTimeout = 3 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
// PVEClientInterface defines the interface for PVE clients (both regular and cluster)
|
// PVEClientInterface defines the interface for PVE clients (both regular and cluster)
|
||||||
type PVEClientInterface interface {
|
type PVEClientInterface interface {
|
||||||
GetNodes(ctx context.Context) ([]proxmox.Node, error)
|
GetNodes(ctx context.Context) ([]proxmox.Node, error)
|
||||||
|
|
@ -520,6 +526,7 @@ type Monitor struct {
|
||||||
scheduler *AdaptiveScheduler
|
scheduler *AdaptiveScheduler
|
||||||
stalenessTracker *StalenessTracker
|
stalenessTracker *StalenessTracker
|
||||||
taskQueue *TaskQueue
|
taskQueue *TaskQueue
|
||||||
|
pollTimeout time.Duration
|
||||||
circuitBreakers map[string]*circuitBreaker
|
circuitBreakers map[string]*circuitBreaker
|
||||||
deadLetterQueue *TaskQueue
|
deadLetterQueue *TaskQueue
|
||||||
failureCounts map[string]int
|
failureCounts map[string]int
|
||||||
|
|
@ -3555,6 +3562,7 @@ func New(cfg *config.Config) (*Monitor, error) {
|
||||||
scheduler: scheduler,
|
scheduler: scheduler,
|
||||||
stalenessTracker: stalenessTracker,
|
stalenessTracker: stalenessTracker,
|
||||||
taskQueue: taskQueue,
|
taskQueue: taskQueue,
|
||||||
|
pollTimeout: derivePollTimeout(cfg),
|
||||||
deadLetterQueue: deadLetterQueue,
|
deadLetterQueue: deadLetterQueue,
|
||||||
circuitBreakers: breakers,
|
circuitBreakers: breakers,
|
||||||
failureCounts: failureCounts,
|
failureCounts: failureCounts,
|
||||||
|
|
@ -4492,6 +4500,31 @@ func (m *Monitor) taskWorker(ctx context.Context, id int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func derivePollTimeout(cfg *config.Config) time.Duration {
|
||||||
|
timeout := defaultTaskTimeout
|
||||||
|
if cfg != nil && cfg.ConnectionTimeout > 0 {
|
||||||
|
timeout = cfg.ConnectionTimeout * 2
|
||||||
|
}
|
||||||
|
if timeout < minTaskTimeout {
|
||||||
|
timeout = minTaskTimeout
|
||||||
|
}
|
||||||
|
if timeout > maxTaskTimeout {
|
||||||
|
timeout = maxTaskTimeout
|
||||||
|
}
|
||||||
|
return timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Monitor) taskExecutionTimeout(instanceType InstanceType) time.Duration {
|
||||||
|
if m == nil {
|
||||||
|
return defaultTaskTimeout
|
||||||
|
}
|
||||||
|
timeout := m.pollTimeout
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = defaultTaskTimeout
|
||||||
|
}
|
||||||
|
return timeout
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Monitor) executeScheduledTask(ctx context.Context, task ScheduledTask) {
|
func (m *Monitor) executeScheduledTask(ctx context.Context, task ScheduledTask) {
|
||||||
if !m.allowExecution(task) {
|
if !m.allowExecution(task) {
|
||||||
if logging.IsLevelEnabled(zerolog.DebugLevel) {
|
if logging.IsLevelEnabled(zerolog.DebugLevel) {
|
||||||
|
|
@ -4562,7 +4595,23 @@ func (m *Monitor) executeScheduledTask(ctx context.Context, task ScheduledTask)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
executor.Execute(ctx, pollTask)
|
taskCtx := ctx
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
timeout := m.taskExecutionTimeout(task.InstanceType)
|
||||||
|
if timeout > 0 {
|
||||||
|
taskCtx, cancel = context.WithTimeout(ctx, timeout)
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
executor.Execute(taskCtx, pollTask)
|
||||||
|
|
||||||
|
if timeout > 0 && stderrors.Is(taskCtx.Err(), context.DeadlineExceeded) {
|
||||||
|
log.Warn().
|
||||||
|
Str("instance", task.InstanceName).
|
||||||
|
Str("type", string(task.InstanceType)).
|
||||||
|
Dur("timeout", timeout).
|
||||||
|
Msg("Polling task timed out; rescheduling with fresh worker")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Monitor) rescheduleTask(task ScheduledTask) {
|
func (m *Monitor) rescheduleTask(task ScheduledTask) {
|
||||||
|
|
|
||||||
68
internal/monitoring/monitor_timeout_test.go
Normal file
68
internal/monitoring/monitor_timeout_test.go
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
package monitoring
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDerivePollTimeout(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cfg *config.Config
|
||||||
|
want time.Duration
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil config falls back to default",
|
||||||
|
cfg: nil,
|
||||||
|
want: defaultTaskTimeout,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "scales with connection timeout",
|
||||||
|
cfg: &config.Config{
|
||||||
|
ConnectionTimeout: 45 * time.Second,
|
||||||
|
},
|
||||||
|
want: 90 * time.Second,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "enforces minimum",
|
||||||
|
cfg: &config.Config{
|
||||||
|
ConnectionTimeout: 5 * time.Second,
|
||||||
|
},
|
||||||
|
want: minTaskTimeout,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "enforces maximum",
|
||||||
|
cfg: &config.Config{
|
||||||
|
ConnectionTimeout: 2 * time.Minute,
|
||||||
|
},
|
||||||
|
want: maxTaskTimeout,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := derivePollTimeout(tt.cfg)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Fatalf("derivePollTimeout() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTaskExecutionTimeout(t *testing.T) {
|
||||||
|
t.Run("uses configured timeout when set", func(t *testing.T) {
|
||||||
|
monitor := &Monitor{pollTimeout: 42 * time.Second}
|
||||||
|
if got := monitor.taskExecutionTimeout(InstanceTypePVE); got != 42*time.Second {
|
||||||
|
t.Fatalf("taskExecutionTimeout() = %v, want %v", got, 42*time.Second)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("falls back to default when unset", func(t *testing.T) {
|
||||||
|
var monitor Monitor
|
||||||
|
if got := monitor.taskExecutionTimeout(InstanceTypePVE); got != defaultTaskTimeout {
|
||||||
|
t.Fatalf("taskExecutionTimeout() = %v, want %v", got, defaultTaskTimeout)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue