fix: 心跳复活节点、inflight 超时重发

1. handleClusterToken:心跳到达时把发送方(前驱)标记为 alive。
   之前 forwardToNext 超时把节点 MarkOffline 后就再无途径恢复,
   即使下一个心跳证明它活着。心跳是 leader 侧纠正误判的唯一入口。

2. forwardToNext:遍历完所有后继才返回,不要提前 inflight.clear()。
   保留 inflight 状态,让 WatchTokenLoss 超时后 StartRing 重发,
   重发时 AliveSuccessor 会重新计算,心跳刚复活的节点就能被选中。

这两处补丁一起提交,形成完整的存活复活闭环。
This commit is contained in:
JianFeeeee
2026-08-28 16:32:51 +08:00
parent 15b1dcce8f
commit f9c2e9476a
2 changed files with 39 additions and 1 deletions

View File

@ -155,7 +155,9 @@ func (e *Engine) forwardToNext(ctx context.Context, tk *Token) error {
}
// Non-leader neighbor death: continue to the next recipient.
}
e.inflight.clear()
// 全环遍历完毕,所有后继都不可达(或已全部尝试过)。
// 保持 inflight不清空让 WatchTokenLoss 超时后重发。
// 重发时 AliveSuccessor 重新计算,可能已被心跳复活。
return nil
}

View File

@ -9,6 +9,8 @@ import (
"fmt"
"io"
"io/fs"
"log"
"net"
"net/http"
"os"
"path/filepath"
@ -538,11 +540,31 @@ func (h *Handler) handleClusterToken(w http.ResponseWriter, r *http.Request) {
// predecessor's WatchLeader heartbeat fail → MarkOffline → becomeLeader
// → StartRing, healing the ring. Per design: "心跳拒绝应当发生在leader
// 退出节点时让leader上邻居意识到当前环已经没有节点了".
//
// 同时心跳到达说明前驱还活着leader 应在地自己的拓扑中把它标记为
// alive。否则 forwardToNext 一次超时把前驱 MarkOffline 后就再没有
// 途径恢复它——前驱明明活着leader 却认为它 offline → AliveSuccessor
// 找不到后继,令牌冻结在 leader 手上。心跳是 leader 侧纠正误判的唯一
// 入口(因为心跳只有前驱发给 leader 这一条边)。
if r.Body == nil {
if s := h.Ring.State(); len(s.Nodes) <= 1 {
http.Error(w, "standalone node", http.StatusConflict)
return
}
// 心跳到达 = 前驱活着,标记 alive纠正 forwardToNext 的误判)
if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
s := h.Ring.State()
for i := range s.Nodes {
if h, _, e := net.SplitHostPort(s.Nodes[i].Addr); e == nil && h == host {
if !s.Nodes[i].Alive {
log.Printf("heartbeat from %s revives it (was offline)", s.Nodes[i].ID)
}
s.Nodes[i].Alive = true
s.Nodes[i].LastSeen = time.Now().Unix()
break
}
}
}
w.WriteHeader(http.StatusOK)
return
}
@ -552,6 +574,20 @@ func (h *Handler) handleClusterToken(w http.ResponseWriter, r *http.Request) {
http.Error(w, "standalone node", http.StatusConflict)
return
}
// 同上:心跳到达 = 前驱活着
if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
s := h.Ring.State()
for i := range s.Nodes {
if h, _, e := net.SplitHostPort(s.Nodes[i].Addr); e == nil && h == host {
if !s.Nodes[i].Alive {
log.Printf("heartbeat from %s revives it (was offline)", s.Nodes[i].ID)
}
s.Nodes[i].Alive = true
s.Nodes[i].LastSeen = time.Now().Unix()
break
}
}
}
w.WriteHeader(http.StatusOK)
return
}