foundationdb/recipes/leader_election/types.rs
1// Copyright 2024 foundationdb-rs developers
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// http://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8//! Core data structures for leader election
9//!
10//! This module defines the fundamental types used in the ballot-based
11//! leader election algorithm, including leader state, candidate info, and configuration.
12//!
13//! # Design Overview
14//!
15//! The election algorithm uses a ballot-based approach (similar to Raft's term):
16//! - **LeaderState**: Stored at a single key, contains ballot number and leader identity
17//! - **CandidateInfo**: Per-candidate registration with versionstamp for ordering
18//! - **Ballot Numbers**: Monotonically increasing, higher ballot always wins
19
20use std::time::Duration;
21
22/// Default lease duration for leadership
23pub const DEFAULT_LEASE_DURATION: Duration = Duration::from_secs(10);
24
25/// Default heartbeat interval (should be lease_duration / 3 approximately)
26pub const DEFAULT_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(3);
27
28/// Default candidate timeout (when to consider a candidate dead)
29pub const DEFAULT_CANDIDATE_TIMEOUT: Duration = Duration::from_secs(15);
30
31/// The core leader state - stored at a single key
32///
33/// Contains all information
34/// needed to determine leadership without scanning candidates.
35///
36/// # Ballot Numbers
37///
38/// The ballot number works like Raft's term:
39/// - Monotonically increasing counter
40/// - Higher ballot always wins
41/// - Prevents split-brain after recovery/partition
42/// - Incremented on every leadership claim or refresh
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct LeaderState {
45 /// Ballot number (like Raft's term)
46 ///
47 /// Always increments when claiming or refreshing leadership.
48 /// Higher ballot wins in any conflict.
49 pub ballot: u64,
50
51 /// Unique identifier of the leader process
52 pub leader_id: String,
53
54 /// Leader's priority (higher = more preferred)
55 ///
56 /// Used for preemption decisions when `allow_preemption` is enabled.
57 pub priority: i32,
58
59 /// Absolute timestamp when lease expires (nanos since epoch)
60 ///
61 /// Leadership is only valid while `current_time < lease_expiry_nanos`.
62 pub lease_expiry_nanos: u64,
63
64 /// Versionstamp assigned when this process registered
65 ///
66 /// Used for identity consistency and ordering.
67 pub versionstamp: [u8; 12],
68}
69
70impl LeaderState {
71 /// Check if the lease is still valid
72 ///
73 /// # Arguments
74 /// * `current_time` - Current time as Duration since epoch
75 ///
76 /// # Returns
77 /// `true` if the lease has not expired
78 pub fn is_lease_valid(&self, current_time: Duration) -> bool {
79 current_time.as_nanos() < self.lease_expiry_nanos as u128
80 }
81
82 /// Get remaining lease duration, if any
83 ///
84 /// # Arguments
85 /// * `current_time` - Current time as Duration since epoch
86 ///
87 /// # Returns
88 /// `Some(Duration)` if lease is still valid, `None` if expired
89 pub fn remaining_lease(&self, current_time: Duration) -> Option<Duration> {
90 let current_nanos = current_time.as_nanos() as u64;
91 if current_nanos < self.lease_expiry_nanos {
92 Some(Duration::from_nanos(
93 self.lease_expiry_nanos - current_nanos,
94 ))
95 } else {
96 None
97 }
98 }
99}
100
101/// Information about a registered candidate
102///
103/// Candidates exist independently of leadership. A process must register
104/// as a candidate before it can claim leadership.
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct CandidateInfo {
107 /// Unique identifier for the process
108 pub process_id: String,
109
110 /// Candidate's priority for leader selection
111 ///
112 /// Higher priority candidates can preempt lower priority leaders
113 /// when preemption is enabled.
114 pub priority: i32,
115
116 /// Last heartbeat timestamp (nanos since epoch)
117 ///
118 /// Used to determine if candidate is still alive.
119 pub last_heartbeat_nanos: u64,
120
121 /// Versionstamp from registration
122 ///
123 /// Fixed at registration time, never changes on heartbeat.
124 /// Provides global ordering of candidates.
125 pub versionstamp: [u8; 12],
126}
127
128impl CandidateInfo {
129 /// Check if this candidate is still alive
130 ///
131 /// # Arguments
132 /// * `current_time` - Current time as Duration since epoch
133 /// * `timeout` - Maximum time since last heartbeat
134 ///
135 /// # Returns
136 /// `true` if the candidate has sent a heartbeat within the timeout
137 pub fn is_alive(&self, current_time: Duration, timeout: Duration) -> bool {
138 let last_seen = Duration::from_nanos(self.last_heartbeat_nanos);
139 current_time.saturating_sub(last_seen) < timeout
140 }
141}
142
143/// Result of an election cycle
144///
145/// Returned by `run_election_cycle` to indicate whether this process
146/// is the leader or a follower.
147#[derive(Debug, Clone)]
148pub enum ElectionResult {
149 /// This process is the leader
150 Leader(LeaderState),
151
152 /// This process is a follower
153 ///
154 /// Contains the current leader state if one exists.
155 Follower(Option<LeaderState>),
156}
157
158impl ElectionResult {
159 /// Check if this result indicates leadership
160 pub fn is_leader(&self) -> bool {
161 matches!(self, ElectionResult::Leader(_))
162 }
163
164 /// Get the leader state, regardless of whether we are leader or follower
165 pub fn leader_state(&self) -> Option<&LeaderState> {
166 match self {
167 ElectionResult::Leader(state) => Some(state),
168 ElectionResult::Follower(Some(state)) => Some(state),
169 ElectionResult::Follower(None) => None,
170 }
171 }
172}
173
174/// Global configuration for the leader election system
175///
176/// Controls the behavior of the election system including lease duration,
177/// timeouts, and preemption policy.
178#[derive(Debug, Clone)]
179pub struct ElectionConfig {
180 /// How long a leadership lease lasts
181 ///
182 /// Leader must refresh before this expires to maintain leadership.
183 /// Longer values reduce election traffic but slow failover.
184 pub lease_duration: Duration,
185
186 /// Recommended heartbeat interval
187 ///
188 /// Candidates and leaders should send heartbeats at this interval.
189 /// Typically `lease_duration / 3` to ensure timely refresh.
190 pub heartbeat_interval: Duration,
191
192 /// How long before a candidate is considered dead
193 ///
194 /// Candidates that haven't sent a heartbeat within this duration
195 /// may be evicted from the candidate list.
196 pub candidate_timeout: Duration,
197
198 /// Master switch to enable/disable elections
199 ///
200 /// When disabled:
201 /// - No new leaders can be elected
202 /// - Existing leader remains until lease expires
203 /// - Registration and heartbeats return errors
204 pub election_enabled: bool,
205
206 /// Whether higher priority processes can preempt current leader
207 ///
208 /// If `true`, a candidate with higher priority can take over
209 /// leadership even if the current leader's lease is valid.
210 /// If `false`, must wait for lease to expire.
211 pub allow_preemption: bool,
212}
213
214impl Default for ElectionConfig {
215 /// Creates default configuration with sensible production values
216 ///
217 /// - 10 second lease duration
218 /// - 3 second heartbeat interval
219 /// - 15 second candidate timeout
220 /// - Elections enabled
221 /// - Preemption enabled
222 fn default() -> Self {
223 Self {
224 lease_duration: DEFAULT_LEASE_DURATION,
225 heartbeat_interval: DEFAULT_HEARTBEAT_INTERVAL,
226 candidate_timeout: DEFAULT_CANDIDATE_TIMEOUT,
227 election_enabled: true,
228 allow_preemption: true,
229 }
230 }
231}
232
233impl ElectionConfig {
234 /// Create a new ElectionConfig with custom lease duration
235 ///
236 /// Other values are derived from the lease duration:
237 /// - heartbeat_interval = lease_duration / 3
238 /// - candidate_timeout = lease_duration * 1.5
239 pub fn with_lease_duration(lease_duration: Duration) -> Self {
240 Self {
241 lease_duration,
242 heartbeat_interval: lease_duration / 3,
243 candidate_timeout: lease_duration + lease_duration / 2,
244 election_enabled: true,
245 allow_preemption: true,
246 }
247 }
248}