|
| 1 | +//===----------------------------------------------------------------------===// |
| 2 | +// |
| 3 | +// This source file is part of the swift-valkey project |
| 4 | +// |
| 5 | +// Copyright (c) 2025 the swift-valkey authors |
| 6 | +// Licensed under Apache License v2.0 |
| 7 | +// |
| 8 | +// See LICENSE.txt for license information |
| 9 | +// See swift-valkey/CONTRIBUTORS.txt for the list of swift-valkey authors |
| 10 | +// |
| 11 | +// SPDX-License-Identifier: Apache-2.0 |
| 12 | +// |
| 13 | +//===----------------------------------------------------------------------===// |
| 14 | + |
| 15 | +/// ``ValkeyTopologyElection`` manages the consensus process for electing a cluster topology. |
| 16 | +/// |
| 17 | +/// This struct tracks votes from cluster nodes for different topology candidates, keeping count of |
| 18 | +/// received votes and determining when a consensus is reached. Once a candidate receives more than half |
| 19 | +/// of the possible votes from all nodes in the cluster, it becomes the elected topology configuration. |
| 20 | +/// |
| 21 | +/// The election process handles: |
| 22 | +/// - Recording votes from nodes |
| 23 | +/// - Tracking vote counts for each topology candidate |
| 24 | +/// - Managing revotes (nodes changing their vote) |
| 25 | +/// - Determining when a winner has been elected |
| 26 | +package struct ValkeyTopologyElection { |
| 27 | + /// Represents a candidate in the topology election, tracking votes and thresholds. |
| 28 | + /// |
| 29 | + /// Each candidate corresponds to a specific cluster description and maintains |
| 30 | + /// count of the votes it has received and how many votes it needs to win. |
| 31 | + private struct Candidate { |
| 32 | + /// The cluster configuration this candidate represents. |
| 33 | + var description: ValkeyClusterDescription |
| 34 | + |
| 35 | + /// The number of votes needed for this candidate to win the election. |
| 36 | + /// Calculated as a simple majority of the total nodes in the cluster. |
| 37 | + var needed: Int |
| 38 | + |
| 39 | + /// The number of votes this candidate has received so far. |
| 40 | + var received: Int |
| 41 | + |
| 42 | + init(description: ValkeyClusterDescription) { |
| 43 | + self.description = description |
| 44 | + // Calculate the needed votes as a simple majority of all nodes across all shards |
| 45 | + self.needed = description.shards.reduce(0) { $0 + $1.nodes.count } / 2 + 1 |
| 46 | + self.received = 0 |
| 47 | + } |
| 48 | + |
| 49 | + /// Adds a vote for this candidate and checks if it has reached the winning threshold. |
| 50 | + /// |
| 51 | + /// - Returns: `true` if this candidate has received enough votes to win, `false` otherwise |
| 52 | + mutating func addVote() -> Bool { |
| 53 | + self.received += 1 |
| 54 | + return self.received >= self.needed |
| 55 | + } |
| 56 | + } |
| 57 | + |
| 58 | + /// Provides metrics about the current state of the election process. |
| 59 | + /// |
| 60 | + /// This structure encapsulates information about a specific topology candidate, |
| 61 | + /// including how many votes it has received and how many it needs to win. |
| 62 | + package struct VoteMetrics { |
| 63 | + /// The total number of topology configurations being considered in this election. |
| 64 | + package var candidateCount: Int |
| 65 | + |
| 66 | + /// The specific topology candidate these metrics refer to. |
| 67 | + package var candidate: ValkeyTopologyCandidate |
| 68 | + |
| 69 | + /// The number of votes this candidate has received so far. |
| 70 | + package var votesReceived: Int |
| 71 | + |
| 72 | + /// The number of votes needed for this candidate to win the election. |
| 73 | + /// This is calculated as (total nodes / 2) + 1, representing a simple majority. |
| 74 | + package var votesNeeded: Int |
| 75 | + } |
| 76 | + |
| 77 | + private var votes = [ValkeyNodeID: ValkeyTopologyCandidate]() |
| 78 | + private var results = [ValkeyTopologyCandidate: Candidate]() |
| 79 | + |
| 80 | + /// The currently elected cluster configuration, if any. |
| 81 | + /// This is set to the first candidate that reaches the required vote threshold. |
| 82 | + package private(set) var winner: ValkeyClusterDescription? |
| 83 | + |
| 84 | + package init() {} |
| 85 | + |
| 86 | + /// Records a vote from a node for a specific cluster description. |
| 87 | + /// |
| 88 | + /// This method handles the core voting logic: |
| 89 | + /// 1. If the node has voted before, its previous vote is removed |
| 90 | + /// 2. The new vote is recorded |
| 91 | + /// 3. If this vote causes a candidate to reach the required threshold, it becomes the winner |
| 92 | + /// |
| 93 | + /// - Parameters: |
| 94 | + /// - description: The cluster configuration the node is voting for |
| 95 | + /// - voter: The ID of the node casting the vote |
| 96 | + /// |
| 97 | + /// - Returns: Metrics about the current state of the election after recording this vote |
| 98 | + /// |
| 99 | + /// - Throws: ``ValkeyClusterError`` if the provided cluster description cannot be converted to a valid topology candidate |
| 100 | + package mutating func voteReceived( |
| 101 | + for description: ValkeyClusterDescription, |
| 102 | + from voter: ValkeyNodeID |
| 103 | + ) throws(ValkeyClusterError) -> VoteMetrics { |
| 104 | + // 1. check that the voter hasn't voted before. |
| 105 | + // - if it has voted before, remove its earlier vote. |
| 106 | + |
| 107 | + let topologyCandidate = try ValkeyTopologyCandidate(description) |
| 108 | + |
| 109 | + if let previousVote = self.votes[voter] { |
| 110 | + self.results[previousVote]!.received -= 1 |
| 111 | + } |
| 112 | + |
| 113 | + self.votes[voter] = topologyCandidate |
| 114 | + if self.results[topologyCandidate, default: .init(description: description)].addVote() { |
| 115 | + if self.winner == nil { |
| 116 | + self.winner = description |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + return VoteMetrics( |
| 121 | + candidateCount: self.results.count, |
| 122 | + candidate: topologyCandidate, |
| 123 | + votesReceived: self.results[topologyCandidate]!.received, |
| 124 | + votesNeeded: self.results[topologyCandidate]!.needed |
| 125 | + ) |
| 126 | + } |
| 127 | +} |
0 commit comments