Bootstrap a new cluster: this node is the founding member. `local_spki_pin` is the SHA-256 SPKI fingerprint of this node's own TLS leaf certificate; `None` in insecure transport mode. When present it is stored in the founding node's `NodeInfo` so that joining peers can pin our identity immediately.
(
config: &ClusterConfig,
catalog: &ClusterCatalog,
local_spki_pin: Option<[u8; 32]>,
)
| 21 | /// stored in the founding node's `NodeInfo` so that joining peers can pin our |
| 22 | /// identity immediately. |
| 23 | pub(super) fn bootstrap( |
| 24 | config: &ClusterConfig, |
| 25 | catalog: &ClusterCatalog, |
| 26 | local_spki_pin: Option<[u8; 32]>, |
| 27 | ) -> Result<ClusterState> { |
| 28 | info!( |
| 29 | node_id = config.node_id, |
| 30 | addr = %config.listen_addr, |
| 31 | groups = config.num_groups, |
| 32 | "bootstrapping new cluster" |
| 33 | ); |
| 34 | |
| 35 | // Create topology with this node. |
| 36 | let mut topology = ClusterTopology::new(); |
| 37 | topology.add_node( |
| 38 | NodeInfo::new(config.node_id, config.listen_addr, NodeState::Active) |
| 39 | .with_spki_pin(local_spki_pin), |
| 40 | ); |
| 41 | |
| 42 | // Create routing table: all groups on this single node. |
| 43 | let routing = RoutingTable::uniform( |
| 44 | config.num_groups, |
| 45 | &[config.node_id], |
| 46 | config.replication_factor.min(1), // Single node → RF=1. |
| 47 | ); |
| 48 | |
| 49 | // Create MultiRaft with all groups (single-node, no peers). |
| 50 | let mut multi_raft = MultiRaft::new(config.node_id, routing.clone(), config.data_dir.clone()) |
| 51 | .with_election_timeout(config.election_timeout_min, config.election_timeout_max); |
| 52 | for group_id in routing.group_ids() { |
| 53 | multi_raft.add_group(group_id, vec![])?; |
| 54 | } |
| 55 | |
| 56 | // Kick every group's election deadline into the past so the very |
| 57 | // first tick of `RaftLoop::run` elects this node as leader of each |
| 58 | // group. Otherwise an incoming `JoinRequest` that arrives before the |
| 59 | // random 150–300 ms election timeout fires would hit a non-leader |
| 60 | // node and be rejected. The bootstrap seed is by definition the only |
| 61 | // voter in every group, so self-election is unambiguous. |
| 62 | let now = std::time::Instant::now(); |
| 63 | for node in multi_raft.groups_mut().values_mut() { |
| 64 | node.election_deadline_override(now - std::time::Duration::from_millis(1)); |
| 65 | } |
| 66 | |
| 67 | // Generate cluster ID and persist everything. |
| 68 | let cluster_id = generate_cluster_id(); |
| 69 | catalog.save_cluster_id(cluster_id)?; |
| 70 | catalog.save_cluster_settings(&ClusterSettings::from_config(config))?; |
| 71 | catalog.save_topology(&topology)?; |
| 72 | catalog.save_routing(&routing)?; |
| 73 | |
| 74 | info!( |
| 75 | node_id = config.node_id, |
| 76 | cluster_id, |
| 77 | groups = config.num_groups, |
| 78 | "cluster bootstrapped" |
| 79 | ); |
| 80 |