Fix node/host dropout issue caused by cluster health failures
Implemented comprehensive state preservation to prevent temporary dropouts:
1. Node Grace Period (60s):
- Track last-online timestamp for each Proxmox node
- Preserve online status during grace period to prevent flapping
- Applied to all node status checks throughout codebase
2. Efficient Polling Preservation:
- Detect when cluster/resources returns empty arrays
- Preserve previous VMs/containers if had resources before
- Handles cluster health check failures gracefully
3. Traditional Polling Preservation:
- Updated preservation logic for per-node VM/container polling
- Triggers when zero resources returned regardless of node response
- Fixed issue where nodes responding with empty data bypassed preservation
Root cause: Intermittent Proxmox cluster health failures ("no healthy nodes
available") caused both efficient and traditional polling to return empty
arrays, immediately clearing all VMs/containers from state.
Changes:
- internal/monitoring/monitor.go: Added node grace period, efficient polling preservation
- internal/monitoring/monitor_polling.go: Fixed traditional polling preservation logic
Fixes frequent UI flickering where vmCount/containerCount would briefly drop to zero.
This commit is contained in:
parent
5a328637af
commit
7dd7a0b0f9
2 changed files with 209 additions and 25 deletions
|
|
@ -490,6 +490,7 @@ type Monitor struct {
|
||||||
instanceInfoCache map[string]*instanceInfo
|
instanceInfoCache map[string]*instanceInfo
|
||||||
pollStatusMap map[string]*pollStatus
|
pollStatusMap map[string]*pollStatus
|
||||||
dlqInsightMap map[string]*dlqInsight
|
dlqInsightMap map[string]*dlqInsight
|
||||||
|
nodeLastOnline map[string]time.Time // Track last time each node was seen online (for grace period)
|
||||||
}
|
}
|
||||||
|
|
||||||
type rrdMemCacheEntry struct {
|
type rrdMemCacheEntry struct {
|
||||||
|
|
@ -773,6 +774,7 @@ const (
|
||||||
hostOfflineGraceMultiplier = 4
|
hostOfflineGraceMultiplier = 4
|
||||||
hostMinimumHealthWindow = 30 * time.Second
|
hostMinimumHealthWindow = 30 * time.Second
|
||||||
hostMaximumHealthWindow = 10 * time.Minute
|
hostMaximumHealthWindow = 10 * time.Minute
|
||||||
|
nodeOfflineGracePeriod = 60 * time.Second // Grace period before marking Proxmox nodes offline
|
||||||
nodeRRDCacheTTL = 30 * time.Second
|
nodeRRDCacheTTL = 30 * time.Second
|
||||||
nodeRRDRequestTimeout = 2 * time.Second
|
nodeRRDRequestTimeout = 2 * time.Second
|
||||||
guestMetadataCacheTTL = 5 * time.Minute
|
guestMetadataCacheTTL = 5 * time.Minute
|
||||||
|
|
@ -3209,6 +3211,7 @@ func New(cfg *config.Config) (*Monitor, error) {
|
||||||
instanceInfoCache: make(map[string]*instanceInfo),
|
instanceInfoCache: make(map[string]*instanceInfo),
|
||||||
pollStatusMap: make(map[string]*pollStatus),
|
pollStatusMap: make(map[string]*pollStatus),
|
||||||
dlqInsightMap: make(map[string]*dlqInsight),
|
dlqInsightMap: make(map[string]*dlqInsight),
|
||||||
|
nodeLastOnline: make(map[string]time.Time),
|
||||||
}
|
}
|
||||||
|
|
||||||
m.breakerBaseRetry = 5 * time.Second
|
m.breakerBaseRetry = 5 * time.Second
|
||||||
|
|
@ -4877,6 +4880,7 @@ func (m *Monitor) pollPVEInstance(ctx context.Context, instanceName string, clie
|
||||||
|
|
||||||
// Convert to models
|
// Convert to models
|
||||||
var modelNodes []models.Node
|
var modelNodes []models.Node
|
||||||
|
nodeEffectiveStatus := make(map[string]string) // Track effective status (with grace period) for each node
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
nodeStart := time.Now()
|
nodeStart := time.Now()
|
||||||
displayName := getNodeDisplayName(instanceCfg, node.Node)
|
displayName := getNodeDisplayName(instanceCfg, node.Node)
|
||||||
|
|
@ -4892,13 +4896,50 @@ func (m *Monitor) pollPVEInstance(ctx context.Context, instanceName string, clie
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply grace period for node status to prevent flapping
|
||||||
|
nodeID := instanceName + "-" + node.Node
|
||||||
|
effectiveStatus := node.Status
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
if strings.ToLower(node.Status) == "online" {
|
||||||
|
// Node is online - update last-online timestamp
|
||||||
|
m.nodeLastOnline[nodeID] = now
|
||||||
|
} else {
|
||||||
|
// Node is reported as offline - check grace period
|
||||||
|
lastOnline, exists := m.nodeLastOnline[nodeID]
|
||||||
|
if exists && now.Sub(lastOnline) < nodeOfflineGracePeriod {
|
||||||
|
// Still within grace period - preserve online status
|
||||||
|
effectiveStatus = "online"
|
||||||
|
log.Debug().
|
||||||
|
Str("instance", instanceName).
|
||||||
|
Str("node", node.Node).
|
||||||
|
Dur("timeSinceOnline", now.Sub(lastOnline)).
|
||||||
|
Dur("gracePeriod", nodeOfflineGracePeriod).
|
||||||
|
Msg("Node offline but within grace period - preserving online status")
|
||||||
|
} else {
|
||||||
|
// Grace period expired or never seen online - mark as offline
|
||||||
|
if exists {
|
||||||
|
log.Info().
|
||||||
|
Str("instance", instanceName).
|
||||||
|
Str("node", node.Node).
|
||||||
|
Dur("timeSinceOnline", now.Sub(lastOnline)).
|
||||||
|
Msg("Node offline and grace period expired - marking as offline")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
// Store effective status for use in subsequent loops
|
||||||
|
nodeEffectiveStatus[node.Node] = effectiveStatus
|
||||||
|
|
||||||
modelNode := models.Node{
|
modelNode := models.Node{
|
||||||
ID: instanceName + "-" + node.Node,
|
ID: nodeID,
|
||||||
Name: node.Node,
|
Name: node.Node,
|
||||||
DisplayName: displayName,
|
DisplayName: displayName,
|
||||||
Instance: instanceName,
|
Instance: instanceName,
|
||||||
Host: connectionHost,
|
Host: connectionHost,
|
||||||
Status: node.Status,
|
Status: effectiveStatus,
|
||||||
Type: "node",
|
Type: "node",
|
||||||
CPU: safeFloat(node.CPU), // Already in percentage
|
CPU: safeFloat(node.CPU), // Already in percentage
|
||||||
Memory: models.Memory{
|
Memory: models.Memory{
|
||||||
|
|
@ -4950,7 +4991,7 @@ func (m *Monitor) pollPVEInstance(ctx context.Context, instanceName string, clie
|
||||||
memoryUpdated := false
|
memoryUpdated := false
|
||||||
|
|
||||||
// Get detailed node info if available (skip for offline nodes)
|
// Get detailed node info if available (skip for offline nodes)
|
||||||
if node.Status == "online" {
|
if effectiveStatus == "online" {
|
||||||
nodeInfo, nodeErr := client.GetNodeStatus(ctx, node.Node)
|
nodeInfo, nodeErr := client.GetNodeStatus(ctx, node.Node)
|
||||||
if nodeErr != nil {
|
if nodeErr != nil {
|
||||||
nodeFallbackReason = "node-status-unavailable"
|
nodeFallbackReason = "node-status-unavailable"
|
||||||
|
|
@ -5240,7 +5281,7 @@ func (m *Monitor) pollPVEInstance(ctx context.Context, instanceName string, clie
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we couldn't update memory metrics using detailed status, preserve previous accurate values if available
|
// If we couldn't update memory metrics using detailed status, preserve previous accurate values if available
|
||||||
if !memoryUpdated && node.Status == "online" {
|
if !memoryUpdated && effectiveStatus == "online" {
|
||||||
if prevMem, exists := prevNodeMemory[modelNode.ID]; exists && prevMem.Total > 0 {
|
if prevMem, exists := prevNodeMemory[modelNode.ID]; exists && prevMem.Total > 0 {
|
||||||
total := int64(node.MaxMem)
|
total := int64(node.MaxMem)
|
||||||
if total == 0 {
|
if total == 0 {
|
||||||
|
|
@ -5292,7 +5333,7 @@ func (m *Monitor) pollPVEInstance(ctx context.Context, instanceName string, clie
|
||||||
if instanceCfg.TemperatureMonitoringEnabled != nil {
|
if instanceCfg.TemperatureMonitoringEnabled != nil {
|
||||||
tempMonitoringEnabled = *instanceCfg.TemperatureMonitoringEnabled
|
tempMonitoringEnabled = *instanceCfg.TemperatureMonitoringEnabled
|
||||||
}
|
}
|
||||||
if node.Status == "online" && m.tempCollector != nil && tempMonitoringEnabled {
|
if effectiveStatus == "online" && m.tempCollector != nil && tempMonitoringEnabled {
|
||||||
tempCtx, tempCancel := context.WithTimeout(ctx, 30*time.Second) // Increased to accommodate SSH operations via proxy
|
tempCtx, tempCancel := context.WithTimeout(ctx, 30*time.Second) // Increased to accommodate SSH operations via proxy
|
||||||
|
|
||||||
// Determine SSH hostname to use (most robust approach):
|
// Determine SSH hostname to use (most robust approach):
|
||||||
|
|
@ -5462,7 +5503,7 @@ func (m *Monitor) pollPVEInstance(ctx context.Context, instanceName string, clie
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
// Skip offline nodes to avoid 595 errors
|
// Skip offline nodes to avoid 595 errors
|
||||||
if node.Status != "online" {
|
if nodeEffectiveStatus[node.Node] != "online" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5564,7 +5605,7 @@ func (m *Monitor) pollPVEInstance(ctx context.Context, instanceName string, clie
|
||||||
|
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
// Skip offline nodes but preserve their existing disk data
|
// Skip offline nodes but preserve their existing disk data
|
||||||
if node.Status != "online" {
|
if nodeEffectiveStatus[node.Node] != "online" {
|
||||||
log.Debug().Str("node", node.Node).Msg("Skipping disk poll for offline node - preserving existing data")
|
log.Debug().Str("node", node.Node).Msg("Skipping disk poll for offline node - preserving existing data")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -5798,7 +5839,7 @@ func (m *Monitor) pollPVEInstance(ctx context.Context, instanceName string, clie
|
||||||
// Always try the efficient cluster/resources endpoint first
|
// Always try the efficient cluster/resources endpoint first
|
||||||
// This endpoint works on both clustered and standalone nodes
|
// This endpoint works on both clustered and standalone nodes
|
||||||
// Testing confirmed it works on standalone nodes like pimox
|
// Testing confirmed it works on standalone nodes like pimox
|
||||||
useClusterEndpoint := m.pollVMsAndContainersEfficient(ctx, instanceName, client)
|
useClusterEndpoint := m.pollVMsAndContainersEfficient(ctx, instanceName, client, nodeEffectiveStatus)
|
||||||
|
|
||||||
if !useClusterEndpoint {
|
if !useClusterEndpoint {
|
||||||
// Fall back to traditional polling only if cluster/resources not available
|
// Fall back to traditional polling only if cluster/resources not available
|
||||||
|
|
@ -5820,10 +5861,10 @@ func (m *Monitor) pollPVEInstance(ctx context.Context, instanceName string, clie
|
||||||
|
|
||||||
// Use optimized parallel polling for better performance
|
// Use optimized parallel polling for better performance
|
||||||
if instanceCfg.MonitorVMs {
|
if instanceCfg.MonitorVMs {
|
||||||
m.pollVMsWithNodes(ctx, instanceName, client, nodes)
|
m.pollVMsWithNodes(ctx, instanceName, client, nodes, nodeEffectiveStatus)
|
||||||
}
|
}
|
||||||
if instanceCfg.MonitorContainers {
|
if instanceCfg.MonitorContainers {
|
||||||
m.pollContainersWithNodes(ctx, instanceName, client, nodes)
|
m.pollContainersWithNodes(ctx, instanceName, client, nodes, nodeEffectiveStatus)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5887,7 +5928,7 @@ func (m *Monitor) pollPVEInstance(ctx context.Context, instanceName string, clie
|
||||||
m.pollBackupTasks(backupCtx, inst, pveClient)
|
m.pollBackupTasks(backupCtx, inst, pveClient)
|
||||||
|
|
||||||
// Poll storage backups - pass nodes to avoid duplicate API calls
|
// Poll storage backups - pass nodes to avoid duplicate API calls
|
||||||
m.pollStorageBackupsWithNodes(backupCtx, inst, pveClient, nodes)
|
m.pollStorageBackupsWithNodes(backupCtx, inst, pveClient, nodes, nodeEffectiveStatus)
|
||||||
|
|
||||||
// Poll guest snapshots
|
// Poll guest snapshots
|
||||||
m.pollGuestSnapshots(backupCtx, inst, pveClient)
|
m.pollGuestSnapshots(backupCtx, inst, pveClient)
|
||||||
|
|
@ -5911,7 +5952,7 @@ func (m *Monitor) pollPVEInstance(ctx context.Context, instanceName string, clie
|
||||||
|
|
||||||
// pollVMsAndContainersEfficient uses the cluster/resources endpoint to get all VMs and containers in one call
|
// pollVMsAndContainersEfficient uses the cluster/resources endpoint to get all VMs and containers in one call
|
||||||
// This works on both clustered and standalone nodes for efficient polling
|
// This works on both clustered and standalone nodes for efficient polling
|
||||||
func (m *Monitor) pollVMsAndContainersEfficient(ctx context.Context, instanceName string, client PVEClientInterface) bool {
|
func (m *Monitor) pollVMsAndContainersEfficient(ctx context.Context, instanceName string, client PVEClientInterface, nodeEffectiveStatus map[string]string) bool {
|
||||||
log.Info().Str("instance", instanceName).Msg("Polling VMs and containers using efficient cluster/resources endpoint")
|
log.Info().Str("instance", instanceName).Msg("Polling VMs and containers using efficient cluster/resources endpoint")
|
||||||
|
|
||||||
// Get all resources in a single API call
|
// Get all resources in a single API call
|
||||||
|
|
@ -6571,14 +6612,115 @@ func (m *Monitor) pollVMsAndContainersEfficient(ctx context.Context, instanceNam
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update state
|
// Preserve VMs and containers from nodes within grace period
|
||||||
if len(allVMs) > 0 {
|
// The cluster/resources endpoint doesn't return VMs/containers from nodes Proxmox considers offline,
|
||||||
m.state.UpdateVMsForInstance(instanceName, allVMs)
|
// but we want to keep showing them if the node is within grace period
|
||||||
|
prevState := m.GetState()
|
||||||
|
|
||||||
|
// Count previous resources for this instance
|
||||||
|
prevVMCount := 0
|
||||||
|
prevContainerCount := 0
|
||||||
|
for _, vm := range prevState.VMs {
|
||||||
|
if vm.Instance == instanceName {
|
||||||
|
prevVMCount++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if len(allContainers) > 0 {
|
for _, container := range prevState.Containers {
|
||||||
m.state.UpdateContainersForInstance(instanceName, allContainers)
|
if container.Instance == instanceName {
|
||||||
|
prevContainerCount++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build map of which nodes are covered by current resources
|
||||||
|
nodesWithResources := make(map[string]bool)
|
||||||
|
for _, res := range resources {
|
||||||
|
nodesWithResources[res.Node] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().
|
||||||
|
Str("instance", instanceName).
|
||||||
|
Int("nodesInResources", len(nodesWithResources)).
|
||||||
|
Int("totalVMsFromResources", len(allVMs)).
|
||||||
|
Int("totalContainersFromResources", len(allContainers)).
|
||||||
|
Int("prevVMs", prevVMCount).
|
||||||
|
Int("prevContainers", prevContainerCount).
|
||||||
|
Msg("Cluster resources received, checking for grace period preservation")
|
||||||
|
|
||||||
|
// If we got ZERO resources but had resources before, and we have no node data,
|
||||||
|
// this likely means the cluster health check failed. Preserve everything.
|
||||||
|
if len(allVMs) == 0 && len(allContainers) == 0 &&
|
||||||
|
(prevVMCount > 0 || prevContainerCount > 0) &&
|
||||||
|
len(nodeEffectiveStatus) == 0 {
|
||||||
|
log.Warn().
|
||||||
|
Str("instance", instanceName).
|
||||||
|
Int("prevVMs", prevVMCount).
|
||||||
|
Int("prevContainers", prevContainerCount).
|
||||||
|
Msg("Cluster returned zero resources but had resources before - likely cluster health issue, preserving all previous resources")
|
||||||
|
|
||||||
|
// Preserve all previous VMs and containers for this instance
|
||||||
|
for _, vm := range prevState.VMs {
|
||||||
|
if vm.Instance == instanceName {
|
||||||
|
allVMs = append(allVMs, vm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, container := range prevState.Containers {
|
||||||
|
if container.Instance == instanceName {
|
||||||
|
allContainers = append(allContainers, container)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for nodes that are within grace period but not in cluster/resources response
|
||||||
|
preservedVMCount := 0
|
||||||
|
preservedContainerCount := 0
|
||||||
|
for nodeName, effectiveStatus := range nodeEffectiveStatus {
|
||||||
|
if effectiveStatus == "online" && !nodesWithResources[nodeName] {
|
||||||
|
// This node is within grace period but Proxmox didn't return its resources
|
||||||
|
// Preserve previous VMs and containers from this node
|
||||||
|
vmsBefore := len(allVMs)
|
||||||
|
containersBefore := len(allContainers)
|
||||||
|
|
||||||
|
// Preserve VMs from this node
|
||||||
|
for _, vm := range prevState.VMs {
|
||||||
|
if vm.Instance == instanceName && vm.Node == nodeName {
|
||||||
|
allVMs = append(allVMs, vm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve containers from this node
|
||||||
|
for _, container := range prevState.Containers {
|
||||||
|
if container.Instance == instanceName && container.Node == nodeName {
|
||||||
|
allContainers = append(allContainers, container)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vmsPreserved := len(allVMs) - vmsBefore
|
||||||
|
containersPreserved := len(allContainers) - containersBefore
|
||||||
|
preservedVMCount += vmsPreserved
|
||||||
|
preservedContainerCount += containersPreserved
|
||||||
|
|
||||||
|
log.Info().
|
||||||
|
Str("instance", instanceName).
|
||||||
|
Str("node", nodeName).
|
||||||
|
Int("vmsPreserved", vmsPreserved).
|
||||||
|
Int("containersPreserved", containersPreserved).
|
||||||
|
Msg("Preserved VMs/containers from node in grace period")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if preservedVMCount > 0 || preservedContainerCount > 0 {
|
||||||
|
log.Info().
|
||||||
|
Str("instance", instanceName).
|
||||||
|
Int("totalPreservedVMs", preservedVMCount).
|
||||||
|
Int("totalPreservedContainers", preservedContainerCount).
|
||||||
|
Msg("Grace period preservation complete")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always update state when using efficient polling path
|
||||||
|
// Even if arrays are empty, we need to update to clear out VMs from genuinely offline nodes
|
||||||
|
m.state.UpdateVMsForInstance(instanceName, allVMs)
|
||||||
|
m.state.UpdateContainersForInstance(instanceName, allContainers)
|
||||||
|
|
||||||
m.pollReplicationStatus(ctx, instanceName, client, allVMs)
|
m.pollReplicationStatus(ctx, instanceName, client, allVMs)
|
||||||
|
|
||||||
log.Info().
|
log.Info().
|
||||||
|
|
@ -7577,7 +7719,7 @@ func (m *Monitor) GetConfigPersistence() *config.ConfigPersistence {
|
||||||
}
|
}
|
||||||
|
|
||||||
// pollStorageBackupsWithNodes polls backups using a provided nodes list to avoid duplicate GetNodes calls
|
// pollStorageBackupsWithNodes polls backups using a provided nodes list to avoid duplicate GetNodes calls
|
||||||
func (m *Monitor) pollStorageBackupsWithNodes(ctx context.Context, instanceName string, client PVEClientInterface, nodes []proxmox.Node) {
|
func (m *Monitor) pollStorageBackupsWithNodes(ctx context.Context, instanceName string, client PVEClientInterface, nodes []proxmox.Node, nodeEffectiveStatus map[string]string) {
|
||||||
|
|
||||||
var allBackups []models.StorageBackup
|
var allBackups []models.StorageBackup
|
||||||
seenVolids := make(map[string]bool) // Track seen volume IDs to avoid duplicates
|
seenVolids := make(map[string]bool) // Track seen volume IDs to avoid duplicates
|
||||||
|
|
@ -7589,7 +7731,7 @@ func (m *Monitor) pollStorageBackupsWithNodes(ctx context.Context, instanceName
|
||||||
|
|
||||||
// For each node, get storage and check content
|
// For each node, get storage and check content
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
if node.Status != "online" {
|
if nodeEffectiveStatus[node.Node] != "online" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -185,7 +185,7 @@ func convertPoolInfoToModel(poolInfo *proxmox.ZFSPoolInfo) *models.ZFSPool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// pollVMsWithNodes polls VMs from all nodes in parallel using goroutines
|
// pollVMsWithNodes polls VMs from all nodes in parallel using goroutines
|
||||||
func (m *Monitor) pollVMsWithNodes(ctx context.Context, instanceName string, client PVEClientInterface, nodes []proxmox.Node) {
|
func (m *Monitor) pollVMsWithNodes(ctx context.Context, instanceName string, client PVEClientInterface, nodes []proxmox.Node, nodeEffectiveStatus map[string]string) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
|
|
||||||
// Channel to collect VM results from each node
|
// Channel to collect VM results from each node
|
||||||
|
|
@ -201,7 +201,7 @@ func (m *Monitor) pollVMsWithNodes(ctx context.Context, instanceName string, cli
|
||||||
// Count online nodes for logging
|
// Count online nodes for logging
|
||||||
onlineNodes := 0
|
onlineNodes := 0
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
if node.Status == "online" {
|
if nodeEffectiveStatus[node.Node] == "online" {
|
||||||
onlineNodes++
|
onlineNodes++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -215,7 +215,7 @@ func (m *Monitor) pollVMsWithNodes(ctx context.Context, instanceName string, cli
|
||||||
// Launch a goroutine for each online node
|
// Launch a goroutine for each online node
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
// Skip offline nodes
|
// Skip offline nodes
|
||||||
if node.Status != "online" {
|
if nodeEffectiveStatus[node.Node] != "online" {
|
||||||
log.Debug().
|
log.Debug().
|
||||||
Str("node", node.Node).
|
Str("node", node.Node).
|
||||||
Str("status", node.Status).
|
Str("status", node.Status).
|
||||||
|
|
@ -788,6 +788,27 @@ func (m *Monitor) pollVMsWithNodes(ctx context.Context, instanceName string, cli
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we got ZERO VMs but had VMs before (likely cluster health issue),
|
||||||
|
// preserve previous VMs instead of clearing them
|
||||||
|
if len(allVMs) == 0 && len(nodes) > 0 {
|
||||||
|
prevState := m.GetState()
|
||||||
|
prevVMCount := 0
|
||||||
|
for _, vm := range prevState.VMs {
|
||||||
|
if vm.Instance == instanceName {
|
||||||
|
allVMs = append(allVMs, vm)
|
||||||
|
prevVMCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if prevVMCount > 0 {
|
||||||
|
log.Warn().
|
||||||
|
Str("instance", instanceName).
|
||||||
|
Int("prevVMs", prevVMCount).
|
||||||
|
Int("successfulNodes", successfulNodes).
|
||||||
|
Int("totalNodes", len(nodes)).
|
||||||
|
Msg("Traditional polling returned zero VMs but had VMs before - preserving previous VMs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update state with all VMs
|
// Update state with all VMs
|
||||||
m.state.UpdateVMsForInstance(instanceName, allVMs)
|
m.state.UpdateVMsForInstance(instanceName, allVMs)
|
||||||
|
|
||||||
|
|
@ -802,7 +823,7 @@ func (m *Monitor) pollVMsWithNodes(ctx context.Context, instanceName string, cli
|
||||||
}
|
}
|
||||||
|
|
||||||
// pollContainersWithNodes polls containers from all nodes in parallel using goroutines
|
// pollContainersWithNodes polls containers from all nodes in parallel using goroutines
|
||||||
func (m *Monitor) pollContainersWithNodes(ctx context.Context, instanceName string, client PVEClientInterface, nodes []proxmox.Node) {
|
func (m *Monitor) pollContainersWithNodes(ctx context.Context, instanceName string, client PVEClientInterface, nodes []proxmox.Node, nodeEffectiveStatus map[string]string) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
|
|
||||||
// Channel to collect container results from each node
|
// Channel to collect container results from each node
|
||||||
|
|
@ -818,7 +839,7 @@ func (m *Monitor) pollContainersWithNodes(ctx context.Context, instanceName stri
|
||||||
// Count online nodes for logging
|
// Count online nodes for logging
|
||||||
onlineNodes := 0
|
onlineNodes := 0
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
if node.Status == "online" {
|
if nodeEffectiveStatus[node.Node] == "online" {
|
||||||
onlineNodes++
|
onlineNodes++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -832,7 +853,7 @@ func (m *Monitor) pollContainersWithNodes(ctx context.Context, instanceName stri
|
||||||
// Launch a goroutine for each online node
|
// Launch a goroutine for each online node
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
// Skip offline nodes
|
// Skip offline nodes
|
||||||
if node.Status != "online" {
|
if nodeEffectiveStatus[node.Node] != "online" {
|
||||||
log.Debug().
|
log.Debug().
|
||||||
Str("node", node.Node).
|
Str("node", node.Node).
|
||||||
Str("status", node.Status).
|
Str("status", node.Status).
|
||||||
|
|
@ -1033,6 +1054,27 @@ func (m *Monitor) pollContainersWithNodes(ctx context.Context, instanceName stri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we got ZERO containers but had containers before (likely cluster health issue),
|
||||||
|
// preserve previous containers instead of clearing them
|
||||||
|
if len(allContainers) == 0 && len(nodes) > 0 {
|
||||||
|
prevState := m.GetState()
|
||||||
|
prevContainerCount := 0
|
||||||
|
for _, container := range prevState.Containers {
|
||||||
|
if container.Instance == instanceName {
|
||||||
|
allContainers = append(allContainers, container)
|
||||||
|
prevContainerCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if prevContainerCount > 0 {
|
||||||
|
log.Warn().
|
||||||
|
Str("instance", instanceName).
|
||||||
|
Int("prevContainers", prevContainerCount).
|
||||||
|
Int("successfulNodes", successfulNodes).
|
||||||
|
Int("totalNodes", len(nodes)).
|
||||||
|
Msg("Traditional polling returned zero containers but had containers before - preserving previous containers")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update state with all containers
|
// Update state with all containers
|
||||||
m.state.UpdateContainersForInstance(instanceName, allContainers)
|
m.state.UpdateContainersForInstance(instanceName, allContainers)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue