Files
neuron-tai/docs/issues/38-tracker-seed-join.md
2026-07-06 14:17:36 +03:00

3.2 KiB

US-038 — Tracker cluster join via a single seed peer

Status: planned Priority: High (blocks adding trackers without restarting the fleet) Stage: Proposed

Context

Today the tracker cluster is fully static. RaftNode and NodeGossip (packages/tracker/meshnet_tracker/raft.py, gossip.py) take a peer list at construction and never change it; /v1/raft/status does not expose membership.

Consequences of starting a new tracker with only one existing peer in --cluster-peers:

  • Existing trackers never learn about the newcomer — they keep heartbeating and replicating to their original list only.
  • Quorum math ((len(self.peers) + 1) / 2) differs per tracker, so vote counts and commit decisions disagree — the cluster silently splits rather than erroring.

Required behavior: a joining tracker is configured with any one live tracker (a seed). It announces itself, the membership change replicates through the Raft log, and every tracker — including the newcomer — converges on the same full peer list. Removing the need to restart existing trackers when the hive grows.

Design

  1. Expose membership. GET /v1/cluster/peers returns {"self": <url>, "peers": [<url>, ...]} (admin-safe: URLs only).
  2. Join handshake. On startup, the joiner calls the seed's POST /v1/cluster/join with its public self-url. The request is hive-HMAC-signed (MESHNET_HIVE_SECRET, same fail-closed rule as gossip — an unauthenticated join on a public tracker would let anyone enter the hive). A non-leader seed forwards to the current leader (same pattern as /v1/nodes/register forwarding).
  3. Membership through the log. The leader appends a cluster-membership entry (single-server change: add one peer per entry). RaftNode gains apply-side handling that swaps self.peers — quorum is recomputed from the applied membership, never from local config. Gossip peer list updates from the same apply hook.
  4. Joiner bootstrap. After a successful join response (which includes the current peer list), the joiner sets its own peers and starts Raft as a follower; it catches up via normal append-entries.
  5. Persistence. Applied membership is written to the stats/state sqlite so a restarted tracker rejoins with the last known list even if its seed is down.
  6. Config semantics. --cluster-peers becomes "seed list": tried in order until one join succeeds. A tracker with no seeds and no persisted membership runs standalone (current single-tracker behavior unchanged).

Out of scope: peer removal/eviction (leave via operator restart for now), joint consensus for multi-server changes, automatic seed retry after startup.

Acceptance criteria

  • Start trackers A+B as a cluster; start C with only A as seed → within one election timeout, A, B, and C all report the same 3-peer membership on GET /v1/cluster/peers, and a value proposed on C commits on A and B
  • Join without a valid hive signature is rejected with 403; join to a follower is forwarded to the leader transparently
  • Restarting C with its seed offline rejoins from persisted membership
  • Standalone tracker (no seeds) behaves exactly as today
  • python -m pytest passes from repo root