tidaldb/tidal-server/benches/scatter.rs
jx12n 6651c14adc feat(m11): cluster security (m11p7) + perf instrumentation floor
m11p7 — secure the cluster, all opt-in (pre-m11p7 byte-for-byte):
- gRPC replication mTLS by default via a custom tokio-rustls acceptor +
  DynamicCertResolver; zero-drop content-hash cert rotation (k8s ..data swap,
  no pod restart, no inotify)
- inter-node HTTP TLS sharing the same resolver (one rotation, both planes) +
  per-node keyed-BLAKE3 signed x-tidal-node-token; marker-without-token -> 403
- admin audit log (operator-leg only) + per-principal rate limit (engine
  RateLimiter; sibling nodes exempt)
- k8s cert-manager manifest (certs.yaml) + scripts/gen-cluster-certs.sh fallback;
  secret.example.yaml gains TIDAL_CLUSTER_KEY (file-mounted, hot-rotatable)
- exit gate verified real: mtls.rs (gRPC foreign-pod), cluster_security.rs
  (HTTP foreign + zero-drop rotation under load), 7 security unit tests

perf — instrument floor (sweep Wave 1):
- new tidal/benches/wal.rs + tidal-server/benches/scatter.rs
- p99->mean honesty relabel; sweep manifest at docs/reviews/perf-sweep-2026-06-13.md
- add @tidal-performance agent (Martin Thompson)

new: cluster/{audit,http_tls,security}.rs, tests/cluster_security.rs,
docs/planning/milestone-11/phase-7.md
2026-06-13 01:25:35 -06:00

156 lines
5.4 KiB
Rust

#![allow(clippy::unwrap_used, clippy::cast_precision_loss)]
//! Criterion benchmark for scatter-gather RETRIEVE fan-out.
//!
//! Today `scatter_gather_retrieve` spawns one OS thread *per shard, per query*
//! behind a single global `Mutex<usize>` + `Condvar` permit semaphore (perf
//! sweep 2026-06-13, finding rank 2). Before this bench existed there was no
//! before/after for that threading model. It measures:
//!
//! - **`scatter_fanout/regions{4,16}`** — single-query fan-out latency. The gap
//! between 4 and 16 regions is the per-shard thread create/teardown +
//! 2 MiB-stack-reservation cost, since the underlying per-shard reads over the
//! tiny replicated dataset are near-instant.
//!
//! - **`scatter_fanout_concurrent/regions{4,16}_q8`** — 8 concurrent queries
//! issued at once, so `8 * regions` workers all contend the one global
//! semaphore. This is the lock-bounce / queue-depth signal that a reused
//! worker pool (wave 5) must improve without regressing single-query latency.
//!
//! Run:
//! ```bash
//! cargo bench -p tidal-server --bench scatter
//! ```
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use criterion::{Criterion, criterion_group, criterion_main};
use tidal_server::scatter_gather::scatter_gather_retrieve;
use tidaldb::query::retrieve::Retrieve;
use tidaldb::replication::shard::RegionId;
use tidaldb::schema::{DecaySpec, EntityId, EntityKind, Schema, SchemaBuilder, Window};
use tidaldb::testing::SimulatedCluster;
use tidaldb::testing::cluster::ClusterConfig;
fn bench_schema() -> Schema {
let mut builder = SchemaBuilder::new();
let _ = builder
.signal(
"view",
EntityKind::Item,
DecaySpec::Exponential {
half_life: Duration::from_secs(7 * 24 * 3600),
},
)
.windows(&[Window::OneHour])
.velocity(false)
.add();
builder.build().unwrap()
}
/// Build an `n`-region replicated cluster pre-seeded with 64 viewed items.
/// Returns the cluster plus the shard list and region-name map that
/// `scatter_gather_retrieve` consumes.
fn build_cluster(
n: u16,
) -> (
Arc<SimulatedCluster>,
Vec<RegionId>,
HashMap<RegionId, String>,
) {
let regions: Vec<RegionId> = (0..n).map(RegionId).collect();
let config = ClusterConfig {
regions: regions.clone(),
leader_region: RegionId(0),
schema: bench_schema(),
profiles: Vec::new(),
transports: None,
};
let cluster = Arc::new(SimulatedCluster::build(config));
// Replicated topology: write to the leader, all regions see all data.
for i in 1..=64u64 {
let eid = EntityId::new(i);
cluster
.write_item_with_metadata(eid, &HashMap::new())
.unwrap();
cluster.write_signal("view", eid, i as f64).unwrap();
}
let names: HashMap<RegionId, String> = regions
.iter()
.map(|&r| (r, format!("region-{}", r.0)))
.collect();
(cluster, regions, names)
}
fn trending_query() -> Retrieve {
Retrieve::builder()
.profile("trending")
.limit(20)
.build()
.unwrap()
}
fn fanout_latency(c: &mut Criterion) {
let mut group = c.benchmark_group("scatter_fanout");
group.sample_size(30);
for n in [4u16, 16] {
let (cluster, shards, names) = build_cluster(n);
let query = trending_query();
group.bench_function(format!("regions{n}"), |b| {
b.iter(|| {
let (result, _meta) =
scatter_gather_retrieve(&cluster, &query, &shards, &names, None).unwrap();
assert!(!result.items.is_empty());
});
});
}
group.finish();
}
fn fanout_concurrent(c: &mut Criterion) {
const CONCURRENCY: usize = 8;
let mut group = c.benchmark_group("scatter_fanout_concurrent");
group.sample_size(20);
group.measurement_time(Duration::from_secs(12));
for n in [4u16, 16] {
let (cluster, shards, names) = build_cluster(n);
let query = trending_query();
group.bench_function(format!("regions{n}_q{CONCURRENCY}"), |b| {
b.iter_custom(|iters| {
let mut elapsed = Duration::ZERO;
for _ in 0..iters {
let start = Instant::now();
let threads: Vec<_> = (0..CONCURRENCY)
.map(|_| {
let cluster = Arc::clone(&cluster);
let query = query.clone();
let shards = shards.clone();
let names = names.clone();
std::thread::spawn(move || {
let (result, _meta) = scatter_gather_retrieve(
&cluster, &query, &shards, &names, None,
)
.unwrap();
assert!(!result.items.is_empty());
})
})
.collect();
for t in threads {
t.join().unwrap();
}
elapsed += start.elapsed();
}
elapsed
});
});
}
group.finish();
}
criterion_group!(benches, fanout_latency, fanout_concurrent);
criterion_main!(benches);