failoverLocked performs the failover while holding the write lock.
(ctx context.Context)
| 157 | |
| 158 | // failoverLocked performs the failover while holding the write lock. |
| 159 | func (cc *ClusterClient) failoverLocked(ctx context.Context) error { |
| 160 | if len(cc.candidates) == 0 { |
| 161 | return fmt.Errorf("no candidates available for failover in cluster group '%s'", cc.groupName) |
| 162 | } |
| 163 | |
| 164 | oldProfile := cc.activeProfile |
| 165 | numCandidates := len(cc.candidates) |
| 166 | |
| 167 | cc.logger.Info("[CLUSTER] Initiating failover from %s", oldProfile) |
| 168 | |
| 169 | // Try each candidate starting from the next one in order |
| 170 | for attempt := 1; attempt <= numCandidates; attempt++ { |
| 171 | idx := (cc.activeIndex + attempt) % numCandidates |
| 172 | entry := cc.candidates[idx] |
| 173 | |
| 174 | cc.logger.Info("[CLUSTER] Failover attempt %d/%d: trying %s", |
| 175 | attempt, numCandidates, entry.Name) |
| 176 | |
| 177 | client, err := NewClient(entry.Config, |
| 178 | WithLogger(cc.logger), |
| 179 | WithCache(cc.cache), |
| 180 | ) |
| 181 | if err != nil { |
| 182 | cc.logger.Error("[CLUSTER] Failover to %s failed: %v", entry.Name, err) |
| 183 | continue |
| 184 | } |
| 185 | |
| 186 | cc.activeClient = client |
| 187 | cc.activeProfile = entry.Name |
| 188 | cc.activeIndex = idx |
| 189 | cc.logger.Info("[CLUSTER] Failover successful: %s -> %s", oldProfile, entry.Name) |
| 190 | |
| 191 | // Notify callback outside the lock to avoid deadlocks |
| 192 | if cc.onFailover != nil { |
| 193 | callback := cc.onFailover |
| 194 | newProfile := entry.Name |
| 195 | go callback(oldProfile, newProfile) |
| 196 | } |
| 197 | |
| 198 | return nil |
| 199 | } |
| 200 | |
| 201 | return fmt.Errorf("failover exhausted: no reachable candidate in cluster group '%s'", cc.groupName) |
| 202 | } |
| 203 | |
| 204 | // StartHealthCheck begins periodic health checking of the active node. |
| 205 | // If the active node becomes unreachable, it automatically triggers failover. |
no test coverage detected