Files
pulse/internal/monitoring/monitor_polling_schedule.go
T
rcourtman c70431caaf Honor configured availability poll interval in the scheduler
An availability target's configured poll interval only seeded the
adaptive scheduler: BuildPlan derived every instance's cadence from the
global adaptive bounds, and a failing probe raised the staleness score
and error penalty, collapsing the probe interval toward the global
5-second minimum. With interval 120s and failure threshold 4 the alert
was promised after ~8 minutes of downtime but fired within the first
minute because the four consecutive failures accumulated at the
collapsed cadence (#1582).

Availability checks promise pollInterval x failureThreshold as the
detection window, so the cadence is a user contract, not a scheduling
hint. Add a FixedIntervalPollProvider extension that pins an instance
to its configured interval, implement it for availability targets, and
bypass adaptive selection wherever the next run is computed (plan
building, rescheduling, and the non-adaptive fallback paths).
2026-07-16 09:26:53 +01:00

100 lines
2.2 KiB
Go

package monitoring
import (
"sort"
"time"
)
func (m *Monitor) describeInstancesForScheduler() []InstanceDescriptor {
providers := m.pollProviderSnapshotWithBuiltins()
if len(providers) == 0 {
return nil
}
total := 0
for _, provider := range providers {
if provider == nil {
continue
}
total += len(provider.ListInstances(m))
}
if total == 0 {
return nil
}
descriptors := make([]InstanceDescriptor, 0, total)
for _, provider := range providers {
if provider == nil {
continue
}
names := append([]string(nil), provider.ListInstances(m)...)
if len(names) == 0 {
continue
}
sort.Strings(names)
providerType := provider.Type()
fixedProvider, _ := provider.(FixedIntervalPollProvider)
for _, name := range names {
desc := InstanceDescriptor{
Name: name,
Type: providerType,
}
if fixedProvider != nil {
desc.FixedInterval = fixedProvider.FixedInstanceInterval(m, name)
}
if m.scheduler != nil {
if last, ok := m.scheduler.LastScheduled(providerType, name); ok {
desc.LastScheduled = last.NextRun
desc.LastInterval = last.Interval
}
}
if m.stalenessTracker != nil {
if snap, ok := m.stalenessTracker.snapshot(providerType, name); ok {
desc.LastSuccess = snap.LastSuccess
desc.LastFailure = snap.LastError
desc.Metadata = TaskMetadata{ChangeHash: snap.ChangeHash}
}
}
descriptors = append(descriptors, desc)
}
}
return descriptors
}
func (m *Monitor) buildScheduledTasks(now time.Time) []ScheduledTask {
descriptors := m.describeInstancesForScheduler()
if len(descriptors) == 0 {
return nil
}
queueDepth := 0
if m.taskQueue != nil {
queueDepth = m.taskQueue.Size()
}
if m.scheduler == nil {
tasks := make([]ScheduledTask, 0, len(descriptors))
for _, desc := range descriptors {
interval := desc.FixedInterval
if interval <= 0 {
interval = m.baseIntervalForInstanceType(desc.Type)
}
if interval <= 0 {
interval = DefaultSchedulerConfig().BaseInterval
}
tasks = append(tasks, ScheduledTask{
InstanceName: desc.Name,
InstanceType: desc.Type,
NextRun: now,
Interval: interval,
})
}
return tasks
}
return m.scheduler.BuildPlan(now, descriptors, queueDepth)
}