Resolves the 142 findings from tidal/docs/reviews/CODE_REVIEW_m0-m10.md across the engine, server, net, and CLI surfaces: - WAL/session-journal durability, checkpoint format, and crash-recovery hardening - Replication shipper/receiver, tenant isolation, and migration paths - Cluster scatter-gather, router, standalone server + health/offload endpoints - tidalctl refactored into command modules with JSON output and WAL-state tooling - Cohort, governance, signal-ledger, and vector-registry correctness fixes - Expanded UAT/integration/durability test coverage across all milestones
204 lines
6.6 KiB
Rust
204 lines
6.6 KiB
Rust
// Integration-test exemptions (same posture as the tidaldb integration tests):
|
|
// unwrap/unwrap_err on known-good fixtures and short-lived read guards are
|
|
// idiomatic here.
|
|
#![allow(clippy::unwrap_used, clippy::significant_drop_tightening)]
|
|
//! Transport trait contract tests for `GrpcTransport`.
|
|
//!
|
|
//! Mirrors the test patterns from `InProcessTransport` but over gRPC on localhost.
|
|
|
|
use std::{
|
|
collections::HashMap,
|
|
net::SocketAddr,
|
|
sync::{
|
|
Arc,
|
|
atomic::{AtomicBool, Ordering},
|
|
},
|
|
thread,
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
use tidal_net::{GrpcTransport, config::GrpcTransportConfig};
|
|
use tidaldb::replication::{
|
|
WalSegmentId,
|
|
shard::{RegionId, ShardId},
|
|
transport::{Transport, TransportError, WalSegmentPayload},
|
|
};
|
|
|
|
/// Get a unique listen address using port 0 (OS-assigned).
|
|
/// Since tonic doesn't support port 0, we bind a `TcpListener` to find a free port.
|
|
fn free_addr() -> SocketAddr {
|
|
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
|
|
listener.local_addr().unwrap()
|
|
}
|
|
|
|
fn make_config(
|
|
shard: ShardId,
|
|
listen: SocketAddr,
|
|
peers: HashMap<ShardId, SocketAddr>,
|
|
) -> GrpcTransportConfig {
|
|
GrpcTransportConfig {
|
|
local_shard: shard,
|
|
listen_addr: listen,
|
|
peers,
|
|
insecure: true,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn make_payload(shard: ShardId, seqno: u64) -> WalSegmentPayload {
|
|
WalSegmentPayload {
|
|
id: WalSegmentId::new(RegionId::SINGLE, shard, seqno),
|
|
bytes: vec![0xAB; 100],
|
|
event_count: 5,
|
|
leader_last_seq: seqno,
|
|
}
|
|
}
|
|
|
|
/// Build two `GrpcTransports` on localhost that can talk to each other.
|
|
fn build_pair() -> (GrpcTransport, GrpcTransport) {
|
|
let addr0 = free_addr();
|
|
let addr1 = free_addr();
|
|
|
|
let config0 = make_config(ShardId(0), addr0, HashMap::from([(ShardId(1), addr1)]));
|
|
let config1 = make_config(ShardId(1), addr1, HashMap::from([(ShardId(0), addr0)]));
|
|
|
|
let t0 = GrpcTransport::new(config0).expect("transport 0");
|
|
let t1 = GrpcTransport::new(config1).expect("transport 1");
|
|
|
|
// Give servers a moment to start.
|
|
thread::sleep(Duration::from_millis(100));
|
|
|
|
(t0, t1)
|
|
}
|
|
|
|
#[test]
|
|
fn send_and_receive_between_shards() {
|
|
let (t0, t1) = build_pair();
|
|
|
|
// Shard 0 sends to Shard 1.
|
|
let payload = make_payload(ShardId(0), 42);
|
|
t0.send_segment(ShardId(1), payload).unwrap();
|
|
|
|
// Shard 1 receives it.
|
|
let received = t1.recv_segment().unwrap();
|
|
assert_eq!(received.id.seqno, 42);
|
|
assert_eq!(received.event_count, 5);
|
|
assert_eq!(received.bytes.len(), 100);
|
|
}
|
|
|
|
#[test]
|
|
fn send_to_unknown_peer_fails() {
|
|
let addr0 = free_addr();
|
|
let config0 = make_config(ShardId(0), addr0, HashMap::new());
|
|
let t0 = GrpcTransport::new(config0).expect("transport 0");
|
|
thread::sleep(Duration::from_millis(50));
|
|
|
|
let result = t0.send_segment(ShardId(99), make_payload(ShardId(0), 1));
|
|
assert!(result.is_err());
|
|
assert!(matches!(
|
|
result.unwrap_err(),
|
|
TransportError::UnknownPeer(_)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn payload_too_large_rejected() {
|
|
let addr0 = free_addr();
|
|
let addr1 = free_addr();
|
|
let config0 = make_config(ShardId(0), addr0, HashMap::from([(ShardId(1), addr1)]));
|
|
let t0 = GrpcTransport::new(config0).expect("transport 0");
|
|
thread::sleep(Duration::from_millis(50));
|
|
|
|
let payload = WalSegmentPayload {
|
|
id: WalSegmentId::new(RegionId::SINGLE, ShardId(0), 1),
|
|
bytes: vec![0u8; 64 * 1024 * 1024 + 1],
|
|
event_count: 0,
|
|
leader_last_seq: 1,
|
|
};
|
|
let result = t0.send_segment(ShardId(1), payload);
|
|
assert!(result.is_err());
|
|
assert!(matches!(
|
|
result.unwrap_err(),
|
|
TransportError::PayloadTooLarge { .. }
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn local_shard_returns_correct_id() {
|
|
let addr = free_addr();
|
|
let config = make_config(ShardId(7), addr, HashMap::new());
|
|
let t = GrpcTransport::new(config).expect("transport");
|
|
assert_eq!(t.local_shard(), ShardId(7));
|
|
}
|
|
|
|
#[test]
|
|
fn shutdown_unparks_recv_segment_and_thread_exits() {
|
|
// Models the real receiver wiring: a worker thread holds its own
|
|
// Arc<GrpcTransport> clone and parks in recv_segment. Without a cancellation
|
|
// path that thread never returns (recv_segment only yields None when every
|
|
// server-side inbound_tx sender drops), so it would leak until process exit
|
|
// AND its Arc would keep GrpcTransport::drop from ever running. shutdown_receivers
|
|
// breaks the cycle: a parked recv_segment returns None deterministically.
|
|
let addr = free_addr();
|
|
let config = make_config(ShardId(0), addr, HashMap::new());
|
|
let transport = Arc::new(GrpcTransport::new(config).expect("transport"));
|
|
thread::sleep(Duration::from_millis(50));
|
|
|
|
let returned_none = Arc::new(AtomicBool::new(false));
|
|
let worker_transport = Arc::clone(&transport);
|
|
let worker_flag = Arc::clone(&returned_none);
|
|
let receiver = thread::spawn(move || {
|
|
// No segment will ever arrive (no peers); this parks until shutdown.
|
|
let segment = worker_transport.recv_segment();
|
|
worker_flag.store(segment.is_none(), Ordering::SeqCst);
|
|
// Drop the worker's Arc clone on return so only the test's Arc remains.
|
|
});
|
|
|
|
// Let the worker reach the parked recv_segment.
|
|
thread::sleep(Duration::from_millis(100));
|
|
assert!(
|
|
!receiver.is_finished(),
|
|
"receiver should still be parked in recv_segment before shutdown"
|
|
);
|
|
|
|
// Trip the cancellation path.
|
|
transport.shutdown_receivers();
|
|
|
|
// The parked recv_segment must return None promptly and the thread must exit.
|
|
let deadline = Instant::now() + Duration::from_secs(5);
|
|
while !receiver.is_finished() && Instant::now() < deadline {
|
|
thread::sleep(Duration::from_millis(10));
|
|
}
|
|
assert!(
|
|
receiver.is_finished(),
|
|
"recv_segment must return after shutdown_receivers so the thread can exit"
|
|
);
|
|
receiver.join().expect("receiver thread joins cleanly");
|
|
assert!(
|
|
returned_none.load(Ordering::SeqCst),
|
|
"recv_segment must return None on shutdown (the receiver's exit condition)"
|
|
);
|
|
|
|
// Only the test's Arc remains; dropping it runs GrpcTransport::drop without leak.
|
|
assert_eq!(Arc::strong_count(&transport), 1);
|
|
drop(transport);
|
|
}
|
|
|
|
#[test]
|
|
fn multiple_segments_fifo_order() {
|
|
let (t0, t1) = build_pair();
|
|
|
|
for seq in 1..=5u64 {
|
|
t0.send_segment(ShardId(1), make_payload(ShardId(0), seq))
|
|
.unwrap();
|
|
}
|
|
|
|
// Brief pause to let segments flow through gRPC.
|
|
thread::sleep(Duration::from_millis(50));
|
|
|
|
for expected_seq in 1..=5u64 {
|
|
let received = t1.recv_segment().unwrap();
|
|
assert_eq!(received.id.seqno, expected_seq);
|
|
}
|
|
}
|