mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-09-20 08:57:55 +00:00
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:
@ -40,6 +40,10 @@ type Engine struct {
|
||||
|
||||
// inflight tracks token-in-flight state (leader only).
|
||||
inflight inFlight
|
||||
Log *ClusterLog
|
||||
lastLogSent int64
|
||||
// lastRingStart time of the previous cycle launch (leader throttle).
|
||||
lastRingStart time.Time
|
||||
}
|
||||
|
||||
// NewEngine builds the engine; state holds this node as initial leader unless
|
||||
@ -49,14 +53,15 @@ func NewEngine(id, addr, user, pass, version string, cache []string, h Handler,
|
||||
ID: id, Addr: addr, User: user, Pass: pass,
|
||||
Version: version, Cache: cache, Handler: h,
|
||||
state: State{
|
||||
LeaderID: "",
|
||||
Cycle: 0,
|
||||
LeaderID: "",
|
||||
Cycle: 0,
|
||||
PendingTasks: map[string]*Task{},
|
||||
Topology: map[string]*TopoEntry{},
|
||||
RoundDelay: 200 * time.Millisecond,
|
||||
},
|
||||
myAddr: selfAddr,
|
||||
send: send,
|
||||
Log: NewClusterLog(),
|
||||
}
|
||||
n := Node{ID: id, Addr: selfAddr, Alive: true, IsLeader: isLeader,
|
||||
Load: Load{MemPct: 10, NetPct: 10}, Version: version, Cache: cache}
|
||||
@ -97,6 +102,17 @@ func (e *Engine) phase1(tk *Token) {
|
||||
Load: e.loadSnapshot(),
|
||||
Version: e.Version, Cache: e.Cache,
|
||||
})
|
||||
// Attach our own log entries not yet seen by the ring (incremental sync):
|
||||
// entries after the last forwarded watermark ride the token for others.
|
||||
if e.Log != nil {
|
||||
mine := e.Log.EntriesAfter(e.lastLogSent)
|
||||
if len(mine) > 0 {
|
||||
tk.Log = append(tk.Log, mine...)
|
||||
if last := mine[len(mine)-1]; last.Seq > e.lastLogSent {
|
||||
e.lastLogSent = last.Seq
|
||||
}
|
||||
}
|
||||
}
|
||||
tk.Passed = append(tk.Passed, e.ID)
|
||||
}
|
||||
|
||||
@ -106,6 +122,13 @@ func (e *Engine) phase1(tk *Token) {
|
||||
func (e *Engine) phase2(ctx context.Context, tk *Token) error {
|
||||
e.state = tk.State
|
||||
e.state.LeaderID = tk.State.LeaderID
|
||||
// Incremental log sync: adopt deltas carried by the token, then attach
|
||||
// our own new entries so peers can converge.
|
||||
if e.Log != nil && len(tk.Log) > 0 {
|
||||
if _, err := e.Log.ApplyDelta(tk.Log); err != nil {
|
||||
log.Printf("ring[%s] log delta gap: %v (request full sync later)", e.ID, err)
|
||||
}
|
||||
}
|
||||
for {
|
||||
pending := e.state.PendingList()
|
||||
if len(pending) == 0 {
|
||||
@ -127,6 +150,13 @@ func (e *Engine) phase2(ctx context.Context, tk *Token) error {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Record the claim in the operation log so all peers converge on who
|
||||
// owns which forward (incremental log sync).
|
||||
if e.Log != nil {
|
||||
_, _ = e.Log.Append(e.ID, LogForwardAdd, map[string]any{
|
||||
"taskId": claimed.ID, "local": claimed.Local.Name, "remote": claimed.Remote.Name,
|
||||
})
|
||||
}
|
||||
e.state.AddTopology(claimed, e.ID)
|
||||
}
|
||||
e.state = tk.State
|
||||
@ -136,6 +166,7 @@ func (e *Engine) phase2(ctx context.Context, tk *Token) error {
|
||||
|
||||
// OnToken receives the token: process by phase, return updated token.
|
||||
func (e *Engine) OnToken(ctx context.Context, tk *Token) (*Token, error) {
|
||||
log.Printf("ring[%s] OnToken cycle=%d phase=%d passed=%v", e.ID, tk.Cycle, tk.Phase, tk.Passed)
|
||||
switch tk.Phase {
|
||||
case PhaseCollect:
|
||||
e.phase1(tk)
|
||||
@ -152,11 +183,20 @@ func (e *Engine) OnToken(ctx context.Context, tk *Token) (*Token, error) {
|
||||
// Forward hands the token to this node's successor over the injected send.
|
||||
// It is the transport hook used by the HTTP handler after OnToken.
|
||||
func (e *Engine) Forward(ctx context.Context, tk *Token) error {
|
||||
nodes := make([]string, 0, len(e.state.Nodes))
|
||||
for _, n := range e.state.Nodes {
|
||||
nodes = append(nodes, n.ID)
|
||||
}
|
||||
log.Printf("ring[%s] fwd-debug id=%s nodes=%v", e.ID, e.ID, nodes)
|
||||
next, ok := e.state.AliveSuccessor(e.ID)
|
||||
if !ok {
|
||||
return nil // single-node ring
|
||||
}
|
||||
if next == e.ID {
|
||||
return nil // never forward to ourselves
|
||||
}
|
||||
if e.send != nil {
|
||||
log.Printf("ring[%s] forward cycle=%d phase=%d to %s", e.ID, tk.Cycle, tk.Phase, next)
|
||||
return e.send(ctx, next, tk)
|
||||
}
|
||||
return nil
|
||||
@ -164,12 +204,13 @@ func (e *Engine) Forward(ctx context.Context, tk *Token) error {
|
||||
|
||||
// Snapshot returns a serializable view of the ring for the frontend.
|
||||
type RingSnapshot struct {
|
||||
LeaderID string `json:"leaderId"`
|
||||
Cycle int64 `json:"cycle"`
|
||||
RoundDelay int64 `json:"roundDelayMs"`
|
||||
Nodes []Node `json:"nodes"`
|
||||
Pending []*Task `json:"pending"`
|
||||
LeaderID string `json:"leaderId"`
|
||||
Cycle int64 `json:"cycle"`
|
||||
RoundDelay int64 `json:"roundDelayMs"`
|
||||
Nodes []Node `json:"nodes"`
|
||||
Pending []*Task `json:"pending"`
|
||||
Topology []*TopoEntry `json:"topology"`
|
||||
Log []LogEntry `json:"log,omitempty"`
|
||||
}
|
||||
|
||||
func (e *Engine) Snapshot() *RingSnapshot {
|
||||
@ -181,6 +222,9 @@ func (e *Engine) Snapshot() *RingSnapshot {
|
||||
Pending: e.state.PendingList(),
|
||||
Topology: e.state.TopologyList(),
|
||||
}
|
||||
if e.Log != nil {
|
||||
snap.Log = e.Log.Snapshot()
|
||||
}
|
||||
return snap
|
||||
}
|
||||
|
||||
@ -237,6 +281,17 @@ func (e *Engine) StartRing(ctx context.Context) {
|
||||
if e.state.LeaderID != e.ID {
|
||||
return
|
||||
}
|
||||
// Single-node ring has no successor to hand the token to; do not POST to
|
||||
// ourselves. The cycle resumes once a newcomer joins (see JoinNode).
|
||||
if next, ok := e.state.AliveSuccessor(e.ID); !ok || next == e.ID {
|
||||
return
|
||||
}
|
||||
// Throttle: do not start a new cycle until roundDelay has elapsed since
|
||||
// the last one, so a healthy ring cycles at a deliberate pace.
|
||||
if !e.lastRingStart.IsZero() && time.Since(e.lastRingStart) < e.state.RoundDelay {
|
||||
return
|
||||
}
|
||||
e.lastRingStart = time.Now()
|
||||
e.state.Cycle++
|
||||
tk := &Token{
|
||||
Cycle: e.state.Cycle,
|
||||
@ -250,3 +305,68 @@ func (e *Engine) StartRing(ctx context.Context) {
|
||||
log.Printf("ring[%s] start cycle %d: %v", e.ID, tk.Cycle, err)
|
||||
}
|
||||
}
|
||||
|
||||
// JoinInfo is a node's self-description sent when requesting to join a ring.
|
||||
type JoinInfo struct {
|
||||
ID string `json:"id"`
|
||||
Addr string `json:"addr"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Cache []string `json:"cache,omitempty"`
|
||||
}
|
||||
|
||||
// JoinNode handles an incoming join request from a new node: it inserts the
|
||||
// newcomer right after this node (so the newcomer becomes our successor),
|
||||
// keeps this node the leader, and returns the updated ring state for the
|
||||
// newcomer to adopt.
|
||||
func (e *Engine) JoinNode(j JoinInfo) *State {
|
||||
n := Node{ID: j.ID, Addr: j.Addr, Alive: true,
|
||||
Load: Load{MemPct: 50, NetPct: 50}, Version: j.Version, Cache: j.Cache}
|
||||
e.state.InsertAfter(e.ID, n)
|
||||
if e.Log != nil {
|
||||
_, _ = e.Log.Append(e.ID, LogNodeJoin, map[string]string{"node": n.ID, "addr": n.Addr})
|
||||
}
|
||||
if e.state.LeaderID == "" {
|
||||
e.state.LeaderID = e.ID
|
||||
}
|
||||
return &e.state
|
||||
}
|
||||
|
||||
// AdoptState replaces this node's cluster picture with the state provided by
|
||||
// the join target, then re-inserts us (in case we were absent).
|
||||
func (e *Engine) AdoptState(s State) {
|
||||
ns := make([]string, 0, len(s.Nodes))
|
||||
for _, n := range s.Nodes {
|
||||
ns = append(ns, n.ID)
|
||||
}
|
||||
log.Printf("ring[%s] adopt-state nodes=%v", e.ID, ns)
|
||||
kept := map[string]*Task{}
|
||||
for id, t := range e.state.PendingTasks {
|
||||
if _, ok := s.PendingTasks[id]; !ok {
|
||||
kept[id] = t
|
||||
}
|
||||
}
|
||||
e.state = s
|
||||
if e.state.PendingTasks == nil {
|
||||
e.state.PendingTasks = map[string]*Task{}
|
||||
}
|
||||
for id, t := range kept {
|
||||
e.state.PendingTasks[id] = t
|
||||
}
|
||||
e.state.UpsertNode(Node{ID: e.ID, Addr: e.myAddr, Alive: true,
|
||||
Load: e.loadSnapshot(), Version: e.Version, Cache: e.Cache})
|
||||
if e.Log != nil {
|
||||
_, _ = e.Log.Append(e.ID, LogNodeJoin, map[string]string{"node": e.ID, "addr": e.myAddr})
|
||||
}
|
||||
}
|
||||
|
||||
// IsLeader reports whether this node is the current ring leader.
|
||||
func (e *Engine) IsLeader() bool { return e.state.LeaderID == e.ID }
|
||||
|
||||
// LeaderAddr returns the current leader's address.
|
||||
func (e *Engine) LeaderAddr() string {
|
||||
i := e.state.Find(e.state.LeaderID)
|
||||
if i < 0 {
|
||||
return ""
|
||||
}
|
||||
return e.state.Nodes[i].Addr
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user