Build a `JoinResponse` for an incoming `JoinRequest`. See module docs for semantics. Mutates `topology` only when the node is newly admitted; idempotent for re-joins with the same address. `cluster_id` is the id of the cluster this node belongs to — the join flow reads it from the local catalog and threads it through so the joining node can persist it and take the `restart()` path on a subsequen
(
req: &JoinRequest,
topology: &mut ClusterTopology,
routing: &RoutingTable,
cluster_id: u64,
)
| 37 | /// catalog has not yet been populated; rejection responses also carry |
| 38 | /// zero. |
| 39 | pub fn handle_join_request( |
| 40 | req: &JoinRequest, |
| 41 | topology: &mut ClusterTopology, |
| 42 | routing: &RoutingTable, |
| 43 | cluster_id: u64, |
| 44 | ) -> JoinResponse { |
| 45 | // Validate the wire version carried in the JOIN payload (belt-and-suspenders |
| 46 | // check; the transport-level handshake already negotiated a compatible version |
| 47 | // before this RPC was dispatched). The `wire_version` field here is the |
| 48 | // cluster-wide schema version (`CLUSTER_WIRE_FORMAT_VERSION`), distinct from |
| 49 | // the transport-level RPC frame version. We require an exact match because |
| 50 | // this build uses floor == ceiling (no backward-compat window in the schema). |
| 51 | if req.wire_version != CLUSTER_WIRE_FORMAT_VERSION { |
| 52 | warn!( |
| 53 | node_id = req.node_id, |
| 54 | joiner_wire_version = req.wire_version, |
| 55 | expected_wire_version = CLUSTER_WIRE_FORMAT_VERSION, |
| 56 | "join request rejected: joiner cluster wire_version mismatch" |
| 57 | ); |
| 58 | return reject(format!( |
| 59 | "joiner wire_version {} does not match this cluster's wire_version {} — \ |
| 60 | rolling upgrade is required before this node can join", |
| 61 | req.wire_version, CLUSTER_WIRE_FORMAT_VERSION |
| 62 | )); |
| 63 | } |
| 64 | |
| 65 | // Validate the listen address early. |
| 66 | let addr: SocketAddr = match req.listen_addr.parse() { |
| 67 | Ok(a) => a, |
| 68 | Err(e) => { |
| 69 | return reject(format!("invalid listen_addr '{}': {e}", req.listen_addr)); |
| 70 | } |
| 71 | }; |
| 72 | |
| 73 | // Collision / idempotency check — both require reading the existing entry. |
| 74 | if let Some(existing) = topology.get_node(req.node_id) { |
| 75 | let existing_addr = existing.addr.clone(); |
| 76 | if existing_addr != req.listen_addr { |
| 77 | // Same id, different address — reject. |
| 78 | return reject(format!( |
| 79 | "node_id {} already registered with different address {} (request: {})", |
| 80 | req.node_id, existing_addr, req.listen_addr |
| 81 | )); |
| 82 | } |
| 83 | // Same id, same address. If already Active we short-circuit — |
| 84 | // no topology mutation at all, just rebuild the wire response. If the |
| 85 | // node was in a non-Active state (Joining/Draining), normalize to |
| 86 | // Active now because it's clearly back online. |
| 87 | if existing.state != NodeState::Active |
| 88 | && let Some(entry) = topology.get_node_mut(req.node_id) |
| 89 | { |
| 90 | entry.state = NodeState::Active; |
| 91 | } |
| 92 | return build_response(topology, routing, cluster_id); |
| 93 | } |
| 94 | |
| 95 | // Brand new node — admit as Active. Stamp the joiner's own |
| 96 | // wire version and identity fields onto its NodeInfo so every |