Files
zeekay 9e1396dc20 docs: three migration guides — Raft/Thrift/ZooKeeper -> Lux consensus
Split into one focused guide per source system + a short index:
- raft-to-lux: drop-in (Do->Commit, FSM->apply, Leader->deterministic writer)
- thrift-to-lux: RPC->ZAP transport+schema; coordination metastore->replog
- zookeeper-to-lux: config/locks/election/membership/watches folded in
Core throughout: replog (ordered replicated log) + pinned-writer for
non-commutative ops; leaderless, permissionless, PQ-final, ZAP-native.
2026-06-24 14:52:41 -07:00

2.9 KiB

Raft → Lux Consensus

For services on etcd/raft, hashicorp/raft, or seaweedfs/raft. Lux consensus (Quasar) is leaderless, permissionless, post-quantum-final, and ZAP-native — votes gossip over the zap-proto transport, no gRPC.

The insight

You don't need Raft — you need a replicated, totally-ordered command log applied identically on every replica. That is replog:

chain := consensus.NewChain(consensus.DefaultConfig())
log := replog.New(chain, apply)        // apply(payload []byte) error  == your FSM
go log.Run(ctx, 20*time.Millisecond)   // applies finalized commands, in order
log.Commit(ctx, marshal(cmd))          // propose + block until finalized & applied

Drop-in mapping

Raft Lux consensus
server.Do(cmd) log.Commit(ctx, marshal(cmd))
FSM Apply(*raft.Log) the apply func you pass to replog.New
Leader() + forward-to-leader gone — see Leaderless ≠ no writer
AddPeer / RemovePeer advisory; permissionless, no reconfiguration round
Snapshot / LoadSnapshot / Restore no-op — finality replaces snapshotting
SetElectionTimeout / heartbeat no-op — no election
IsLeader() gate cs.Writer() == cs.self (below)

The whole replacement is one type. Our reference (a storage master) swapped seaweedfs/raft and hashicorp/raft for a single replog-backed ConsensusServer with these methods as thin shims.

Leaderless ≠ no writer

The log is leaderless: any replica proposes, consensus totally-orders, and commutative commands (a monotonic counter, an LWW map) reconcile by order. But non-commutative ops — allocating IDs, growing/assigning resources, GC — double-allocate if N nodes run them at once.

Do not re-introduce an elected leader. Pin the writer deterministically to the lowest member address — every node computes the same one from the consensus-agreed membership, no election, no forwarding; when it leaves, the next-lowest takes over:

func (cs *ConsensusServer) Writer() string {
	w := cs.self
	for _, m := range cs.members {
		if m < w { w = m }
	}
	return w
}
// gate non-commutative ops on:  cs.Writer() == cs.self

An app-level role orthogonal to the leaderless log — the decomplected answer to "Raft handed me a leader for free."

Why it's better

No election stalls. No leader as a single point of failure. No forward-to-leader hop (lower tail latency). No reconfiguration round to add/evict a node. PQ-final (Quasar BLS + Corona + ML-DSA). And consensus rides your existing ZAP transport — no separate Raft RPC port, no gRPC.

Checklist

  • FSM expressed as apply(payload) error
  • writes via log.Commit; reads off applied in-memory state
  • non-commutative ops gated on the deterministic pinned writer
  • raft dependency removed from go.mod