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.
This commit is contained in:
Chris Lu authored and GitHub committed 2026-10-28 00:58:31 -07:00
1 parent 81879fba7b
commit 190ab32cfb
15 files changed
+423 -116

No files matched your search

+241 -97
View File
@@ -5,12 +5,12 @@ import (
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"net/http"
"os"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
@@ -20,6 +20,7 @@ import (
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/pb/s3_lifecycle_pb"
"github.com/seaweedfs/seaweedfs/weed/pb/s3_pb"
"github.com/seaweedfs/seaweedfs/weed/pb/vector_pb"
@@ -36,6 +37,8 @@ import (
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
"github.com/seaweedfs/seaweedfs/weed/util/version"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var (
@@ -55,8 +58,6 @@ type S3Options struct {
portGrpc *int
portIceberg *int
portLance *int
portVectors *int
vectorsGrpc *string
icebergCredentialRole *string
icebergCredentialDuration *int
config *string
@@ -115,8 +116,6 @@ func init() {
s3StandaloneOptions.portGrpc = cmdS3.Flag.Int("port.grpc", 0, "s3 server grpc listen port")
s3StandaloneOptions.portIceberg = cmdS3.Flag.Int("port.iceberg", 8181, "Iceberg REST Catalog server listen port (0 to disable)")
s3StandaloneOptions.portLance = cmdS3.Flag.Int("port.lance", 9101, "Lance Namespace server listen port (0 to disable); credential vending uses -iceberg.credentialRole")
s3StandaloneOptions.portVectors = cmdS3.Flag.Int("port.vectors", 0, "S3 Vectors API server listen port (0 to disable); requires -vectors.grpc")
s3StandaloneOptions.vectorsGrpc = cmdS3.Flag.String("vectors.grpc", "", "weed-vector-server gRPC address for the S3 Vectors API")
s3StandaloneOptions.icebergCredentialRole = cmdS3.Flag.String("iceberg.credentialRole", "", "IAM role ARN the Iceberg catalog assumes to vend table-scoped credentials (empty disables vending)")
s3StandaloneOptions.icebergCredentialDuration = cmdS3.Flag.Int("iceberg.credentialDurationSeconds", 3600, "lifetime of credentials vended by the Iceberg catalog")
s3StandaloneOptions.domainName = cmdS3.Flag.String("domainName", "", "suffix of the host name in comma separated list, {bucket}.{domainName}")
@@ -440,11 +439,19 @@ func (s3opt *S3Options) startS3Server() bool {
}
}
// Start S3 Vectors API server if enabled
if s3opt.portVectors != nil && *s3opt.portVectors > 0 {
if httpS := s3opt.startVectorsServer(s3ApiServer, grpcDialOption, listenOnLocalhost); httpS != nil {
httpServers = append(httpServers, httpS)
}
// The Vectors API rides the main -port listener whenever a weed-vector
// backend is reachable — /s3vectors/ in the SigV4 credential scope picks
// the vectors router.
vectorsHandler := http.Handler(router)
if vectorsRouter := s3opt.vectorsRouter(s3ApiServer, grpcDialOption, masterAddresses); vectorsRouter != nil {
vr, mainRouter := vectorsRouter, router
vectorsHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if isVectorsRequest(r) {
vr.ServeHTTP(w, r)
return
}
mainRouter.ServeHTTP(w, r)
})
}
if runtime.GOOS != "windows" {
@@ -455,7 +462,7 @@ func (s3opt *S3Options) startS3Server() bool {
}
if s3SocketListener := pb.ListenOnLocalSocket(localSocket); s3SocketListener != nil {
s3SocketListener = &util.Listener{Listener: s3SocketListener, Timeout: idleTimeoutSeconds(s3opt.idleTimeout)}
socketServer := newHttpServer(router, nil)
socketServer := newHttpServer(vectorsHandler, nil)
httpServers = append(httpServers, socketServer)
go func() {
if err := socketServer.Serve(s3SocketListener); err != nil && err != http.ErrServerClosed {
@@ -530,7 +537,7 @@ func (s3opt *S3Options) startS3Server() bool {
if *s3opt.portHttps == 0 {
glog.V(0).Infof("Start Seaweed S3 API Server %s at https port %d", version.Version(), *s3opt.port)
if s3ApiLocalListener != nil {
localServer := newHttpServer(router, tlsConfig)
localServer := newHttpServer(vectorsHandler, tlsConfig)
httpServers = append(httpServers, localServer)
go func() {
if err := localServer.ServeTLS(s3ApiLocalListener, "", ""); err != nil && err != http.ErrServerClosed {
@@ -538,7 +545,7 @@ func (s3opt *S3Options) startS3Server() bool {
}
}()
}
httpS := newHttpServer(router, tlsConfig)
httpS := newHttpServer(vectorsHandler, tlsConfig)
httpServers = append(httpServers, httpS)
shutdown := s3opt.newShutdown(stopGrpcServer, s3ApiServer, httpServers)
if err = httpS.ServeTLS(s3ApiListener, "", ""); err != nil && err != http.ErrServerClosed {
@@ -554,7 +561,7 @@ func (s3opt *S3Options) startS3Server() bool {
glog.Fatalf("S3 API HTTPS listener on %s:%d error: %v", *s3opt.bindIp, *s3opt.portHttps, err)
}
if s3ApiLocalListenerHttps != nil {
localHttpsServer := newHttpServer(router, tlsConfig)
localHttpsServer := newHttpServer(vectorsHandler, tlsConfig)
httpServers = append(httpServers, localHttpsServer)
go func() {
if err := localHttpsServer.ServeTLS(s3ApiLocalListenerHttps, "", ""); err != nil && err != http.ErrServerClosed {
@@ -562,7 +569,7 @@ func (s3opt *S3Options) startS3Server() bool {
}
}()
}
httpsServer := newHttpServer(router, tlsConfig)
httpsServer := newHttpServer(vectorsHandler, tlsConfig)
httpServers = append(httpServers, httpsServer)
go func() {
if err := httpsServer.ServeTLS(s3ApiListenerHttps, "", ""); err != nil && err != http.ErrServerClosed {
@@ -574,7 +581,7 @@ func (s3opt *S3Options) startS3Server() bool {
if *s3opt.tlsPrivateKey == "" || *s3opt.portHttps > 0 {
glog.V(0).Infof("Start Seaweed S3 API Server %s at http port %d", version.Version(), *s3opt.port)
if s3ApiLocalListener != nil {
localServer := newHttpServer(router, nil)
localServer := newHttpServer(vectorsHandler, nil)
httpServers = append(httpServers, localServer)
go func() {
if err := localServer.Serve(s3ApiLocalListener); err != nil && err != http.ErrServerClosed {
@@ -582,7 +589,7 @@ func (s3opt *S3Options) startS3Server() bool {
}
}()
}
httpS := newHttpServer(router, nil)
httpS := newHttpServer(vectorsHandler, nil)
httpServers = append(httpServers, httpS)
shutdown := s3opt.newShutdown(stopGrpcServer, s3ApiServer, httpServers)
if err = httpS.Serve(s3ApiListener); err != nil && err != http.ErrServerClosed {
@@ -702,107 +709,221 @@ func (s3opt *S3Options) startLanceServer(s3ApiServer *s3api.S3ApiServer, listenO
return httpS
}
// startVectorsServer starts the S3 Vectors API server on a separate port. It
// fans index and data calls out to the shared VectorService over gRPC; with no
// -vectors.grpc the API is not served.
func (s3opt *S3Options) startVectorsServer(s3ApiServer *s3api.S3ApiServer, grpcDialOption grpc.DialOption, listenOnLocalhost bool) *http.Server {
if *s3opt.vectorsGrpc == "" {
glog.Warningf("port.vectors set without -vectors.grpc; S3 Vectors API disabled")
return nil
}
conn, err := pb.GrpcDial(context.Background(), *s3opt.vectorsGrpc, false, grpcDialOption)
if err != nil {
glog.Errorf("S3 Vectors: dial %s: %v", *s3opt.vectorsGrpc, err)
// vectorsRouter builds the S3 Vectors API router — workers register to the
// master and the backend resolves them lazily, so a worker that comes up
// after the gateway still gets picked up on first query.
func (s3opt *S3Options) vectorsRouter(s3ApiServer *s3api.S3ApiServer, grpcDialOption grpc.DialOption, masterAddresses []pb.ServerAddress) *mux.Router {
if len(masterAddresses) == 0 {
glog.Warningf("S3 Vectors API disabled: no weed-vector registered on master")
return nil
}
backend := &vectorDiscovery{masters: masterAddresses, grpcDialOption: grpcDialOption}
vectorsRouter := mux.NewRouter().SkipClean(true)
vectorsServer := s3vectors.NewServer(s3ApiServer, s3ApiServer, vector_pb.NewVectorServiceClient(conn))
vectorsServer := s3vectors.NewServer(s3ApiServer, s3ApiServer, backend)
vectorsServer.SetRegion(s3tables.DefaultRegion)
vectorsServer.SetAccountID(s3tables.DefaultAccountID)
vectorsServer.RegisterRoutes(vectorsRouter)
return vectorsRouter
}
// Mirror the main API: when TLS is configured the vectors port serves
// HTTPS, not a second plaintext endpoint. Build the TLS config before
// binding listeners so a certificate failure does not leave ports bound.
var tlsConfig *tls.Config
if *s3opt.tlsPrivateKey != "" {
// The reloading provider reads the key pair asynchronously, so validate
// it once up front: a missing or mismatched pair must fail here rather
// than after the listeners bind an endpoint that cannot handshake.
if _, err := tls.LoadX509KeyPair(*s3opt.tlsCertificate, *s3opt.tlsPrivateKey); err != nil {
glog.Errorf("S3 Vectors: load HTTPS certificate: %v", err)
conn.Close()
return nil
}
getCert, _, err := security.NewReloadingServerCertificate(*s3opt.tlsCertificate, *s3opt.tlsPrivateKey)
// vectorDiscovery resolves the currently registered weed-vector through the
// master and caches the connection; workers joining after gateway startup
// are picked up on first query.
type vectorDiscovery struct {
masters []pb.ServerAddress
grpcDialOption grpc.DialOption
mu sync.Mutex
worker string
conn *grpc.ClientConn
client vector_pb.VectorServiceClient
nextResolve time.Time
}
// reset drops the cached worker so the next call rediscovers — invoked on
// transport errors only, and only when the failed client is still the
// cached one (a concurrent request may have already re-resolved).
func (d *vectorDiscovery) reset(c vector_pb.VectorServiceClient) {
d.mu.Lock()
defer d.mu.Unlock()
if c != d.client {
return
}
if d.conn != nil {
d.conn.Close()
}
d.worker, d.conn, d.client = "", nil, nil
d.nextResolve = time.Time{}
}
// Only Unavailable proves a transport failure — a request deadline or an
// application-level error can fire on a healthy connection.
func isTransportError(err error) bool {
return status.Code(err) == codes.Unavailable
}
func (d *vectorDiscovery) resolve() (vector_pb.VectorServiceClient, error) {
d.mu.Lock()
defer d.mu.Unlock()
worker := discoverVectorWorker(d.masters, d.grpcDialOption)
if worker == "" {
return nil, fmt.Errorf("no weed-vector registered on master")
}
if worker != d.worker {
if d.conn != nil {
d.conn.Close()
}
conn, err := pb.GrpcDial(context.Background(), worker, false, d.grpcDialOption)
if err != nil {
glog.Errorf("S3 Vectors: load HTTPS certificate: %v", err)
conn.Close()
return nil
}
clientAuth := tls.NoClientCert
var caCertPool *x509.CertPool
if *s3opt.tlsVerifyClientCert {
clientAuth = tls.RequireAndVerifyClientCert
caCertPool, err = loadClientCAPool(*s3opt.tlsCACertificate)
if err != nil {
glog.Errorf("S3 Vectors: %v", err)
conn.Close()
return nil
}
}
tlsConfig = &tls.Config{
GetCertificate: getCert,
ClientAuth: clientAuth,
ClientCAs: caCertPool,
}
if err := security.FixTlsConfig(util.GetViper(), tlsConfig); err != nil {
glog.Errorf("S3 Vectors: tls config: %v", err)
conn.Close()
return nil
return nil, err
}
d.worker, d.conn = worker, conn
d.client = vector_pb.NewVectorServiceClient(conn)
}
return d.client, nil
}
listenAddress := fmt.Sprintf("%s:%d", *s3opt.bindIp, *s3opt.portVectors)
vectorsListener, vectorsLocalListener, err := util.NewIpAndLocalListeners(
*s3opt.bindIp, *s3opt.portVectors, idleTimeoutSeconds(s3opt.idleTimeout), listenOnLocalhost)
if err != nil {
glog.Errorf("S3 Vectors listener on %s error: %v", listenAddress, err)
conn.Close()
return nil
const vectorResolveInterval = 5 * time.Second
func (d *vectorDiscovery) cli() (vector_pb.VectorServiceClient, error) {
d.mu.Lock()
cli := d.client
past := !time.Now().Before(d.nextResolve)
if cli != nil && !past {
d.mu.Unlock()
return cli, nil
}
if !past && cli == nil {
d.mu.Unlock()
return nil, fmt.Errorf("no weed-vector registered on master")
}
d.nextResolve = time.Now().Add(vectorResolveInterval)
d.mu.Unlock()
return d.resolve()
}
if tlsConfig != nil {
glog.V(0).Infof("Start S3 Vectors API Server at https://%s", listenAddress)
} else {
glog.V(0).Infof("Start S3 Vectors API Server at http://%s", listenAddress)
func (d *vectorDiscovery) CreateIndex(ctx context.Context, in *vector_pb.CreateIndexRequest, opts ...grpc.CallOption) (resp *vector_pb.CreateIndexResponse, err error) {
for attempt := 0; attempt < 2; attempt++ {
var c vector_pb.VectorServiceClient
if c, err = d.cli(); err != nil {
return nil, err
}
if resp, err = c.CreateIndex(ctx, in, opts...); err == nil {
return resp, nil
}
if !isTransportError(err) {
return nil, err
}
d.reset(c)
}
httpS := newHttpServer(vectorsRouter, tlsConfig)
serve := func(listener net.Listener) error {
if tlsConfig != nil {
return httpS.ServeTLS(listener, "", "")
return nil, err
}
func (d *vectorDiscovery) DropIndex(ctx context.Context, in *vector_pb.DropIndexRequest, opts ...grpc.CallOption) (resp *vector_pb.DropIndexResponse, err error) {
for attempt := 0; attempt < 2; attempt++ {
var c vector_pb.VectorServiceClient
if c, err = d.cli(); err != nil {
return nil, err
}
if resp, err = c.DropIndex(ctx, in, opts...); err == nil {
return resp, nil
}
if !isTransportError(err) {
return nil, err
}
d.reset(c)
}
return nil, err
}
func (d *vectorDiscovery) PutVectors(ctx context.Context, in *vector_pb.PutVectorsRequest, opts ...grpc.CallOption) (resp *vector_pb.PutVectorsResponse, err error) {
for attempt := 0; attempt < 2; attempt++ {
var c vector_pb.VectorServiceClient
if c, err = d.cli(); err != nil {
return nil, err
}
if resp, err = c.PutVectors(ctx, in, opts...); err == nil {
return resp, nil
}
if !isTransportError(err) {
return nil, err
}
return httpS.Serve(listener)
d.reset(c)
}
if vectorsLocalListener != nil {
go func() {
if err := serve(vectorsLocalListener); err != nil && err != http.ErrServerClosed {
glog.V(0).Infof("S3 Vectors localhost listener error: %v", err)
}
}()
return nil, err
}
func (d *vectorDiscovery) GetVectors(ctx context.Context, in *vector_pb.GetVectorsRequest, opts ...grpc.CallOption) (resp *vector_pb.GetVectorsResponse, err error) {
for attempt := 0; attempt < 2; attempt++ {
var c vector_pb.VectorServiceClient
if c, err = d.cli(); err != nil {
return nil, err
}
if resp, err = c.GetVectors(ctx, in, opts...); err == nil {
return resp, nil
}
if !isTransportError(err) {
return nil, err
}
d.reset(c)
}
go func() {
if err := serve(vectorsListener); err != nil && err != http.ErrServerClosed {
glog.Errorf("S3 Vectors API Server Fail to serve: %v", err)
return nil, err
}
func (d *vectorDiscovery) DeleteVectors(ctx context.Context, in *vector_pb.DeleteVectorsRequest, opts ...grpc.CallOption) (resp *vector_pb.DeleteVectorsResponse, err error) {
for attempt := 0; attempt < 2; attempt++ {
var c vector_pb.VectorServiceClient
if c, err = d.cli(); err != nil {
return nil, err
}
if resp, err = c.DeleteVectors(ctx, in, opts...); err == nil {
return resp, nil
}
if !isTransportError(err) {
return nil, err
}
d.reset(c)
}
return nil, err
}
func (d *vectorDiscovery) ListVectors(ctx context.Context, in *vector_pb.ListVectorsRequest, opts ...grpc.CallOption) (resp *vector_pb.ListVectorsResponse, err error) {
for attempt := 0; attempt < 2; attempt++ {
var c vector_pb.VectorServiceClient
if c, err = d.cli(); err != nil {
return nil, err
}
if resp, err = c.ListVectors(ctx, in, opts...); err == nil {
return resp, nil
}
}()
return httpS
if !isTransportError(err) {
return nil, err
}
d.reset(c)
}
return nil, err
}
func (d *vectorDiscovery) QueryVectors(ctx context.Context, in *vector_pb.QueryVectorsRequest, opts ...grpc.CallOption) (resp *vector_pb.QueryVectorsResponse, err error) {
for attempt := 0; attempt < 2; attempt++ {
var c vector_pb.VectorServiceClient
if c, err = d.cli(); err != nil {
return nil, err
}
if resp, err = c.QueryVectors(ctx, in, opts...); err == nil {
return resp, nil
}
if !isTransportError(err) {
return nil, err
}
d.reset(c)
}
return nil, err
}
// isVectorsRequest routes by the SigV4 credential scope — the s3vectors
// service name lives there, not in the path.
func isVectorsRequest(r *http.Request) bool {
if v := r.Header.Get("Authorization"); v != "" {
return strings.Contains(v, "/s3vectors/")
}
return strings.Contains(r.URL.Query().Get("X-Amz-Credential"), "/s3vectors/")
}
// loadClientCAPool builds the client CA pool from -cacert.file, failing when
// the file reads but carries no usable certificate.
func loadClientCAPool(caCertFile string) (*x509.CertPool, error) {
pool := x509.NewCertPool()
if caCertFile == "" {
@@ -928,3 +1049,26 @@ func (v lanceCredentialVendor) VendTableCredentials(ctx context.Context, princip
Expiration: credentials.Expiration,
}, nil
}
// discoverVectorWorker resolves one registered weed-vector server through the
// master's cluster-nodes list; OSS fans every vector call to that single
// backend.
func discoverVectorWorker(masters []pb.ServerAddress, grpcDialOption grpc.DialOption) string {
for _, master := range masters {
conn, err := pb.GrpcDial(context.Background(), master.ToGrpcAddress(), false, grpcDialOption)
if err != nil {
continue
}
resp, err := master_pb.NewSeaweedClient(conn).ListClusterNodes(context.Background(), &master_pb.ListClusterNodesRequest{
ClientType: "vector",
})
if err != nil {
continue
}
for _, node := range resp.ClusterNodes {
glog.V(0).Infof("S3 Vectors: discovered weed-vector at %s via master %s", node.Address, master)
return node.Address
}
}
return ""
}