mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-24 23:10:41 +02:00
* master: never auto-admit raft peers on a single-node configuration A master started without cluster peers (peers=none, or no peers so the list holds only itself) declares a one-node cluster, yet any foreign master that registered over KeepConnected was admitted as a second voter. The resulting two-voter cluster cannot reach quorum when the foreign peer is unreachable, wedging a previously healthy single master. Gate raft admission on having been configured with cluster peers; masters started with a peer list keep the announce-to-join flow used to grow the cluster. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * master: source SingleMaster from -peers=none, not the peer count A master started with an empty peer list ends up with a one-peer map containing itself, and NewMasterServer inferred declared single-master mode from that count. The master still mints a raft cluster through the bootstrap path, but AdmitRaftPeer then ignores every later joiner, so a deployment that grows from one master to several can never form a quorum. isSingleMasterMode already spells out the declared case (-peers=none); populate MasterOption.SingleMaster from it instead. * master: cover clustered raft admission with a real in-memory leader The previous clustered test had no raft leader and asserted nothing, so an AdmitRaftPeer that always returned early still passed. Bootstrap a single-node hashicorp raft over the in-memory transport, wait for leadership, and assert the leader reports and admits a missing peer. * master: return the raft admission result and assert committed join AdmitRaftPeer ran AddVoter on a detached goroutine, so the only observable state was an in-flight marker deleted on success and failure alike, and the clustered test could never tell whether the join worked. Admission now hands back a result channel; the test runs a connected second in-memory node and asserts the peer sees itself a committed voter. --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
120 lines
4.1 KiB
Go
120 lines
4.1 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
hashicorpRaft "github.com/hashicorp/raft"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/topology"
|
|
)
|
|
|
|
func TestSingleMasterNeverAdmitsRaftPeers(t *testing.T) {
|
|
ms := &MasterServer{option: &MasterOption{SingleMaster: true, Master: "localhost:9333"}}
|
|
|
|
// A foreign master announcing itself must not touch the admission path:
|
|
// ms.Topo is deliberately nil so any admission attempt panics the test.
|
|
if done := ms.AdmitRaftPeer("10.0.0.1:9333"); done != nil {
|
|
t.Fatal("single-master mode admitted a foreign raft peer")
|
|
}
|
|
}
|
|
|
|
func TestClusteredLeaderAdmitsMissingPeer(t *testing.T) {
|
|
topo := topology.NewTopology("topo", nil, 128*1024*1024, 0, false)
|
|
leader, leaderTransport := newInmemRaftLeader(t, "localhost:9333")
|
|
topo.HashicorpRaft = leader
|
|
ms := &MasterServer{
|
|
option: &MasterOption{Master: "localhost:9333"},
|
|
Topo: topo,
|
|
}
|
|
|
|
peer := pb.ServerAddress("10.0.0.1:9333")
|
|
name, missing := ms.missingRaftPeerName(peer)
|
|
if !missing || name != "10.0.0.1:9333" {
|
|
t.Fatalf("clustered leader did not report the missing raft peer: %q %v", name, missing)
|
|
}
|
|
peerRaft := newInmemRaftFollower(t, peer, leaderTransport)
|
|
|
|
done := ms.AdmitRaftPeer(peer)
|
|
if done == nil {
|
|
t.Fatal("clustered leader skipped admission of a missing raft peer")
|
|
}
|
|
if err := <-done; err != nil {
|
|
t.Fatalf("raft peer admission failed: %v", err)
|
|
}
|
|
|
|
// The joined node's own configuration is the committed view: it only
|
|
// learns the quorum it was admitted into through replication.
|
|
for deadline := time.Now().Add(10 * time.Second); ; {
|
|
if raftHasVoter(peerRaft, string(peer)) {
|
|
return
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("admitted peer never saw itself in the quorum: %v",
|
|
peerRaft.GetConfiguration().Configuration().Servers)
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
func raftHasVoter(r *hashicorpRaft.Raft, id string) bool {
|
|
for _, server := range r.GetConfiguration().Configuration().Servers {
|
|
if server.Suffrage == hashicorpRaft.Voter && string(server.ID) == id {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func newInmemRaftFollower(t *testing.T, self pb.ServerAddress, peerTransport *hashicorpRaft.InmemTransport) *hashicorpRaft.Raft {
|
|
t.Helper()
|
|
conf := hashicorpRaft.DefaultConfig()
|
|
conf.LocalID = hashicorpRaft.ServerID(self)
|
|
addr, transport := hashicorpRaft.NewInmemTransport(hashicorpRaft.ServerAddress(self.ToGrpcAddress()))
|
|
transport.Connect(peerTransport.LocalAddr(), peerTransport)
|
|
peerTransport.Connect(addr, transport)
|
|
r, err := hashicorpRaft.NewRaft(conf,
|
|
&StateMachine{topo: topology.NewTopology("fsm", nil, 128*1024*1024, 0, false)},
|
|
hashicorpRaft.NewInmemStore(), hashicorpRaft.NewInmemStore(),
|
|
hashicorpRaft.NewInmemSnapshotStore(), transport)
|
|
if err != nil {
|
|
t.Fatalf("new raft: %v", err)
|
|
}
|
|
t.Cleanup(func() { r.Shutdown() })
|
|
return r
|
|
}
|
|
|
|
func newInmemRaftLeader(t *testing.T, self string) (*hashicorpRaft.Raft, *hashicorpRaft.InmemTransport) {
|
|
t.Helper()
|
|
conf := hashicorpRaft.DefaultConfig()
|
|
conf.LocalID = hashicorpRaft.ServerID(self)
|
|
conf.HeartbeatTimeout = 50 * time.Millisecond
|
|
conf.ElectionTimeout = 50 * time.Millisecond
|
|
conf.LeaderLeaseTimeout = 25 * time.Millisecond
|
|
conf.CommitTimeout = 5 * time.Millisecond
|
|
addr, transport := hashicorpRaft.NewInmemTransport(hashicorpRaft.ServerAddress(self))
|
|
r, err := hashicorpRaft.NewRaft(conf,
|
|
&StateMachine{topo: topology.NewTopology("fsm", nil, 128*1024*1024, 0, false)},
|
|
hashicorpRaft.NewInmemStore(), hashicorpRaft.NewInmemStore(),
|
|
hashicorpRaft.NewInmemSnapshotStore(), transport)
|
|
if err != nil {
|
|
t.Fatalf("new raft: %v", err)
|
|
}
|
|
t.Cleanup(func() { r.Shutdown() })
|
|
|
|
if err := r.BootstrapCluster(hashicorpRaft.Configuration{
|
|
Servers: []hashicorpRaft.Server{{Suffrage: hashicorpRaft.Voter, ID: conf.LocalID, Address: addr}},
|
|
}).Error(); err != nil {
|
|
t.Fatalf("bootstrap cluster: %v", err)
|
|
}
|
|
for deadline := time.Now().Add(10 * time.Second); ; {
|
|
if r.State() == hashicorpRaft.Leader {
|
|
return r, transport
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatal("in-memory raft did not elect a leader")
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
}
|