fix: prevent VM-specific errors from marking cluster nodes unhealthy (addresses #405)

- Fixed issue where QEMU guest agent errors incorrectly marked nodes as unhealthy
- Nodes with VMs missing guest agents no longer affect cluster health status
- Reduced health check retry interval from 30s to 5s for faster recovery
- Storage and backup polling now works correctly even when some VMs lack guest agents
This commit is contained in:
Pulse Monitor
2025-09-04 19:34:11 +00:00
parent 4f72aa1c9b
commit 65b6ef6cac
+58 -21
View File
@@ -81,14 +81,20 @@ func (cc *ClusterClient) initialHealthCheck() {
cancel()
cc.mu.Lock()
// Check if error is VM-specific (shouldn't affect health)
isVMSpecificError := false
if err != nil {
cc.nodeHealth[ep] = false
log.Info().
Str("cluster", cc.name).
Str("endpoint", ep).
Msg("Cluster endpoint failed initial health check")
} else {
// Create a proper client with full timeout for actual use
errStr := err.Error()
if strings.Contains(errStr, "No QEMU guest agent") ||
strings.Contains(errStr, "QEMU guest agent is not running") ||
strings.Contains(errStr, "guest agent") {
isVMSpecificError = true
}
}
if err == nil || isVMSpecificError {
// Node is healthy - create a proper client with full timeout for actual use
fullCfg := cc.config
fullCfg.Host = ep
fullClient, clientErr := NewClient(fullCfg)
@@ -102,11 +108,26 @@ func (cc *ClusterClient) initialHealthCheck() {
} else {
cc.nodeHealth[ep] = true
cc.clients[ep] = fullClient // Store the full client, not test client
log.Info().
Str("cluster", cc.name).
Str("endpoint", ep).
Msg("Cluster endpoint passed initial health check")
if isVMSpecificError {
log.Debug().
Str("cluster", cc.name).
Str("endpoint", ep).
Msg("Cluster endpoint healthy despite VM-specific errors")
} else {
log.Info().
Str("cluster", cc.name).
Str("endpoint", ep).
Msg("Cluster endpoint passed initial health check")
}
}
} else {
// Real connectivity issue
cc.nodeHealth[ep] = false
log.Info().
Str("cluster", cc.name).
Str("endpoint", ep).
Err(err).
Msg("Cluster endpoint failed initial health check")
}
cc.lastHealthCheck[ep] = time.Now()
cc.mu.Unlock()
@@ -192,14 +213,29 @@ func (cc *ClusterClient) getHealthyClient(ctx context.Context) (*Client, error)
cancel()
if testErr != nil {
// Mark as unhealthy
cc.nodeHealth[selectedEndpoint] = false
log.Warn().
Str("cluster", cc.name).
Str("endpoint", selectedEndpoint).
Err(testErr).
Msg("Cluster endpoint failed connectivity test")
return nil, fmt.Errorf("endpoint %s failed connectivity test: %w", selectedEndpoint, testErr)
// Check if this is a VM-specific error that shouldn't mark the node unhealthy
testErrStr := testErr.Error()
if strings.Contains(testErrStr, "No QEMU guest agent") ||
strings.Contains(testErrStr, "QEMU guest agent is not running") ||
strings.Contains(testErrStr, "guest agent") {
// This is a VM-specific issue, not a connectivity problem
// The node is actually healthy, so don't mark it unhealthy
log.Debug().
Str("cluster", cc.name).
Str("endpoint", selectedEndpoint).
Err(testErr).
Msg("Ignoring VM-specific error during connectivity test")
// Continue with client creation since the node is actually accessible
} else {
// Mark as unhealthy for real connectivity issues
cc.nodeHealth[selectedEndpoint] = false
log.Warn().
Str("cluster", cc.name).
Str("endpoint", selectedEndpoint).
Err(testErr).
Msg("Cluster endpoint failed connectivity test")
return nil, fmt.Errorf("endpoint %s failed connectivity test: %w", selectedEndpoint, testErr)
}
}
log.Debug().
@@ -244,9 +280,10 @@ func (cc *ClusterClient) recoverUnhealthyNodes(ctx context.Context) {
now := time.Now()
for endpoint, healthy := range cc.nodeHealth {
if !healthy {
// Skip if we checked this endpoint recently (within 30 seconds)
// Skip if we checked this endpoint recently (within 5 seconds)
// Reduced from 30 seconds to allow faster recovery
if lastCheck, exists := cc.lastHealthCheck[endpoint]; exists {
if now.Sub(lastCheck) < 30*time.Second {
if now.Sub(lastCheck) < 5*time.Second {
continue
}
}