M2 Raft Hardening — Status & Handoff¶
Current-main status:
IMPLEMENTEDwith focusedUNIT-PROVENfixtures for the source paths described below. The branch names retained in this document are historical provenance, not current deployment selectors. The loopback validator is a bounded lab mechanism, but this page does not claim that it was run for the current artifact. There is noLIVEor1M-CERTIFIEDevidence in this handoff.Scope: the multi-Raft follow-ups documented on
CONCEPT:EG-KG.sharding.raft-resharding— pooled per-peer connections, group-per-tenant-range routing, per-group snapshot scoping, leader balancing across groups, heartbeat coalescing, and the openraft 0.10 storage/network migration.
File/line references identify the implementation path on current main unless a
section explicitly says it is historical provenance.
UNIT-PROVEN — implemented + focused-lib-tested¶
1. Pooled per-peer Raft connections — CONCEPT:AU-KG.ontology.manage-arbitrary¶
- What: the scaffold opened a fresh
TcpStreamper append/vote/snapshot RPC and dropped it after one round-trip (a connect+handshake per heartbeat, per peer, per group). A sharedPeerPoolnow keeps a bounded set of WARM connections per peer address, reused across RPCs and across ALL groups on the node (one pool perMultiRaft, mirroring the already-shared inbound listener). - Where:
src/raft/network.rs:94struct PeerPool(idle map +opens/reusescounters).src/raft/network.rs:148PeerPool::round_trip— take-warm-or-connect, with the stale-retry contract: a reused connection that fails on the first frame is discarded and the call retries ONCE on a fresh connection, so openraft never sees a spurious failure. Wire is strict request→response on one stream (no correlation id), so a connection is loaned out exclusively for one round-trip and only returned to the idle set if it SUCCEEDED.src/raft/network.rs:220GroupNetworkFactorycarriespool: Arc<PeerPool>;src/raft/network.rs:264GroupNetworkClient::round_tripnow callsself.pool.round_trip(...)instead ofTcpStream::connectper call.src/raft/multi.rs:179MultiRaft.poolfield, created atmulti.rs:223(network::PeerPool::new()), threaded into every group's factory atmulti.rs:328; exposed viaMultiRaft::pool()(multi.rs:263).- Proven by:
src/raft/tests.rs:600peer_pool_reuses_warm_connection— three sequential RPCs to one echo peer pay exactly ONE TCP connect (opens()==1,reuses()>=2).src/raft/tests.rs:642peer_pool_retries_on_stale_connection— an echo server that closes after one frame forces a reconnect; the second round-trip still succeeds (opens()==2, no hard error).
2. Group-per-tenant-range routing ring — CONCEPT:AU-KG.ingest.mirror-inbound¶
- What:
GroupRouter::group_ofnow resolvesoverride → tenant-range ring → DEFAULT_GROUP. The ring is a sorted, de-duplicated set of group ids that un-pinned graphs hash-distribute across via a stable FNV-1a hash (identical on every node, never persisted — NOTRandomState). With no ring configured (the default) every un-pinned graph maps toDEFAULT_GROUP— byte-for-byte the single-group scaffold. - Where:
src/raft/multi.rs:62GroupRouter.ringfield +fnv1ahelper.src/raft/multi.rs:86group_of,multi.rs:101set_group_ring,multi.rs:110group_ring.src/raft/multi.rsMultiRaft::configure_group_ring(n, peers, bootstrap)— brings up groups0..nwith the complete configured peer set and installs the ring;n<=1is a no-op (single-group). Every non-default group therefore has the same quorum replication and failover contract as the default group.- Proven by:
src/raft/tests.rs:678group_router_distributes_tenants_across_ring— empty ring ⇒ all toDEFAULT_GROUP; a 4-group ring spreads 200 tenants across ≥2 groups; deterministic on repeat;assignoverride beats the ring;is_cross_shardworks across real ranges; empty ring collapses back to default.
3. Per-group snapshot scoping — CONCEPT:AU-KG.ingest.staged¶
- What: a group's snapshot dump is SCOPED to the graphs whose tenant range resolves
to THIS group, so a large tenant in one group never bloats another group's snapshot.
Snapshot membership is enumerated from the complete graph catalog, not the resident
cache, so cold/evicted graphs remain recoverable. In Raft snapshot schema 4, each graph
is serialized once as its complete raw durable row set; decoded node/edge/semantic copies are forbidden
because they duplicate memory/wire cost, expose plaintext beside encrypted rows, and can
disagree with durable authority. Install validates the durable
graph_metaidentity, rejects cross-group rows, reconstructs and atomically publishes the serving projection with its exact durable incarnation, and removes stale same-group graphs absent from the snapshot. A store opened WITHOUT a router (the direct/single-store path) dumps the whole registry, preserving the scaffold behavior. - Where:
src/raft/mod.rsAppCtx.router: Option<Arc<multi::GroupRouter>>— threaded through everyAppCtxconstruction (node/harnesses/tests =None;MultiRaft::create_groupbuilds astore_ctxcarryingSome(self.router)atsrc/raft/multi.rs:311).src/raft/store.rsdump_graphsfilters the fullregistry.list()catalog byrouter.group_of(name) == self.group_idwhen a router is present.src/raft/store.rsscoped_snapshot_graph_names(#[cfg(test)]) — the test seam.- Proven by:
src/raft/tests.rsgroup_snapshot_is_scoped_to_its_tenant_range— two graphs pinned to non-default groups 3 and 5; group 3's snapshot carries ONLY residentgraphA, group 5's ONLY catalog-only/evictedgraphB, and the DEFAULT group (0) carries ONLY the un-pinned bootstrap__commons__(no bleed or cold-graph loss); a router-less store dumps the WHOLE catalog (__commons__+ both graphs).
Historical repository evidence: the cited release build and focused Raft
tests passed for the implementation record. This is UNIT-PROVEN source
evidence, not a current live deployment, multi-host soak, or 1M certification.
UNIT-PROVEN R1/R2/R3 — historical branch feat/m2-r1r3-completion¶
All three remaining M2 items are now implemented + lib-tested on top of the base. Single-group default behavior is unchanged — the full pre-existing
--lib raftsuite (incl. the 3-node failover test + the nemesis gauntlets) stays green. Concept-ID note: the original plan reserved EG-KG.storage.kg-kg-2 (R1) and KG-2.269 (R2). By the time this branch landed, KG-2.269 had been claimed by another session (shard-commons, "Ingestion graph routing"), so the heartbeat work uses a fresh id. Final mapping: R3 = EG-KG.storage.kg-kg-2, R1 = EG-KG.sharding.multi-raft, R2 = EG-KG.storage.concept-2 (all reserved in the ledger).
R3. Per-group multi-NODE membership join — CONCEPT:EG-KG.storage.kg-kg-2¶
- What: a group can now be grown from a single-node bootstrap to span multiple NODES via the openraft add-learner → change-membership lifecycle, per group, over the shared listener.
- Where (
src/raft/multi.rs): join_group(gid, peers)— stand a group up on a node as an EMPTY, non-bootstrapping member (neverinitializes) ready to receive replication.add_group_member(gid, new_node, addr)— leader-side:add_learner(.., blocking)(blocks until the joiner is up-to-date) thenchange_membership(voters ∪ {new}).remove_group_member(gid, node)— leader-sidechange_membershipto a reduced voter set; idempotent; refuses to remove the LAST voter.group_membership(gid)— the current sorted VOTER set from the replicated metrics.- Proven by:
src/raft/tests.rsmulti_node_group_join_then_leader_rebalance— node 1 single-member-bootstraps group 7, nodes 2/3join_groupit empty, the leaderadd_group_members both → membership becomes[1,2,3], and a write through the leader replicates to the two freshly-joined voters.
R1. Leader balancing across groups — CONCEPT:EG-KG.sharding.multi-raft¶
- What:
MultiRaft::rebalance_leaders()spreads leadership so one node isn't leader for every group. EVERY node runs the pass (like a real cluster); per group it computes the deterministic round-robin target (desired_leader(gid, sorted_voters)— identical on every node, no coordination) and either claims a group it should lead (triggers an election, rate-limited per group byELECT_COOLDOWN= 10s so it never flaps) or yields a group it leads but shouldn't (disables its heartbeat so the lease expires and the target can take over). No-op for single-voter groups + already-balanced groups. Returns aRebalanceReport { targets, elected, yielded, errors }. - openraft-0.9 reality (important): openraft 0.9.24 has NO
transfer_leader(it arrives in 0.10 astrigger_transfer_leader), and its anti-disruption / leader- stickiness makes followers REJECT a challenger's vote while they still hear from a healthy leader. So a triggered election ALONE cannot move leadership off a healthy incumbent — the incumbent must cooperatively yield (stop heartbeating). That is the yield half above; with both halves the cluster converges to the round-robin spread within a couple of election timeouts. - Where (
src/raft/multi.rs):desired_leader(pure fn),RebalanceReport,MultiRaft::rebalance_leaders+may_trigger_elect(cooldown),last_electfield. - Proven by:
src/raft/tests.rsdesired_leader_round_robin_spreads_across_voters(unit: the selection spreads across all voters, deterministic) and the integration half ofmulti_node_group_join_then_leader_rebalance— over the 3-voter group 7 the target is node 2 (7 % 3), the first pass shows node 1yielded+ node 2elected, periodic passes converge leadership to node 2, and a further pass is a no-op (idempotent).
R2. Heartbeat coalescing — CONCEPT:EG-KG.storage.concept-2¶
- What: same-destination Raft HEARTBEATS (empty-entries
AppendEntries) across N groups coalesce into ONE batched frame to that peer instead of N frames per heartbeat interval; the batch rides the AU-KG.ontology.manage-arbitraryPeerPool(one connect-amortized round-trip). Only heartbeats coalesce — log-bearing appends / votes / snapshots stay individual (latency/ordering-sensitive). - Where (
src/raft/network.rs): RaftFrame { One(GroupRpc) | Batch(Vec<GroupRpc>) }+RaftFrameReply— the top-level wire envelope; a single-RPC frame isOne, so the existing per-group path is behavior-identical (and the 3-node + 2-group tests exercise it).HeartbeatCoalescer— buffers heartbeats per peer (offergates onis_heartbeat),drain_batches()builds one ordered batch per peer,send_batch()ships it over the pool and returns the ordered per-RPC replies;coalesced/flushescounters.src/raft/multi.rsserve_conndemuxes aBatchby dispatching each tagged sub-RPC to its group and replying in the SAME order.- Proven by:
src/raft/tests.rsheartbeat_coalescer_batches_per_peer(unit: bucket-by-peer, non-heartbeats refused, counters) andcoalesced_batch_round_trips_on_one_connection(a 3-heartbeat batch demuxes to 3 ordered replies over EXACTLY ONE TCP connect on a live listener).
Historical repository evidence: the cited build and focused Raft suite
reported 23 passing tests for that implementation record. This remains
UNIT-PROVEN evidence only; it is not a live or 1M result.
IMPLEMENTED / UNIT-PROVEN — openraft 0.9 → 0.10 migration + native handoff (CONCEPT:AU-KG.backend.authority-has-already-acked)¶
Branch
feat/openraft-010-cluster. Bumped openraft to 0.10 (pinned=0.10.0-alpha.26, the line that carriestransfer_leader) and migrated the wholesrc/raft/API surface to the v2 split-storage model. The cooperative yield-then-claim leader balancing (EG-KG.sharding.multi-raft) is replaced by the native graceful handoff. Seedocs/architecture/cluster_deployment.mdfor the multi-node deploy.
- Storage = v2 split traits. openraft 0.10 removed the combined
RaftStorage+ theAdaptor.EgStorenow implementsRaftLogStorage(+ super-traitRaftLogReader) ANDRaftStateMachine(+RaftSnapshotBuilder) onArc<EgStore>;create_grouppasses the SAMEArcas both (no adaptor). Every storage method returnsstd::io::Error(the 0.9StorageError/StorageIOErrorconstructors are gone); types use the…Of<C>aliases.appendnow signals durability via anIOFlushedcallback (fired right after our group-commit fsync);applyconsumes aStreamof(entry, responder)andsends each response;delete_conflict_logs_since/purge_logs_uptobecametruncate_after/purge; the chunkedinstall_snapshotbecame full-snapshot transfer. - Network =
RaftNetworkV2. The deprecated v1RaftNetworkwas deleted; the client implementsRaftNetworkV2(blanket-deriving theNet*sub-traits the factory needs). The snapshot RPC is now one taggedfull_snapshotframe (vote + meta + body) → the follower'sinstall_full_snapshot.RPCError<C>is single-generic. A newtransfer_leaderRPC forwards the handoff notification to the target. - R1 native instant handoff (DONE).
MultiRaft::rebalance_leadersno longer yields heartbeats — for each group THIS node leads whose round-robin target is elsewhere, it issues the nativeraft.trigger().transfer_leader(target)(graceful, near-instant; openraft hands a fresh term + the leader vote to the target and notifies it to campaign at once).RebalanceReport.{elected,yielded}collapse totransferred. Rate-limited per group byTRANSFER_COOLDOWNso it never spams transfers. - Focused tests: the cited
cargo test --features "full,cluster" --lib raftresult is repositoryUNIT-PROVENevidence for the 0.10 API and native transfer.scripts/validate-raft-cluster.shis the bounded lab route; a run against a specific artifact must be recorded separately asLAB-PROVEN.
STILL REMAINING — needs real multi-node hardware (not in-process testable)¶
- Both: true cross-host soak (leadership actually rebalancing across machines, heartbeat-frame reduction under load) needs multi-node hardware; the in-process harness proves correctness, not the distributed performance win.
NE-171 source closure — cadence, durable group authority, bounded balance¶
The source seams described above are now wired in the current tree; root-owned validation remains the release gate:
HeartbeatCoalesceris installed on every liveGroupNetworkFactory. Empty-entryappend_entriescalls enqueue per destination, wait through a bounded 5 ms window, and receive ordered replies from oneRaftFrame::Batch; log-bearing appends, votes, snapshots, and transfer notifications retain the single-RPC path. The queue and frame remain bounded by the existing peer/RPC caps, and shutdown fails pending waiters before groups are drained.- Existing on-disk redb shard count is reconciled into
RaftClusterConfig.groupsbeforeDEFAULT_GROUPor the tenant ring starts. A configured group count remains the fresh-store request; an existing layout wins until an offline shard migration. - A 15 s manager-owned scheduler invokes the existing native
transfer_leaderpath. It observes current leaders, requires a one-slot load margin (equal loads are hysteresis), permits one automatic transfer per pass, enforces the existing 10 s per-group cooldown, and fails closed when current/target failure domains are equal or unknown.EPISTEMIC_GRAPH_RAFT_FAILURE_DOMAINScan provide an explicit complete node→domain map; absent that map, the advertised endpoint host is the safety domain. - Source fixtures cover the batch frame contract, durable-count reconciliation, deterministic domain parsing/safety, and existing single-node/cluster term and no-churn paths. A cross-host soak and measured performance benefit are deliberately not claimed here.
See also: Capabilities matrix · Engine Scaling Program · Catalog-driven Resharding · Cluster Deployment · Distribution / Robotics / GPU.