mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-10 22:08:48 +01:00
* vector: expose request metrics and registration-aware health endpoints * vector: report immutable read cache and storage fetch metrics * docs: describe vector engine health, metrics, and maintenance * vector: count canceled requests when their handler is dropped * vector: account for uncached large immutable storage reads * vector: reliably release the stalled credential reader in tests * vector: observe large reads as their streamed body is consumed * vector: bound heartbeats on established master registration streams * test: exercise hardened vector maintenance on Go and Rust volumes * vector: register engines in their configured filer group * vector: make retried index creation preserve compatible datasets * vector: allow registration to advertise a separate address * test: release stalled credential readers without blocking FIFO cleanup * test: allow hardened vector coverage in a named filer group * vector: discover engines in the same filer group as their gateway * vector: fall back when the advertised address is empty * admin: expose vector readiness, maintenance, and management tools (#12520) * s3vectors: share vector discovery with trusted management clients * admin: show vector engine readiness and recent maintenance activity * admin: suggest S3 Vectors IAM actions from the supported operations * admin: manage vector indexes, policies, tags, encryption, and query data * admin: validate SSE-S3 support before creating encrypted vector buckets * admin: discover vector engines in their registration group * admin: probe vector engines concurrently with independent time budgets * admin: verify encryption capability on the remote S3 gateway * admin: defer discovery until after the HTTP server can start * admin: distinguish unknown vector health services from failed engines * admin: count workers that can execute vector index maintenance * admin: allow read-only users to query vectors and inspect tags * vector: scope gateway and admin discovery to the configured filer group * admin: refresh policy state after mutations and serialize policy controls * vector: drain active RPCs before closing replaced discovery connections * vector: respect request cancellation while waiting for discovery * vector: prefer another registered engine after a transport failure * admin: check all masters before reporting no registered vector engines * iam: honor attached vector policies without broadening legacy grants * vector: allow short-lived discovery clients to close their connections * vector: refresh cached master addresses for each discovery lookup * iam: enforce vector bucket tag conditions in attached policies * iam: authorize vector bucket creation with the requested tags * test: exercise vector IAM grants and tag denies on Go and Rust volumes * admin: reject authenticated vector mutations without an admin role * test: run vector IAM assertions before the shared engine is stopped * vector: add native mini startup, shell tools, and a runnable Compose example (#12522) * s3vectors: share vector discovery with trusted management clients * admin: show vector engine readiness and recent maintenance activity * admin: suggest S3 Vectors IAM actions from the supported operations * admin: manage vector indexes, policies, tags, encryption, and query data * admin: validate SSE-S3 support before creating encrypted vector buckets * admin: discover vector engines in their registration group * admin: probe vector engines concurrently with independent time budgets * admin: verify encryption capability on the remote S3 gateway * admin: defer discovery until after the HTTP server can start * admin: distinguish unknown vector health services from failed engines * admin: count workers that can execute vector index maintenance * admin: allow read-only users to query vectors and inspect tags * vector: scope gateway and admin discovery to the configured filer group * admin: refresh policy state after mutations and serialize policy controls * vector: drain active RPCs before closing replaced discovery connections * vector: respect request cancellation while waiting for discovery * vector: prefer another registered engine after a transport failure * admin: check all masters before reporting no registered vector engines * iam: honor attached vector policies without broadening legacy grants * vector: allow short-lived discovery clients to close their connections * vector: refresh cached master addresses for each discovery lookup * iam: enforce vector bucket tag conditions in attached policies * iam: authorize vector bucket creation with the requested tags * test: exercise vector IAM grants and tag denies on Go and Rust volumes * admin: reject authenticated vector mutations without an admin role * test: run vector IAM assertions before the shared engine is stopped * mini: optionally start and supervise the vector engine * mini: bootstrap vector buckets with existing Lance catalog helpers * shell: manage vector buckets, indexes, and vector data * docs: provide a runnable vector and maintenance worker Compose stack * shell: scope vector discovery to the session and close its connections * mini: honor the bind address for vector and metrics listeners * mini: reserve vector ports before assigning gRPC and compatibility listeners * mini: wait for vector storage setup and master registration * mini: create requested vector buckets without requiring the Lance HTTP listener * shell: refresh master addresses during vector discovery * mini: resolve sibling vector executables with platform extensions * vector: preserve uncertain creates and fix mini listener allocation (#12525) * vector: retain catalog records when index creation has an unknown outcome * mini: keep chosen listener ports reserved throughout allocation * mini: resolve hostname binds before starting vector listeners * vector: fix integration test imports and recover interrupted index creation (#12533) * shell: register vector operations from the command package * s3vectors: resume interrupted index initialization safely * mini: exclude disabled WebDAV from listener allocation * s3vectors: retain uncertain creates across refused retries * s3vectors: claim pending indexes before retrying initialization * s3vectors: compare resolved distance metrics during recovery * s3vectors: preserve metadata keys when retrying initialization * s3vectors: verify ownership before initializing new catalog records
353 lines
10 KiB
Go
353 lines
10 KiB
Go
package s3vectors
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/connectivity"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
// A worker that registers a moment after a failed lookup must surface inside
|
|
// a caller's retry budget: the no-client recheck window is much shorter than
|
|
// the live-client membership refresh.
|
|
func TestVectorDiscoveryRechecksRegistration(t *testing.T) {
|
|
var calls int32
|
|
d := &vectorDiscovery{
|
|
discover: func(masters []pb.ServerAddress, opt grpc.DialOption) []string {
|
|
atomic.AddInt32(&calls, 1)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
if _, err := d.cli(t.Context()); status.Code(err) != codes.Unavailable {
|
|
t.Fatalf("no registered worker: expected Unavailable, got %v", err)
|
|
}
|
|
if _, err := d.cli(t.Context()); status.Code(err) != codes.Unavailable {
|
|
t.Fatalf("no registered worker: expected Unavailable, got %v", err)
|
|
}
|
|
if got := atomic.LoadInt32(&calls); got != 1 {
|
|
t.Fatalf("lookup inside the recheck window hit master again: %d lookups", got)
|
|
}
|
|
|
|
// Once the recheck interval passes the next call re-discovers.
|
|
d.mu.Lock()
|
|
d.nextResolve = time.Now().Add(-time.Second)
|
|
d.mu.Unlock()
|
|
d.discover = func(masters []pb.ServerAddress, opt grpc.DialOption) []string {
|
|
atomic.AddInt32(&calls, 1)
|
|
return []string{"127.0.0.1:9301"}
|
|
}
|
|
d.grpcDialOption = grpc.WithTransportCredentials(insecure.NewCredentials())
|
|
|
|
if _, err := d.cli(t.Context()); err != nil {
|
|
t.Fatalf("worker registered: expected a client, got %v", err)
|
|
}
|
|
if got := atomic.LoadInt32(&calls); got != 2 {
|
|
t.Fatalf("expired recheck window did not rediscover: %d lookups", got)
|
|
}
|
|
|
|
// A live client rides out the refresh interval without rediscovery.
|
|
if _, err := d.cli(t.Context()); err != nil {
|
|
t.Fatalf("cached client rejected: %v", err)
|
|
}
|
|
if got := atomic.LoadInt32(&calls); got != 2 {
|
|
t.Fatalf("live client rediscovered inside the refresh window: %d lookups", got)
|
|
}
|
|
}
|
|
|
|
// A lookup slower than the throttle interval (an unreachable first master
|
|
// costs the full dial timeout) must not leave the next lookup already due,
|
|
// or every request queues behind back-to-back lookups.
|
|
func TestVectorDiscoveryThrottlesFromLookupEnd(t *testing.T) {
|
|
const lookup = 600 * time.Millisecond
|
|
var calls int32
|
|
d := &vectorDiscovery{
|
|
grpcDialOption: grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
discover: func(masters []pb.ServerAddress, opt grpc.DialOption) []string {
|
|
atomic.AddInt32(&calls, 1)
|
|
time.Sleep(lookup)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
if _, err := d.cli(t.Context()); status.Code(err) != codes.Unavailable {
|
|
t.Fatalf("no registered worker: expected Unavailable, got %v", err)
|
|
}
|
|
if _, err := d.cli(t.Context()); status.Code(err) != codes.Unavailable {
|
|
t.Fatalf("no registered worker: expected Unavailable, got %v", err)
|
|
}
|
|
if got := atomic.LoadInt32(&calls); got != 1 {
|
|
t.Fatalf("slow lookup was repeated right away: %d lookups", got)
|
|
}
|
|
|
|
d.discover = func(masters []pb.ServerAddress, opt grpc.DialOption) []string {
|
|
time.Sleep(lookup)
|
|
return []string{"127.0.0.1:9301"}
|
|
}
|
|
for i := 0; i < 2; i++ {
|
|
d.mu.Lock()
|
|
d.nextResolve = time.Time{}
|
|
d.mu.Unlock()
|
|
if _, err := d.cli(t.Context()); err != nil {
|
|
t.Fatalf("worker registered: expected a client, got %v", err)
|
|
}
|
|
waitResolved(d)
|
|
}
|
|
d.mu.Lock()
|
|
left := time.Until(d.nextResolve)
|
|
d.mu.Unlock()
|
|
if left < vectorResolveInterval-lookup/2 {
|
|
t.Fatalf("refresh of a live worker is due in %v, want ~%v", left, vectorResolveInterval)
|
|
}
|
|
}
|
|
|
|
func waitResolved(d *vectorDiscovery) {
|
|
d.mu.Lock()
|
|
resolving := d.resolving
|
|
d.mu.Unlock()
|
|
if resolving != nil {
|
|
<-resolving
|
|
}
|
|
}
|
|
|
|
// A lookup stuck on an unreachable master must not hold up requests that
|
|
// already have a live worker, and concurrent callers start only one lookup.
|
|
func TestVectorDiscoveryLookupDoesNotBlockCachedClient(t *testing.T) {
|
|
d := &vectorDiscovery{
|
|
grpcDialOption: grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
discover: func(masters []pb.ServerAddress, opt grpc.DialOption) []string {
|
|
return []string{"127.0.0.1:9301"}
|
|
},
|
|
}
|
|
cached, err := d.cli(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("worker registered: expected a client, got %v", err)
|
|
}
|
|
|
|
var calls int32
|
|
entered, release := make(chan struct{}), make(chan struct{})
|
|
d.discover = func(masters []pb.ServerAddress, opt grpc.DialOption) []string {
|
|
if atomic.AddInt32(&calls, 1) == 1 {
|
|
close(entered)
|
|
}
|
|
<-release
|
|
return []string{"127.0.0.1:9301"}
|
|
}
|
|
d.mu.Lock()
|
|
d.nextResolve = time.Time{}
|
|
d.mu.Unlock()
|
|
|
|
const callers = 8
|
|
got := make(chan error, callers+1)
|
|
call := func() {
|
|
c, err := d.cli(t.Context())
|
|
if err == nil && c != cached {
|
|
err = fmt.Errorf("got a different client")
|
|
}
|
|
got <- err
|
|
}
|
|
go call()
|
|
<-entered
|
|
for i := 0; i < callers; i++ {
|
|
go call()
|
|
}
|
|
timeout := time.After(2 * time.Second)
|
|
for i := 0; i <= callers; i++ {
|
|
select {
|
|
case err := <-got:
|
|
if err != nil {
|
|
close(release)
|
|
t.Fatalf("cached client: %v", err)
|
|
}
|
|
case <-timeout:
|
|
close(release)
|
|
t.Fatalf("cached client blocked behind an in-flight lookup")
|
|
}
|
|
}
|
|
close(release)
|
|
waitResolved(d)
|
|
if n := atomic.LoadInt32(&calls); n != 1 {
|
|
t.Fatalf("concurrent callers ran %d lookups, want 1", n)
|
|
}
|
|
}
|
|
|
|
// Callers with no worker wait for the in-flight lookup and share its result
|
|
// rather than failing while it runs.
|
|
func TestVectorDiscoveryUncachedCallersShareLookup(t *testing.T) {
|
|
var calls int32
|
|
release := make(chan struct{})
|
|
d := &vectorDiscovery{
|
|
grpcDialOption: grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
discover: func(masters []pb.ServerAddress, opt grpc.DialOption) []string {
|
|
atomic.AddInt32(&calls, 1)
|
|
<-release
|
|
return []string{"127.0.0.1:9301"}
|
|
},
|
|
}
|
|
const callers = 8
|
|
got := make(chan error, callers)
|
|
for i := 0; i < callers; i++ {
|
|
go func() {
|
|
_, err := d.cli(t.Context())
|
|
got <- err
|
|
}()
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
close(release)
|
|
for i := 0; i < callers; i++ {
|
|
if err := <-got; err != nil {
|
|
t.Fatalf("caller waiting on the lookup: %v", err)
|
|
}
|
|
}
|
|
if n := atomic.LoadInt32(&calls); n != 1 {
|
|
t.Fatalf("concurrent callers ran %d lookups, want 1", n)
|
|
}
|
|
}
|
|
|
|
// A failed dial to a new worker must leave the cached connection open.
|
|
func TestVectorDiscoveryKeepsConnOnDialError(t *testing.T) {
|
|
d := &vectorDiscovery{
|
|
grpcDialOption: grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
discover: func(masters []pb.ServerAddress, opt grpc.DialOption) []string {
|
|
return []string{"127.0.0.1:9301"}
|
|
},
|
|
}
|
|
cached, err := d.cli(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("worker registered: expected a client, got %v", err)
|
|
}
|
|
|
|
// Without transport credentials the dial fails synchronously.
|
|
d.grpcDialOption = nil
|
|
d.discover = func(masters []pb.ServerAddress, opt grpc.DialOption) []string {
|
|
return []string{"127.0.0.1:9302"}
|
|
}
|
|
d.mu.Lock()
|
|
d.nextResolve = time.Time{}
|
|
d.mu.Unlock()
|
|
if c, err := d.cli(t.Context()); err != nil || c != cached {
|
|
t.Fatalf("cached client: got %v, %v", c, err)
|
|
}
|
|
waitResolved(d)
|
|
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
if d.client != cached || d.conn.GetState() == connectivity.Shutdown {
|
|
t.Fatalf("failed dial dropped the cached connection")
|
|
}
|
|
}
|
|
|
|
func TestVectorDiscoveryDrainsRetiredConnections(t *testing.T) {
|
|
d := &vectorDiscovery{grpcDialOption: grpc.WithTransportCredentials(insecure.NewCredentials()), discover: func([]pb.ServerAddress, grpc.DialOption) []string { return []string{"127.0.0.1:9301"} }}
|
|
first, err := d.cli(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := d.cli(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conn := d.conn
|
|
d.reset(first)
|
|
d.release(first)
|
|
if conn.GetState() == connectivity.Shutdown {
|
|
t.Fatal("active RPC lost its connection")
|
|
}
|
|
d.release(second)
|
|
if conn.GetState() != connectivity.Shutdown {
|
|
t.Fatal("retired connection was not closed after the last RPC")
|
|
}
|
|
}
|
|
|
|
func TestVectorDiscoveryWaitingRespectsCancellation(t *testing.T) {
|
|
entered, release := make(chan struct{}), make(chan struct{})
|
|
d := &vectorDiscovery{discover: func([]pb.ServerAddress, grpc.DialOption) []string { close(entered); <-release; return nil }}
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
result := make(chan error, 1)
|
|
go func() { _, err := d.cli(ctx); result <- err }()
|
|
<-entered
|
|
cancel()
|
|
select {
|
|
case err := <-result:
|
|
if status.Code(err) != codes.Canceled {
|
|
t.Fatal(err)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("request remained blocked after cancellation")
|
|
}
|
|
close(release)
|
|
waitResolved(d)
|
|
}
|
|
|
|
func TestVectorDiscoveryRetriesAnotherWorker(t *testing.T) {
|
|
d := &vectorDiscovery{grpcDialOption: grpc.WithTransportCredentials(insecure.NewCredentials()), discover: func([]pb.ServerAddress, grpc.DialOption) []string {
|
|
return []string{"127.0.0.1:9301", "127.0.0.1:9302"}
|
|
}}
|
|
first, err := d.cli(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
d.reset(first)
|
|
d.release(first)
|
|
second, err := d.cli(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer d.release(second)
|
|
if d.worker != "127.0.0.1:9302" {
|
|
t.Fatalf("retry selected failed worker: %s", d.worker)
|
|
}
|
|
}
|
|
|
|
func TestVectorDiscoveryCloseDuringLookup(t *testing.T) {
|
|
entered, release := make(chan struct{}), make(chan struct{})
|
|
d := &vectorDiscovery{grpcDialOption: grpc.WithTransportCredentials(insecure.NewCredentials()), discover: func([]pb.ServerAddress, grpc.DialOption) []string {
|
|
close(entered)
|
|
<-release
|
|
return []string{"127.0.0.1:9301"}
|
|
}}
|
|
done := make(chan error, 1)
|
|
go func() { _, err := d.cli(t.Context()); done <- err }()
|
|
<-entered
|
|
_ = d.Close()
|
|
close(release)
|
|
if err := <-done; status.Code(err) != codes.Canceled {
|
|
t.Fatal(err)
|
|
}
|
|
if d.conn != nil {
|
|
t.Fatal("closed discovery opened a connection")
|
|
}
|
|
}
|
|
|
|
func TestVectorDiscoveryRefreshesMasterSnapshot(t *testing.T) {
|
|
masters := []pb.ServerAddress{}
|
|
d := NewVectorDiscovery(func() []pb.ServerAddress { return masters }, grpc.WithTransportCredentials(insecure.NewCredentials()), "")
|
|
d.discover = func(current []pb.ServerAddress, _ grpc.DialOption) []string {
|
|
if len(current) > 0 {
|
|
return []string{"127.0.0.1:9301"}
|
|
}
|
|
return nil
|
|
}
|
|
if _, err := d.cli(t.Context()); status.Code(err) != codes.Unavailable {
|
|
t.Fatal(err)
|
|
}
|
|
masters = []pb.ServerAddress{"master:9333"}
|
|
d.mu.Lock()
|
|
d.nextResolve = time.Time{}
|
|
d.mu.Unlock()
|
|
client, err := d.cli(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
d.release(client)
|
|
_ = d.Close()
|
|
}
|