Files
seaweedfs/weed/cluster/cluster.go
T
Chris Lu 190ab32cfb vector: weed-vector registers to master; s3 discovers it there (#12146)
* vector: weed-vector registers with the weed master

weed-vector-server --masters holds a KeepConnected stream to the weed
master, the same mechanism filer and s3 use via wdclient — the server
shows up in ListClusterNodes(client_type=vector). Resends registration
and rediscovers on failure.

* vector: s3 discovers weed-vector through the master

With -vectors.grpc unset the vectors gateway resolves one registered
weed-vector via ListClusterNodes on the masters it already derives
from filer config — discovery replaces the static dial for the
single-worker OSS path. The conformance harness registers the engine
to mini's master and drops -vectors.grpc.

* vector: default-fill registration fields

..Default::default() keeps the source compiling against proto variants
that carry extra fields, so the same file works in either tree.

* vector: serve the Vectors API on the main s3 port

The vectors router sits on the main -port listener behind a dispatch
that checks the SigV4 credential scope for /s3vectors/ — no separate
port needed. -port.vectors stays for explicit dedicated-port
deployments; the conformance harness now hits vectors through :8334.

* vector: drop -port.vectors entirely

It was new — no compat to preserve. The main -port listener serves the
API whenever a weed-vector backend is reachable.

* vector: drop -vectors.grpc; discovery is the only locator

weed-vector always registers to master, the gateway always resolves it
through ListClusterNodes — a static address just forks membership.

* vector: binary is weed-vector

Matches the enterprise binary; package and lib names are unchanged.

* vector: review fixes — cluster type, grpc port, lazy resolve, leader hint, advertise

- cluster: register 'vector' as a node type so ListClusterNodes returns
  weed-vector members
- s3: dial masters via ToGrpcAddress (HTTP port + 10000)
- s3: backend resolves lazily through vectorDiscovery — workers joining
  after gateway startup work on first query, and address changes re-dial
- master_client: convert master HTTP addr to grpc +10000, honor http(s)
  scheme, follow the leader hint in KeepConnectedResponse, and
  --advertise covers wildcard/unreachable listen addresses

* vector: no --advertise flag; derive the outbound address

A wildcard listen resolves to the source IP the master sees via a
route lookup, so registration always advertises something reachable
without another flag.

* vector: review fixes — plaintext dispatch, conn leaks, discovery backoff

The two plaintext http servers now serve the vectors dispatch too —
signed s3vectors calls on the main port reach the right router.
discoverVectorWorker closes each master conn and bounds lookups to 5s;
a dead or absent worker no longer re-hits master on every RPC — the
resolver backs off for 5s between rediscovery attempts.

* vector: 1 GiB grpc cap, dead-worker invalidation, CLI rename

Valid PutVectors batches exceed the tonic 4 MiB default — match the
gateway's 1 GiB cap. A replaced worker no longer sticks: any RPC error
resets the cached address and the next call rediscovers through the
master. The core CLI bin renames to weed-vector-cli so the two crates
do not collide on the same output path.

* vector: retry-only-transport reset, periodic re-resolve, cli rename

- reset() takes the failed client and runs only on transport errors —
  an InvalidArgument on a healthy conn no longer interrupts unrelated
  requests, and a concurrent re-resolve wins the race.
- cli() re-resolves once the interval passes even with a live client,
  so a worker that moves addresses is picked up.
- verify.rs spike docs use weed-vector-cli after the bin rename.

* vector: only Unavailable counts as a transport failure

A request deadline or an application error can fire on a healthy
connection — resetting the shared conn on those churned reconnects.
2026-10-28 00:58:31 -07:00

205 lines
6.2 KiB
Go

package cluster
import (
"sync"
"time"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
)
const (
MasterType = "master"
VolumeServerType = "volumeServer"
FilerType = "filer"
BrokerType = "broker"
S3Type = "s3"
VectorType = "vector"
)
type FilerGroupName string
type DataCenter string
type Rack string
type ClusterNode struct {
Address pb.ServerAddress
Version string
counter int
CreatedTs time.Time
DataCenter DataCenter
Rack Rack
}
type ClusterNodeGroups struct {
groupMembers map[FilerGroupName]*GroupMembers
sync.RWMutex
}
type Cluster struct {
filerGroups *ClusterNodeGroups
brokerGroups *ClusterNodeGroups
s3Groups *ClusterNodeGroups
vectorGroups *ClusterNodeGroups
}
func newClusterNodeGroups() *ClusterNodeGroups {
return &ClusterNodeGroups{
groupMembers: map[FilerGroupName]*GroupMembers{},
}
}
func (g *ClusterNodeGroups) getGroupMembers(filerGroup FilerGroupName, createIfNotFound bool) *GroupMembers {
members, found := g.groupMembers[filerGroup]
if !found && createIfNotFound {
members = newGroupMembers()
g.groupMembers[filerGroup] = members
}
return members
}
func (g *ClusterNodeGroups) AddClusterNode(filerGroup FilerGroupName, nodeType string, dataCenter DataCenter, rack Rack, address pb.ServerAddress, version string) []*master_pb.KeepConnectedResponse {
g.Lock()
defer g.Unlock()
m := g.getGroupMembers(filerGroup, true)
if t := m.addMember(dataCenter, rack, address, version); t != nil {
return buildClusterNodeUpdateMessage(true, filerGroup, nodeType, address)
}
return nil
}
func (g *ClusterNodeGroups) RemoveClusterNode(filerGroup FilerGroupName, nodeType string, address pb.ServerAddress) []*master_pb.KeepConnectedResponse {
g.Lock()
defer g.Unlock()
m := g.getGroupMembers(filerGroup, false)
if m == nil {
return nil
}
if m.removeMember(address) {
return buildClusterNodeUpdateMessage(false, filerGroup, nodeType, address)
}
return nil
}
func (g *ClusterNodeGroups) ListClusterNode(filerGroup FilerGroupName) (nodes []*ClusterNode) {
g.Lock()
defer g.Unlock()
m := g.getGroupMembers(filerGroup, false)
if m == nil {
return nil
}
for _, node := range m.members {
nodes = append(nodes, node)
}
return
}
func NewCluster() *Cluster {
return &Cluster{
filerGroups: newClusterNodeGroups(),
brokerGroups: newClusterNodeGroups(),
s3Groups: newClusterNodeGroups(),
vectorGroups: newClusterNodeGroups(),
}
}
func (cluster *Cluster) AddClusterNode(ns, nodeType string, dataCenter DataCenter, rack Rack, address pb.ServerAddress, version string) []*master_pb.KeepConnectedResponse {
filerGroup := FilerGroupName(ns)
switch nodeType {
case FilerType:
return cluster.filerGroups.AddClusterNode(filerGroup, nodeType, dataCenter, rack, address, version)
case BrokerType:
return cluster.brokerGroups.AddClusterNode(filerGroup, nodeType, dataCenter, rack, address, version)
case S3Type:
return cluster.s3Groups.AddClusterNode(filerGroup, nodeType, dataCenter, rack, address, version)
case VectorType:
return cluster.vectorGroups.AddClusterNode(filerGroup, nodeType, dataCenter, rack, address, version)
case MasterType:
return buildClusterNodeUpdateMessage(true, filerGroup, nodeType, address)
}
return nil
}
func (cluster *Cluster) RemoveClusterNode(ns string, nodeType string, address pb.ServerAddress) []*master_pb.KeepConnectedResponse {
filerGroup := FilerGroupName(ns)
switch nodeType {
case FilerType:
return cluster.filerGroups.RemoveClusterNode(filerGroup, nodeType, address)
case BrokerType:
return cluster.brokerGroups.RemoveClusterNode(filerGroup, nodeType, address)
case S3Type:
return cluster.s3Groups.RemoveClusterNode(filerGroup, nodeType, address)
case VectorType:
return cluster.vectorGroups.RemoveClusterNode(filerGroup, nodeType, address)
case MasterType:
return buildClusterNodeUpdateMessage(false, filerGroup, nodeType, address)
}
return nil
}
func (cluster *Cluster) ListClusterNode(filerGroup FilerGroupName, nodeType string) (nodes []*ClusterNode) {
switch nodeType {
case FilerType:
return cluster.filerGroups.ListClusterNode(filerGroup)
case BrokerType:
return cluster.brokerGroups.ListClusterNode(filerGroup)
case S3Type:
return cluster.s3Groups.ListClusterNode(filerGroup)
case VectorType:
return cluster.vectorGroups.ListClusterNode(filerGroup)
case MasterType:
}
return
}
// ListClusterNodeUpdates reports the current members as add updates, so a
// client that just connected can rebuild the membership it missed while it was
// away.
func (cluster *Cluster) ListClusterNodeUpdates(filerGroup FilerGroupName, nodeType string) (updates []*master_pb.KeepConnectedResponse) {
for _, node := range cluster.ListClusterNode(filerGroup, nodeType) {
updates = append(updates, buildClusterNodeUpdateMessage(true, filerGroup, nodeType, node.Address)...)
}
return
}
// IsKnownNode reports whether address is currently registered under nodeType
// in any filer group. The lookup is intentionally group-agnostic because callers
// (e.g. Ping admission) only know the target address, not the group it joined.
func (cluster *Cluster) IsKnownNode(nodeType string, address pb.ServerAddress) bool {
var groups *ClusterNodeGroups
switch nodeType {
case FilerType:
groups = cluster.filerGroups
case BrokerType:
groups = cluster.brokerGroups
case S3Type:
groups = cluster.s3Groups
case VectorType:
groups = cluster.vectorGroups
default:
return false
}
groups.RLock()
defer groups.RUnlock()
for _, members := range groups.groupMembers {
if _, found := members.members[address]; found {
return true
}
// fall back to a port-tolerant comparison so callers that omit the
// grpc-port suffix still match a registered peer
for stored := range members.members {
if stored.Equals(address) {
return true
}
}
}
return false
}
func buildClusterNodeUpdateMessage(isAdd bool, filerGroup FilerGroupName, nodeType string, address pb.ServerAddress) (result []*master_pb.KeepConnectedResponse) {
result = append(result, &master_pb.KeepConnectedResponse{
ClusterNodeUpdate: &master_pb.ClusterNodeUpdate{
FilerGroup: string(filerGroup),
NodeType: nodeType,
Address: string(address),
IsAdd: isAdd,
},
})
return
}