Files
seaweedfs/seaweed-vector/crates/server/src/main.rs
T
Chris Lu 8236c8d136 vector: integrate engine health, admin management, and local deployment (#12517)
* 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
2026-11-10 04:21:18 -08:00

280 lines
10 KiB
Rust

use std::path::PathBuf;
use anyhow::{Context, Result};
use clap::Parser;
use tokio_stream::wrappers::TcpListenerStream;
use tonic::transport::Server;
use weed_lance_worker::cluster::Cluster;
use weed_vector_server::pb::vector::vector_service_server::VectorServiceServer;
use weed_vector_server::read_cache::ReadCache;
use weed_vector_server::security::Security;
use weed_vector_server::service::VectorServer;
use weed_vector_server::tls::{self, ServerTls};
use weed_vector_server::{
master_client,
metrics::{self, Metrics},
observed::ObservedService,
};
/// The VectorService the S3 gateway fans vector calls out to.
#[derive(Parser)]
struct Args {
/// gRPC listen address.
#[arg(long, env = "WEED_VECTOR_LISTEN", default_value = "127.0.0.1:8090")]
listen: String,
/// Address to register when it differs from the listen address.
#[arg(long, env = "WEED_VECTOR_ADVERTISE")]
advertise: Option<String>,
/// Comma-separated master addresses. The engine registers with them, so
/// the gateway finds it, and asks them for the filers it keeps tables
/// through.
#[arg(long, alias = "master", env = "WEED_VECTOR_MASTERS")]
masters: String,
/// The filer group whose filers the engine keeps tables through.
#[arg(
long,
alias = "filer.group",
env = "WEED_VECTOR_FILER_GROUP",
default_value = ""
)]
filer_group: String,
/// Memory for caching reads of write-once table files, in MiB; 0 disables.
#[arg(long, env = "WEED_VECTOR_READ_CACHE_MB", default_value_t = 512)]
read_cache_mb: u64,
/// Partitions each query probes in a table's vector index. Default: a
/// tenth of the index's partitions, at least 8; more under a filter.
#[arg(long, env = "WEED_VECTOR_NPROBES")]
nprobes: Option<std::num::NonZeroUsize>,
/// The cluster's security.toml: [grpc.vector] for this server,
/// [grpc.client] for the masters and filers, https.client for volume
/// servers under HTTPS, jwt.signing.read.key for signed volume reads,
/// s3.sse.kek or s3.sse.key for SSE-S3 buckets. By default the file weed
/// processes find in -config_dir, ., ~/.seaweedfs and /etc/seaweedfs.
#[arg(long, env = "WEED_VECTOR_SECURITY_TOML")]
security_toml: Option<PathBuf>,
#[arg(
long,
alias = "metricsPort",
env = "WEED_VECTOR_METRICS_PORT",
default_value_t = 0
)]
metrics_port: u16,
#[arg(
long,
alias = "metricsIp",
env = "WEED_VECTOR_METRICS_IP",
default_value = "127.0.0.1"
)]
metrics_ip: String,
/// The directory weed's -config_dir names, searched for security.toml
/// first.
#[arg(long, alias = "config_dir", env = "WEED_VECTOR_CONFIG_DIR")]
config_dir: Option<PathBuf>,
}
// weed tools take Go-style single-dash long flags; normalize_args converts
// any -flag to --flag — the same normalization weed-volume's config.rs does —
// so -master and --masters parse the same. The masters field carries the
// singular alias clap-side.
fn normalize_args(args: Vec<String>) -> Vec<String> {
let mut args = args;
let mut i = 1; // skip args[0] (binary name)
while i < args.len() {
let arg = &args[i];
if arg == "--" {
break;
}
if arg.starts_with("--") || !arg.starts_with('-') {
i += 1;
continue;
}
let without_dash = &arg[1..];
if without_dash.len() > 1 && !without_dash.starts_with(|c: char| c.is_ascii_digit()) {
args[i] = format!("-{}", arg);
}
i += 1;
}
args
}
/// A wildcard listen address is not reachable for peers; derive the outbound
/// IP toward the first master instead — the address the master sees anyway.
fn advertised_addr(listen: &str, masters: &[String], advertise: Option<&str>) -> String {
if let Some(address) = advertise
.map(str::trim)
.filter(|address| !address.is_empty())
{
return address.to_string();
}
let Some((host, _)) = listen.rsplit_once(':') else {
return listen.to_string();
};
if !matches!(host, "0.0.0.0" | "::" | "" | "[::]") {
return listen.to_string();
}
let port = listen.rsplit_once(':').map(|(_, p)| p).unwrap_or_default();
if let Some(master) = masters.first() {
// Masters may arrive as host:http.grpc or scheme://host:http —
// the UDP probe only needs a dialable host:port.
let probe = master.rsplit_once("://").map(|(_, r)| r).unwrap_or(master);
let probe = match probe.rsplit_once(':') {
Some((h, p)) => match p.split_once('.') {
Some((_, g)) => format!("{h}:{g}"),
None => probe.to_string(),
},
None => probe.to_string(),
};
if let Ok(sock) = std::net::UdpSocket::bind((host, 0u16)) {
if sock.connect(&probe).is_ok() {
if let Ok(local) = sock.local_addr() {
return format!("{}:{}", local.ip(), port);
}
}
}
}
listen.to_string()
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let args = Args::parse_from(normalize_args(std::env::args().collect()));
weed_lance_worker::tls::install_default_crypto_provider();
let security = Security::load(args.security_toml.as_deref(), args.config_dir.as_deref())?;
if let Some(path) = security.path() {
tracing::info!("security settings from {}", path.display());
}
let server_tls = security
.grpc_server_tls("grpc.vector")?
.map(ServerTls::load)
.transpose()?;
// Bind before registering, so the master never lists an engine that is
// about to exit because its port is taken.
let listener = tokio::net::TcpListener::bind(&args.listen)
.await
.with_context(|| format!("listen on {}", args.listen))?;
let cluster = Cluster::new(&args.masters, &args.filer_group, &security)?;
let reads = cluster.storage(&security).await?;
let (health, health_service) = tonic_health::server::health_reporter();
let metrics = Metrics::new()?.with_health(health);
metrics.set_connected(false).await;
let cache = (args.read_cache_mb > 0)
.then(|| ReadCache::new(args.read_cache_mb << 20).with_metrics(metrics.clone()));
let mut service = VectorServer::new()
.with_nprobes(args.nprobes.map(std::num::NonZeroUsize::get))
.with_storage(reads);
if let Some(cache) = &cache {
service = service.with_read_cache(cache.clone());
}
if args.metrics_port > 0 {
let ip = args
.metrics_ip
.trim_start_matches('[')
.trim_end_matches(']')
.parse::<std::net::IpAddr>()?;
let listener =
tokio::net::TcpListener::bind(std::net::SocketAddr::new(ip, args.metrics_port)).await?;
let metrics = metrics.clone();
tokio::spawn(async move {
if let Err(error) = metrics::serve_listener(metrics, listener, cache).await {
tracing::error!(%error, "vector metrics server stopped");
}
});
}
tracing::info!(
listen = %args.listen,
"weed-vector; serving gRPC {}, dialing masters {}",
match &server_tls {
Some(tls) if tls.files().allowed_common_names.is_empty()
&& tls.files().allowed_wildcard_domain.is_empty() =>
"over TLS with client certificates required",
Some(_) => "over TLS with allowed client certificates required",
None => "in plaintext",
},
if cluster.master_tls.is_some() { "over TLS" } else { "in plaintext" },
);
// Registered only now, with storage connected and the listener bound,
// so the gateway never finds an engine that cannot serve yet.
let advertise = advertised_addr(&args.listen, &cluster.masters, args.advertise.as_deref());
tokio::spawn(master_client::keep_connected(
cluster.masters.clone(),
advertise,
args.filer_group.clone(),
cluster.master_tls.clone(),
metrics.clone(),
));
let router = Server::builder()
.add_service(health_service)
// Match the gateway's 1 GiB cap — put batches over the tonic 4 MiB
// default otherwise get rejected before they reach the handler.
.add_service(
VectorServiceServer::new(ObservedService {
inner: service,
metrics,
})
.max_decoding_message_size(1 << 30)
.max_encoding_message_size(1 << 30),
);
match server_tls {
Some(tls) => {
tokio::spawn(tls.clone().refresh(tls::refresh_interval()));
router.serve_with_incoming(tls.incoming(listener)).await?;
}
None => {
router
.serve_with_incoming(TcpListenerStream::new(listener))
.await?
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_empty_advertise_uses_the_listen_address() {
for value in [None, Some(""), Some(" ")] {
assert_eq!(
advertised_addr("127.0.0.1:8090", &[], value),
"127.0.0.1:8090"
);
}
assert_eq!(
advertised_addr("127.0.0.1:8090", &[], Some("vector.example:8090")),
"vector.example:8090"
);
}
#[test]
fn nprobes_must_be_positive() {
let parse = |nprobes: &str| {
Args::try_parse_from(["weed-vector", "--masters", "m:9333", "--nprobes", nprobes])
};
assert!(parse("0").is_err());
let args = parse("12").unwrap();
assert_eq!(args.nprobes.map(std::num::NonZeroUsize::get), Some(12));
}
#[test]
fn the_catalog_flags_are_gone() {
let parse = |extra: &[&str]| {
let mut argv = vec!["weed-vector", "-master", "m:9333"];
argv.extend_from_slice(extra);
Args::try_parse_from(normalize_args(argv.into_iter().map(String::from).collect()))
};
assert_eq!(parse(&[]).unwrap().masters, "m:9333");
// The gateway resolves every table it sends; the engine has no
// catalog to name or authenticate to.
assert!(parse(&["--namespace", "http://localhost:9101"]).is_err());
assert!(parse(&["-filer-signing-key", "k"]).is_err());
}
#[test]
fn the_masters_are_required() {
assert!(Args::try_parse_from(["weed-vector"]).is_err());
}
}