foundationdb/recipes/leader_election/
types.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
// Copyright 2024 foundationdb-rs developers
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.

//! Core data structures for leader election
//!
//! This module defines the fundamental types used in the ballot-based
//! leader election algorithm, including leader state, candidate info, and configuration.
//!
//! # Design Overview
//!
//! The election algorithm uses a ballot-based approach (similar to Raft's term):
//! - **LeaderState**: Stored at a single key, contains ballot number and leader identity
//! - **CandidateInfo**: Per-candidate registration with versionstamp for ordering
//! - **Ballot Numbers**: Monotonically increasing, higher ballot always wins

use std::time::Duration;

/// Default lease duration for leadership
pub const DEFAULT_LEASE_DURATION: Duration = Duration::from_secs(10);

/// Default heartbeat interval (should be lease_duration / 3 approximately)
pub const DEFAULT_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(3);

/// Default candidate timeout (when to consider a candidate dead)
pub const DEFAULT_CANDIDATE_TIMEOUT: Duration = Duration::from_secs(15);

/// The core leader state - stored at a single key
///
/// Contains all information
/// needed to determine leadership without scanning candidates.
///
/// # Ballot Numbers
///
/// The ballot number works like Raft's term:
/// - Monotonically increasing counter
/// - Higher ballot always wins
/// - Prevents split-brain after recovery/partition
/// - Incremented on every leadership claim or refresh
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LeaderState {
    /// Ballot number (like Raft's term)
    ///
    /// Always increments when claiming or refreshing leadership.
    /// Higher ballot wins in any conflict.
    pub ballot: u64,

    /// Unique identifier of the leader process
    pub leader_id: String,

    /// Leader's priority (higher = more preferred)
    ///
    /// Used for preemption decisions when `allow_preemption` is enabled.
    pub priority: i32,

    /// Absolute timestamp when lease expires (nanos since epoch)
    ///
    /// Leadership is only valid while `current_time < lease_expiry_nanos`.
    pub lease_expiry_nanos: u64,

    /// Versionstamp assigned when this process registered
    ///
    /// Used for identity consistency and ordering.
    pub versionstamp: [u8; 12],
}

impl LeaderState {
    /// Check if the lease is still valid
    ///
    /// # Arguments
    /// * `current_time` - Current time as Duration since epoch
    ///
    /// # Returns
    /// `true` if the lease has not expired
    pub fn is_lease_valid(&self, current_time: Duration) -> bool {
        current_time.as_nanos() < self.lease_expiry_nanos as u128
    }

    /// Get remaining lease duration, if any
    ///
    /// # Arguments
    /// * `current_time` - Current time as Duration since epoch
    ///
    /// # Returns
    /// `Some(Duration)` if lease is still valid, `None` if expired
    pub fn remaining_lease(&self, current_time: Duration) -> Option<Duration> {
        let current_nanos = current_time.as_nanos() as u64;
        if current_nanos < self.lease_expiry_nanos {
            Some(Duration::from_nanos(
                self.lease_expiry_nanos - current_nanos,
            ))
        } else {
            None
        }
    }
}

/// Information about a registered candidate
///
/// Candidates exist independently of leadership. A process must register
/// as a candidate before it can claim leadership.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CandidateInfo {
    /// Unique identifier for the process
    pub process_id: String,

    /// Candidate's priority for leader selection
    ///
    /// Higher priority candidates can preempt lower priority leaders
    /// when preemption is enabled.
    pub priority: i32,

    /// Last heartbeat timestamp (nanos since epoch)
    ///
    /// Used to determine if candidate is still alive.
    pub last_heartbeat_nanos: u64,

    /// Versionstamp from registration
    ///
    /// Fixed at registration time, never changes on heartbeat.
    /// Provides global ordering of candidates.
    pub versionstamp: [u8; 12],
}

impl CandidateInfo {
    /// Check if this candidate is still alive
    ///
    /// # Arguments
    /// * `current_time` - Current time as Duration since epoch
    /// * `timeout` - Maximum time since last heartbeat
    ///
    /// # Returns
    /// `true` if the candidate has sent a heartbeat within the timeout
    pub fn is_alive(&self, current_time: Duration, timeout: Duration) -> bool {
        let last_seen = Duration::from_nanos(self.last_heartbeat_nanos);
        current_time.saturating_sub(last_seen) < timeout
    }
}

/// Result of an election cycle
///
/// Returned by `run_election_cycle` to indicate whether this process
/// is the leader or a follower.
#[derive(Debug, Clone)]
pub enum ElectionResult {
    /// This process is the leader
    Leader(LeaderState),

    /// This process is a follower
    ///
    /// Contains the current leader state if one exists.
    Follower(Option<LeaderState>),
}

impl ElectionResult {
    /// Check if this result indicates leadership
    pub fn is_leader(&self) -> bool {
        matches!(self, ElectionResult::Leader(_))
    }

    /// Get the leader state, regardless of whether we are leader or follower
    pub fn leader_state(&self) -> Option<&LeaderState> {
        match self {
            ElectionResult::Leader(state) => Some(state),
            ElectionResult::Follower(Some(state)) => Some(state),
            ElectionResult::Follower(None) => None,
        }
    }
}

/// Global configuration for the leader election system
///
/// Controls the behavior of the election system including lease duration,
/// timeouts, and preemption policy.
#[derive(Debug, Clone)]
pub struct ElectionConfig {
    /// How long a leadership lease lasts
    ///
    /// Leader must refresh before this expires to maintain leadership.
    /// Longer values reduce election traffic but slow failover.
    pub lease_duration: Duration,

    /// Recommended heartbeat interval
    ///
    /// Candidates and leaders should send heartbeats at this interval.
    /// Typically `lease_duration / 3` to ensure timely refresh.
    pub heartbeat_interval: Duration,

    /// How long before a candidate is considered dead
    ///
    /// Candidates that haven't sent a heartbeat within this duration
    /// may be evicted from the candidate list.
    pub candidate_timeout: Duration,

    /// Master switch to enable/disable elections
    ///
    /// When disabled:
    /// - No new leaders can be elected
    /// - Existing leader remains until lease expires
    /// - Registration and heartbeats return errors
    pub election_enabled: bool,

    /// Whether higher priority processes can preempt current leader
    ///
    /// If `true`, a candidate with higher priority can take over
    /// leadership even if the current leader's lease is valid.
    /// If `false`, must wait for lease to expire.
    pub allow_preemption: bool,
}

impl Default for ElectionConfig {
    /// Creates default configuration with sensible production values
    ///
    /// - 10 second lease duration
    /// - 3 second heartbeat interval
    /// - 15 second candidate timeout
    /// - Elections enabled
    /// - Preemption enabled
    fn default() -> Self {
        Self {
            lease_duration: DEFAULT_LEASE_DURATION,
            heartbeat_interval: DEFAULT_HEARTBEAT_INTERVAL,
            candidate_timeout: DEFAULT_CANDIDATE_TIMEOUT,
            election_enabled: true,
            allow_preemption: true,
        }
    }
}

impl ElectionConfig {
    /// Create a new ElectionConfig with custom lease duration
    ///
    /// Other values are derived from the lease duration:
    /// - heartbeat_interval = lease_duration / 3
    /// - candidate_timeout = lease_duration * 1.5
    pub fn with_lease_duration(lease_duration: Duration) -> Self {
        Self {
            lease_duration,
            heartbeat_interval: lease_duration / 3,
            candidate_timeout: lease_duration + lease_duration / 2,
            election_enabled: true,
            allow_preemption: true,
        }
    }
}