Files
pulse/internal/monitoring/monitor_runtime_settings.go
T
courtmanr@gmail.com 893aa0b2cd Clear temperature SSH failure backoff on system-settings save (#1638)
The reset added in b45bd66b9 only fired when the temperature SSH key
file on disk changed (mtime/size). An operator who repairs SSH access
any other way — fixing authorized_keys on the host, repairing
known_hosts, restoring network reachability — still waited out a
backoff window that may have compounded toward fifteen minutes.

A system-settings save is the natural operator touchpoint after such a
repair, so the settings handler now fans ResetSSHFailureBackoff out to
every live tenant monitor after a successful save, clearing the
per-host temperature SSH backoff and the knownhosts keyscan backoff.
The reset touches in-memory retry timing only; nothing is persisted
and no request field controls it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 13:28:55 +01:00

177 lines
5.5 KiB
Go

package monitoring
import "time"
// Runtime-tunable polling cadence settings.
//
// Each tenant monitor polls against a detached DeepCopy of the base config,
// so mutating the base config after a system-settings save never reaches a
// running monitor (#1619). These setters push the saved values into live
// monitors directly, shadowing the config fields. The overrides live behind
// runtimePollingMu because polling goroutines read them every cycle while
// the settings API writes them.
// runtimePollingOverrides holds the polling-cadence values the settings API
// has pushed into this live monitor, shadowing the corresponding fields of
// the monitor's detached config copy. nil means "use the config value".
type runtimePollingOverrides struct {
backupEnabled *bool
backupInterval *time.Duration
pbsInterval *time.Duration
pmgInterval *time.Duration
}
// backupPollingEnabledSetting returns the effective EnableBackupPolling value.
func (m *Monitor) backupPollingEnabledSetting() bool {
if m == nil || m.config == nil {
return false
}
m.runtimePollingMu.RLock()
override := m.runtimePollingOverride.backupEnabled
m.runtimePollingMu.RUnlock()
if override != nil {
return *override
}
return m.config.EnableBackupPolling
}
// backupPollingIntervalSetting returns the effective BackupPollingInterval.
// Zero means cycle-based scheduling (BackupPollingCycles).
func (m *Monitor) backupPollingIntervalSetting() time.Duration {
if m == nil || m.config == nil {
return 0
}
m.runtimePollingMu.RLock()
override := m.runtimePollingOverride.backupInterval
m.runtimePollingMu.RUnlock()
if override != nil {
return *override
}
return m.config.BackupPollingInterval
}
// pbsPollingIntervalSetting returns the effective PBSPollingInterval before
// clamping.
func (m *Monitor) pbsPollingIntervalSetting() time.Duration {
if m == nil || m.config == nil {
return 0
}
m.runtimePollingMu.RLock()
override := m.runtimePollingOverride.pbsInterval
m.runtimePollingMu.RUnlock()
if override != nil {
return *override
}
return m.config.PBSPollingInterval
}
// pmgPollingIntervalSetting returns the effective PMGPollingInterval before
// clamping.
func (m *Monitor) pmgPollingIntervalSetting() time.Duration {
if m == nil || m.config == nil {
return 0
}
m.runtimePollingMu.RLock()
override := m.runtimePollingOverride.pmgInterval
m.runtimePollingMu.RUnlock()
if override != nil {
return *override
}
return m.config.PMGPollingInterval
}
// SetBackupPollingEnabled toggles backup polling on the live monitor.
// Re-enabling clears the per-instance last-poll timestamps so the next
// polling cycle runs an immediate catch-up poll, matching the behavior of
// a full monitor reload.
func (m *Monitor) SetBackupPollingEnabled(enabled bool) {
if m == nil {
return
}
m.runtimePollingMu.Lock()
wasEnabled := m.config != nil && m.config.EnableBackupPolling
if m.runtimePollingOverride.backupEnabled != nil {
wasEnabled = *m.runtimePollingOverride.backupEnabled
}
m.runtimePollingOverride.backupEnabled = &enabled
m.runtimePollingMu.Unlock()
if enabled && !wasEnabled {
m.resetBackupPollTimestamps()
}
}
// SetBackupPollingInterval updates the backup polling cadence on the live
// monitor. Lowering the interval (or switching from cycle-based scheduling
// to an interval) clears the per-instance last-poll timestamps so the next
// polling cycle runs an immediate catch-up poll rather than waiting out the
// remainder of the old interval.
func (m *Monitor) SetBackupPollingInterval(interval time.Duration) {
if m == nil {
return
}
if interval < 0 {
interval = 0
}
m.runtimePollingMu.Lock()
previous := time.Duration(0)
if m.config != nil {
previous = m.config.BackupPollingInterval
}
if m.runtimePollingOverride.backupInterval != nil {
previous = *m.runtimePollingOverride.backupInterval
}
m.runtimePollingOverride.backupInterval = &interval
m.runtimePollingMu.Unlock()
if interval > 0 && (previous <= 0 || interval < previous) {
m.resetBackupPollTimestamps()
}
}
// SetPBSPollingInterval updates the PBS polling cadence on the live monitor.
// The scheduler re-reads the base interval every cycle, so no further action
// is needed.
func (m *Monitor) SetPBSPollingInterval(interval time.Duration) {
if m == nil || interval <= 0 {
return
}
m.runtimePollingMu.Lock()
m.runtimePollingOverride.pbsInterval = &interval
m.runtimePollingMu.Unlock()
}
// SetPMGPollingInterval updates the PMG polling cadence on the live monitor.
// The scheduler re-reads the base interval every cycle, so no further action
// is needed.
func (m *Monitor) SetPMGPollingInterval(interval time.Duration) {
if m == nil || interval <= 0 {
return
}
m.runtimePollingMu.Lock()
m.runtimePollingOverride.pmgInterval = &interval
m.runtimePollingMu.Unlock()
}
// ResetSSHFailureBackoff clears the temperature collector's per-host SSH
// backoff and the knownhosts keyscan backoff. A system-settings save is an
// operator touchpoint that often follows repairing SSH access, and the
// on-disk key-change check only notices key file replacement, so the save
// itself also drops the backoff windows rather than making the operator wait
// one out (#1638).
func (m *Monitor) ResetSSHFailureBackoff() {
if m == nil {
return
}
m.tempCollector.ResetSSHFailures()
}
// resetBackupPollTimestamps clears the per-instance backup poll timestamps
// so the next cycle polls immediately.
func (m *Monitor) resetBackupPollTimestamps() {
m.mu.Lock()
m.lastPVEBackupPoll = make(map[string]time.Time)
m.lastPBSBackupPoll = make(map[string]time.Time)
m.mu.Unlock()
}