Skip to main content

krabka_raft/kraft/controller/
startup.rs

1//! Construction of a running controller: the low-level spawn over an already
2//! seeded state, and [`KraftController::open`], which recovers the image,
3//! the control state and the durable quorum state from a data directory first.
4
5use {
6    super::{
7        Engine, KraftConfig, KraftControlState, KraftController, METADATA_LOG_CLEAN_INTERVAL,
8        PendingDowngradeSnapshot,
9        activation::{Activation, check_bootstrap_records},
10        applied_image::AppliedImage,
11        checkpoint::{BOOTSTRAP_SNAPSHOT_ID, latest_checkpoint_id, load_latest_checkpoint},
12        quorum_state_file::load_quorum_state,
13        records::features_epoch_after_snapshot,
14        recovery::{
15            CommittedReplay, control_state_at, replay_committed_with_timestamp,
16            replay_control_records, with_uncommitted_feature_levels,
17        },
18        timing::initial_election_at,
19    },
20    crate::{
21        config::{
22            ControllerFetchMissLimit, MetadataLogConfig, MetadataRaftCommandQueueCapacity,
23            MetadataRaftFetchMax,
24        },
25        error::RaftError,
26        event_queue::{EventQueueMeter, EventQueueRecorder},
27        kraft::{
28            core::{QuorumStateMachine, QuorumTimeouts},
29            log::KraftLog,
30            snapshot_fetch::MetadataSnapshotFetchMax,
31            transport::{CommandSender, PeerSender},
32            types::{NodeId, QuorumState},
33        },
34        snapshot::SnapshotReader,
35    },
36    krabka_ids::Offset,
37    krabka_metadata::{MetadataImage, MetadataRecord, VotersRecord, voters::VoterSet},
38    krabka_units::{
39        fmt::Human as _,
40        prelude::{ByteSize, Time},
41    },
42    std::{collections::BTreeMap, fs, path::PathBuf, sync::Arc, time::SystemTime},
43    tokio::{
44        sync::{mpsc, watch},
45        time::{Duration, Instant},
46    },
47    uuid::Uuid,
48};
49
50/// Metadata recovered before the controller engine is spawned.
51struct RecoveredMetadata {
52    image: MetadataImage,
53    last_snapshot_end_offset: Offset,
54    last_snapshot_timestamp_ms: i64,
55    last_applied_record_timestamp_ms: i64,
56    downgrade_snapshot_pending: Option<PendingDowngradeSnapshot>,
57    /// The finalized-features epoch of `image`, or `-1` before any features
58    /// delta.
59    features_epoch: i64,
60}
61
62impl KraftController {
63    /// Build the engine over an already-opened [`KraftLog`] and spawn its loop
64    /// task. Recovery (snapshot + replay + quorum-state file) is wired by
65    /// [`Self::open`]; this lower-level entrypoint takes the seed state directly
66    /// and is used by tests/drivers that supply their own [`KraftLog`].
67    ///
68    /// The returned handle's loop runs until [`Self::shutdown`] (or all handles
69    /// drop). `data_dir` is where the engine writes the quorum-state file and
70    /// checkpoints.
71    ///
72    /// # Errors
73    ///
74    /// Returns an error if starting the engine fails. A fresh controller has
75    /// no recovered downgrade boundary, so it needs no downgrade-checkpoint
76    /// recovery.
77    pub fn spawn(config: KraftConfig, log: KraftLog, data_dir: PathBuf) -> Result<Self, RaftError> {
78        let cluster_id = config.cluster_id;
79        let image = MetadataImage::new(cluster_id);
80        Self::spawn_with_image(config, log, data_dir, RecoveredMetadata {
81            image,
82            last_snapshot_end_offset: Offset(0),
83            last_snapshot_timestamp_ms: 0,
84            last_applied_record_timestamp_ms: 0,
85            downgrade_snapshot_pending: None,
86            features_epoch: -1,
87        })
88    }
89
90    /// Spawn the engine starting from an already-recovered [`MetadataImage`]
91    /// (the restart-recovery path through [`Self::open`] threads the rebuilt
92    /// image in here so the published `current_image` reflects it immediately).
93    fn spawn_with_image(
94        config: KraftConfig,
95        log: KraftLog,
96        data_dir: PathBuf,
97        recovered: RecoveredMetadata,
98    ) -> Result<Self, RaftError> {
99        let RecoveredMetadata {
100            image,
101            last_snapshot_end_offset,
102            last_snapshot_timestamp_ms,
103            last_applied_record_timestamp_ms,
104            downgrade_snapshot_pending,
105            features_epoch,
106        } = recovered;
107        let KraftConfig {
108            me,
109            directory_id,
110            mut initial_state,
111            timeouts,
112            heartbeat_interval,
113            controller_fetch_miss_limit,
114            metadata_raft_command_queue_capacity,
115            metadata_raft_fetch_max,
116            peers,
117            snapshot_interval_records,
118            max_bytes_between_snapshots,
119            max_snapshot_interval,
120            metadata_snapshot_fetch_max,
121            metadata_log,
122            activation,
123            event_queue_recorder,
124            ..
125        } = config;
126
127        // Every record in a cleanly reopened log is committed. Recover the
128        // latest control state before constructing the core so elections never
129        // briefly use stale configured voters.
130        replay_control_records(&log, &mut initial_state, metadata_raft_fetch_max)?;
131        let controls =
132            KraftControlState::new(initial_state.voters.clone(), initial_state.kraft_version);
133        let core = QuorumStateMachine::new(me, initial_state, timeouts);
134        let initial_leader = core.quorum_state().leader_id;
135        let initial_was_leader = core.role().is_leader();
136        let initial_epoch = core.quorum_state().leader_epoch;
137
138        // The controller voter set lives in the raft `QuorumState` (seeded from
139        // config under KIP-595 static voters, recovered from the quorum-state
140        // file on restart), NOT on the KIP-631-framed metadata log — `V1Voters`
141        // is a raft-control record with no KIP-631 counterpart. Mirror it into
142        // the published `MetadataImage` so image readers (e.g. the broker's
143        // voter-set views, auto-join) observe the live quorum membership.
144        let mut image = image;
145        image.apply(&MetadataRecord::V1KRaftVersion(
146            krabka_metadata::KRaftVersionRecord {
147                kraft_version: core.quorum_state().kraft_version,
148            },
149        ));
150        image.apply(&MetadataRecord::V1Voters(VotersRecord {
151            voters: core.quorum_state().voters.clone(),
152        }));
153
154        let (image, image_rx) = AppliedImage::new(image);
155        let (features_epoch_tx, features_epoch_rx) = watch::channel(features_epoch);
156        let (leader_tx, leader_rx) = watch::channel(initial_leader);
157        // Captured before the log moves into `Engine`; it seeds both the first
158        // published snapshot and the engine's `leader_reported_hwm`, which a
159        // node that has heard from no leader yet answers from.
160        let initial_hwm = log.hwm().0;
161        let initial_snapshot = super::queries::initial_quorum_snapshot(
162            &core,
163            &log,
164            initial_hwm,
165            last_snapshot_timestamp_ms,
166        );
167        let (quorum_tx, quorum_rx) = watch::channel(initial_snapshot);
168        let (fault_tx, fault_rx) = watch::channel(None);
169        let (cmd_tx, cmd_rx) = CommandSender::channel(metadata_raft_command_queue_capacity.get());
170
171        let clock_base = Instant::now();
172        // A fresh voter arms its election timer so a bootstrap cluster elects
173        // without an injected event. Observers/followers leave it disarmed.
174        let election_at = initial_election_at(
175            &core,
176            initial_leader,
177            clock_base,
178            me,
179            initial_epoch,
180            timeouts.election,
181        );
182
183        let engine_peers = Arc::clone(&peers);
184        let mut engine = Engine {
185            me,
186            core,
187            log,
188            image,
189            peers,
190            features_epoch,
191            features_epoch_tx,
192            leader_tx,
193            quorum_tx,
194            cmd_tx: cmd_tx.clone(),
195            event_queue: EventQueueMeter::new(event_queue_recorder),
196            turn: None,
197            fault_tx,
198            data_dir,
199            clock_base,
200            timeouts,
201            heartbeat_interval,
202            controller_fetch_miss_limit,
203            metadata_raft_fetch_max,
204            election_at,
205            fetch_at: None,
206            check_quorum_at: None,
207            fetch_misses: 0,
208            discovery_attempts: 0,
209            commit_waiters: Vec::new(),
210            was_leader: initial_was_leader,
211            held_epoch: initial_epoch,
212            registration_writes: BTreeMap::new(),
213            snapshot_interval_records,
214            max_bytes_between_snapshots,
215            max_snapshot_interval,
216            metadata_snapshot_fetch_max,
217            metadata_log,
218            noop_at: None,
219            clean_at: clock_base + METADATA_LOG_CLEAN_INTERVAL,
220            last_snapshot_end_offset,
221            last_snapshot_timestamp_ms,
222            last_applied_record_timestamp_ms,
223            pending_record_processing_time_us_total: 0,
224            pending_record_processing_time_samples: 0,
225            pending_record_processing_time_us_max: 0,
226            last_snapshot_at_ms: 0,
227            bytes_since_snapshot: 0,
228            snapshot_job: None,
229            snapshot_write_delay: Duration::ZERO,
230            downgrade_snapshot_pending,
231            #[cfg(test)]
232            downgrade_snapshot_failures_remaining: 0,
233            snapshot_fetch: None,
234            installed_snapshot_epoch: None,
235            controls,
236            directory_id,
237            observers: BTreeMap::new(),
238            fetch_purgatory: Vec::new(),
239            wall_clock_base: SystemTime::now(),
240            leader_reported_hwm: initial_hwm,
241            pending_reconfig: None,
242            activation,
243            activation_fault: None,
244            replay_fault: None,
245        };
246
247        // A restart can rediscover a committed downgrade whose earlier local
248        // checkpoint failed. Make one synchronous recovery attempt before the
249        // engine is exposed; the regular event loop keeps retrying thereafter.
250        while engine.downgrade_snapshot_pending.is_some() {
251            engine.write_downgrade_snapshot_and_prune()?;
252        }
253
254        tokio::spawn(engine.run(cmd_rx));
255
256        Ok(Self {
257            cmd_tx,
258            image_rx,
259            features_epoch_rx,
260            fault_rx,
261            leader_rx,
262            quorum_rx,
263            peers: engine_peers,
264            me,
265            registration_admission: Arc::default(),
266        })
267    }
268
269    /// Open the engine over `data_dir`: recover the [`MetadataImage`] from the
270    /// latest checkpoint + replay committed log batches, and seed the durable
271    /// [`QuorumState`] from the node-local quorum-state file. The
272    /// `bootstrap` voter set/cluster id is used only when no quorum-state file
273    /// exists yet.
274    ///
275    /// The bootstrap checkpoint, at offset 0 and epoch 0, is not a snapshot
276    /// of the log. The engine takes its KIP-853 control state, but it does
277    /// not apply its metadata records to the image. Those records replace
278    /// the bootstrap records of `activation` when there are any, and the
279    /// leader writes them to an empty log, as Kafka's
280    /// `QuorumController.handleLoadBootstrap` keeps them for its activation.
281    ///
282    /// # Errors
283    /// Returns [`RaftError`] if the log/checkpoint cannot be opened or read, or
284    /// `validate_image` refuses the committed image, or that image with every
285    /// feature level the log holds above its high watermark, before
286    /// checkpoint repair or an engine task can start.
287    #[tracing::instrument(
288        level = "info",
289        skip_all,
290        fields(node = me.0, %cluster_id, election_timeout = %timeouts.election.human(), fetch_timeout = %timeouts.fetch.human()),
291        err
292    )]
293    #[allow(
294        clippy::too_many_arguments,
295        reason = "recovery inputs are independent and explicit at this low-level boundary"
296    )]
297    pub fn open(
298        data_dir: PathBuf,
299        me: NodeId,
300        cluster_id: Uuid,
301        directory_id: Uuid,
302        bootstrap_voters: VoterSet,
303        timeouts: QuorumTimeouts,
304        heartbeat_interval: Option<Time>,
305        controller_fetch_miss_limit: ControllerFetchMissLimit,
306        metadata_raft_command_queue_capacity: MetadataRaftCommandQueueCapacity,
307        metadata_raft_fetch_max: MetadataRaftFetchMax,
308        peers: Arc<dyn PeerSender>,
309        snapshot_interval_records: u64,
310        max_bytes_between_snapshots: ByteSize,
311        max_snapshot_interval: Time,
312        metadata_snapshot_fetch_max: MetadataSnapshotFetchMax,
313        metadata_log: MetadataLogConfig,
314        mut activation: Activation,
315        event_queue_recorder: Arc<dyn EventQueueRecorder>,
316        validate_image: impl Fn(&MetadataImage) -> Result<(), RaftError>,
317    ) -> Result<Self, RaftError> {
318        fs::create_dir_all(&data_dir).map_err(krabka_log::LogError::Io)?;
319        let log = KraftLog::open(&data_dir, &metadata_log)?;
320
321        // Recover the image from the checkpoint plus only the durable committed
322        // prefix. An uncommitted voter record can remain at the log end after a
323        // crash and must not become authoritative during restart.
324        let mut image = MetadataImage::new(cluster_id);
325        let mut snapshot_control = None;
326        let mut last_snapshot_end_offset = Offset(0);
327        let mut last_snapshot_timestamp_ms = 0;
328        let mut features_epoch = -1;
329        let mut bootstrap_source = "the controller configuration";
330        if let Some(bytes) = load_latest_checkpoint(&data_dir)? {
331            let contents = SnapshotReader::read(&bytes)?;
332            // The records this checkpoint contains are below its boundary and
333            // gone from the log, so its header is the only place their
334            // create-time survives a restart.
335            last_snapshot_timestamp_ms = contents.last_contained_log_timestamp;
336            if latest_checkpoint_id(&data_dir) == Some(BOOTSTRAP_SNAPSHOT_ID) {
337                // Kafka's `KafkaRaftClient` gives the bootstrap checkpoint to
338                // `handleLoadBootstrap` and not to `handleLoadSnapshot`. Its
339                // records are not in the log, so they are not in the image:
340                // the leader writes them to the log, and every replica
341                // applies them from there.
342                if !contents.metadata_records.is_empty() {
343                    tracing::info!(
344                        records = contents.metadata_records.len(),
345                        "loaded the bootstrap records of the bootstrap checkpoint"
346                    );
347                    activation.bootstrap_records = contents.metadata_records;
348                    bootstrap_source = "the bootstrap checkpoint";
349                }
350            } else {
351                image = MetadataImage::from_records(cluster_id, &contents.metadata_records);
352                if let Some((end_offset, _)) = latest_checkpoint_id(&data_dir) {
353                    features_epoch = features_epoch_after_snapshot(
354                        &contents.metadata_records,
355                        end_offset,
356                        features_epoch,
357                    );
358                }
359            }
360            if let Some(control) = contents.control_state {
361                image.apply(&MetadataRecord::V1KRaftVersion(
362                    krabka_metadata::KRaftVersionRecord {
363                        kraft_version: control.kraft_version,
364                    },
365                ));
366                image.apply(&MetadataRecord::V1Voters(VotersRecord {
367                    voters: control.voters.clone(),
368                }));
369                snapshot_control = Some(control);
370            }
371            if let Some((off, _ep)) = latest_checkpoint_id(&data_dir) {
372                // Checkpoint filenames encode the raw offset (on-disk boundary).
373                last_snapshot_end_offset = Offset(off);
374            }
375            // Replay starts at the durable log start below. This deliberately
376            // rediscovers a downgrade when a crash landed after checkpoint
377            // rename but before prefix pruning.
378        }
379        check_bootstrap_records(&activation.bootstrap_records, bootstrap_source)?;
380        let CommittedReplay {
381            pending: mut downgrade_snapshot_pending,
382            last_applied_timestamp_ms: replayed_timestamp_ms,
383            features_epoch: replayed_features_epoch,
384        } = replay_committed_with_timestamp(
385            &log,
386            &mut image,
387            log.log_start_offset(),
388            metadata_raft_fetch_max,
389        )?;
390        let last_applied_record_timestamp_ms =
391            replayed_timestamp_ms.unwrap_or(last_snapshot_timestamp_ms);
392        // The replay starts at the log start, which can sit below a checkpoint
393        // whose prefix is not yet cleaned. A feature batch met there is older
394        // than the checkpoint's epoch and must not move it back.
395        let features_epoch =
396            replayed_features_epoch.map_or(features_epoch, |replayed| replayed.max(features_epoch));
397
398        let mut boundary_state = QuorumState::bootstrap(cluster_id, bootstrap_voters.clone());
399        if let Some(control) = &snapshot_control {
400            boundary_state.kraft_version = control.kraft_version;
401            boundary_state.voters = control.voters.clone();
402        }
403
404        // Seed the durable quorum state from the file, falling back to a fresh
405        // bootstrap when absent.
406        let mut initial_state = load_quorum_state(&data_dir, cluster_id, &bootstrap_voters)?
407            .unwrap_or_else(|| QuorumState::bootstrap(cluster_id, bootstrap_voters));
408        if let Some(control) = &snapshot_control {
409            initial_state.kraft_version = control.kraft_version;
410            initial_state.voters = control.voters.clone();
411        }
412        if let Some(pending) = &mut downgrade_snapshot_pending {
413            let boundary_state = control_state_at(
414                &log,
415                &boundary_state,
416                pending.end_offset,
417                metadata_raft_fetch_max,
418            )?;
419            pending.image.apply(&MetadataRecord::V1KRaftVersion(
420                krabka_metadata::KRaftVersionRecord {
421                    kraft_version: boundary_state.kraft_version,
422                },
423            ));
424            pending.image.apply(&MetadataRecord::V1Voters(VotersRecord {
425                voters: boundary_state.voters,
426            }));
427        }
428        replay_control_records(&log, &mut initial_state, metadata_raft_fetch_max)?;
429        image.apply(&MetadataRecord::V1KRaftVersion(
430            krabka_metadata::KRaftVersionRecord {
431                kraft_version: initial_state.kraft_version,
432            },
433        ));
434        image.apply(&MetadataRecord::V1Voters(VotersRecord {
435            voters: initial_state.voters.clone(),
436        }));
437
438        // The guard reads the committed image, which the engine starts from,
439        // and then every feature level the log holds above the high
440        // watermark: the high watermark recovered from its cache file can sit
441        // below a level that the engine would commit and then refuse. The
442        // committed image is read on its own too, because an uncommitted
443        // record can supersede a committed level that the engine still
444        // starts at.
445        validate_image(&image)?;
446        validate_image(&with_uncommitted_feature_levels(
447            &log,
448            &image,
449            metadata_raft_fetch_max,
450        )?)?;
451
452        Self::spawn_with_image(
453            KraftConfig {
454                me,
455                cluster_id,
456                directory_id,
457                initial_state,
458                timeouts,
459                heartbeat_interval,
460                controller_fetch_miss_limit,
461                metadata_raft_command_queue_capacity,
462                metadata_raft_fetch_max,
463                peers,
464                snapshot_interval_records,
465                max_bytes_between_snapshots,
466                max_snapshot_interval,
467                metadata_snapshot_fetch_max,
468                metadata_log,
469                activation,
470                event_queue_recorder,
471            },
472            log,
473            data_dir,
474            RecoveredMetadata {
475                image,
476                last_snapshot_end_offset,
477                last_snapshot_timestamp_ms,
478                last_applied_record_timestamp_ms,
479                downgrade_snapshot_pending,
480                features_epoch,
481            },
482        )
483    }
484}