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}