Apply a JoinResponse: reconstruct topology, routing, and MultiRaft from wire data. Order of operations is load-bearing for crash safety: 1. Reconstruct the `ClusterTopology` and `RoutingTable` in memory. 2. Persist topology + routing to the catalog **first**, before any on-disk side effects. If we crash after this step, the next boot sees `catalog.is_bootstrapped() == true` and takes the `restar
(
config: &ClusterConfig,
catalog: &ClusterCatalog,
transport: &NexarTransport,
resp: &JoinResponse,
)
| 240 | /// 4. Register every peer address in the transport before returning |
| 241 | /// so the first outgoing AppendEntries has a known destination. |
| 242 | fn apply_join_response( |
| 243 | config: &ClusterConfig, |
| 244 | catalog: &ClusterCatalog, |
| 245 | transport: &NexarTransport, |
| 246 | resp: &JoinResponse, |
| 247 | ) -> Result<ClusterState> { |
| 248 | // 1. Reconstruct topology. |
| 249 | let mut topology = ClusterTopology::new(); |
| 250 | for node in &resp.nodes { |
| 251 | let state = NodeState::from_u8(node.state).unwrap_or(NodeState::Active); |
| 252 | let spki_pin: Option<[u8; 32]> = node.spki_pin.as_deref().and_then(|b| { |
| 253 | if b.len() == 32 { |
| 254 | let mut arr = [0u8; 32]; |
| 255 | arr.copy_from_slice(b); |
| 256 | Some(arr) |
| 257 | } else { |
| 258 | None |
| 259 | } |
| 260 | }); |
| 261 | let mut info = NodeInfo::new( |
| 262 | node.node_id, |
| 263 | node.addr.parse().unwrap_or_else(|_| { |
| 264 | "0.0.0.0:0" |
| 265 | .parse() |
| 266 | .expect("invariant: \"0.0.0.0:0\" is a valid SocketAddr literal") |
| 267 | }), |
| 268 | state, |
| 269 | ) |
| 270 | .with_wire_version(node.wire_version) |
| 271 | .with_spiffe_id(node.spiffe_id.clone()) |
| 272 | .with_spki_pin(spki_pin); |
| 273 | // Override raft_groups from wire data (NodeInfo::new starts empty). |
| 274 | info.raft_groups = node.raft_groups.clone(); |
| 275 | if node.node_id == config.node_id { |
| 276 | info.state = NodeState::Active; |
| 277 | } |
| 278 | topology.add_node(info); |
| 279 | } |
| 280 | |
| 281 | // 1. Reconstruct routing table. |
| 282 | let mut group_members = std::collections::HashMap::new(); |
| 283 | for g in &resp.groups { |
| 284 | group_members.insert( |
| 285 | g.group_id, |
| 286 | GroupInfo { |
| 287 | leader: g.leader, |
| 288 | members: g.members.clone(), |
| 289 | learners: g.learners.clone(), |
| 290 | }, |
| 291 | ); |
| 292 | } |
| 293 | let routing = RoutingTable::from_parts(resp.vshard_to_group.clone(), group_members); |
| 294 | |
| 295 | // 2. Persist to catalog before any on-disk Raft side effects. |
| 296 | // Cluster id is written first so `is_bootstrapped()` returns |
| 297 | // `true` on any subsequent boot — without this, a joined node |
| 298 | // that restarts would re-enter the bootstrap/join path |
| 299 | // instead of taking `restart()`. Zero is a valid marker: the |
no test coverage detected