feat(M6): token-ring incremental log sync — append-only op log, delta rides token round-2, nodes converge on identical history (e2e verified join events)

This commit is contained in:
2026-08-18 08:59:59 +08:00
parent f6c4b91a96
commit e59feb82c1
10 changed files with 635 additions and 34 deletions

View File

@ -16,9 +16,14 @@ import (
)
// LossTimeout returns the token-loss threshold per the authoritative design:
// roundDelay/2 + 20ms.
// roundDelay/2 + 20ms, floored at 600ms so a healthy fast ring is never
// misjudged as lost.
func LossTimeout(roundDelay time.Duration) time.Duration {
return roundDelay/2 + 20*time.Millisecond
t := roundDelay/2 + 20*time.Millisecond
if t < 600*time.Millisecond {
return 600 * time.Millisecond
}
return t
}
// inFlight tracks token-in-flight state (leader only).
@ -65,8 +70,9 @@ func (f *inFlight) lastDelay() time.Duration {
}
// flipIfComplete promotes the collect round to the sync round when every alive
// node has been stamped; at the end of sync it starts a fresh cycle. Only the
// leader flips.
// node has been stamped; at the end of sync it completes the cycle. Only the
// leader flips. Passed carries the set of nodes that saw this token this round;
// it is NOT reset between phases (only between cycles).
func (e *Engine) flipIfComplete(tk *Token) {
if e.state.LeaderID != e.ID {
return
@ -74,14 +80,15 @@ func (e *Engine) flipIfComplete(tk *Token) {
switch tk.Phase {
case PhaseCollect:
if allAlivePassed(&e.state, tk.Passed) {
// Round 1 done: go to round 2 (sync). Round-2 stamping restarts
// from the leader so the token walks the ring once more.
tk.Phase = PhaseSync
tk.Passed = nil
tk.Passed = []string{e.ID}
e.state.RoundDelay = tkDelaySince(tk)
}
case PhaseSync:
if allAlivePassed(&e.state, tk.Passed) {
// cycle done: start next collect round.
e.state.Cycle++
// Cycle done: leader holds token; next StartRing bumps cycle.
e.state.RoundDelay = tkDelaySince(tk)
}
}
@ -162,10 +169,33 @@ func (e *Engine) WatchLeader(ctx context.Context) {
return
case <-tick.C:
succ, ok := e.state.AliveSuccessor(e.ID)
if !ok {
if !ok || succ == e.ID {
continue // single-node ring: nothing to monitor
}
// Only the leader's predecessor (the previous alive node) monitors
// it; a node whose successor is the leader is the predecessor.
if e.state.LeaderID != succ {
continue
}
if e.state.LeaderID != succ {
pred, ok := e.state.AlivePredecessor(e.ID)
if !ok || pred == e.ID {
continue
}
// We monitor the leader only if we ARE its predecessor.
if succ != e.state.LeaderID {
continue
}
// pred must be us: leader's predecessor sends the heartbeat.
if e.state.Nodes[e.state.Find(succ)].ID == e.ID {
continue
}
if e.state.Find(e.ID) == -1 {
continue
}
// This node is the leader's predecessor iff leader's predecessor
// (alive) equals our ID.
leaderPred, ok := e.state.AlivePredecessor(e.state.LeaderID)
if !ok || leaderPred != e.ID {
continue
}
if e.send != nil {