1use std::net::{SocketAddr, UdpSocket};
27use std::sync::Arc;
28use std::time::{Duration, Instant};
29
30use async_trait::async_trait;
31use chitchat::transport::{Socket, Transport, UdpTransport};
32use object_store::{
33 path::Path as OsPath, CopyOptions, GetOptions, GetResult, ListResult, MultipartUpload,
34 ObjectMeta, ObjectStore, PutMultipartOptions, PutOptions, PutPayload, PutResult,
35};
36use parking_lot::Mutex;
37use rustc_hash::FxHashSet;
38use tokio::sync::watch;
39
40use super::control::{AssignmentSnapshotStore, ChitchatKv, ClusterController, ClusterKv};
41use super::discovery::{
42 Discovery, DiscoveryError, GossipDiscovery, GossipDiscoveryConfig, NodeId, NodeInfo,
43 NodeMetadata, NodeState,
44};
45
46const GOSSIP_BIND_ATTEMPTS: usize = 16;
47
48pub struct NetworkRules {
54 dropped: Mutex<FxHashSet<(SocketAddr, SocketAddr)>>,
55}
56
57impl std::fmt::Debug for NetworkRules {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 f.debug_struct("NetworkRules")
60 .field("drop_count", &self.dropped.lock().len())
61 .finish()
62 }
63}
64
65impl NetworkRules {
66 #[must_use]
68 pub fn new() -> Self {
69 Self {
70 dropped: Mutex::new(FxHashSet::default()),
71 }
72 }
73
74 pub fn partition(&self, side_a: &[SocketAddr], side_b: &[SocketAddr]) {
78 let mut set = self.dropped.lock();
79 for a in side_a {
80 for b in side_b {
81 set.insert((*a, *b));
82 set.insert((*b, *a));
83 }
84 }
85 }
86
87 pub fn drop_pair(&self, src: SocketAddr, dst: SocketAddr) {
89 self.dropped.lock().insert((src, dst));
90 }
91
92 pub fn heal(&self) {
94 self.dropped.lock().clear();
95 }
96
97 #[must_use]
99 pub fn is_dropped(&self, src: SocketAddr, dst: SocketAddr) -> bool {
100 self.dropped.lock().contains(&(src, dst))
101 }
102}
103
104impl Default for NetworkRules {
105 fn default() -> Self {
106 Self::new()
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub enum ObjectStoreFault {
113 None,
115 FailWrites,
117 FailReads,
119 FailAll,
121}
122
123impl ObjectStoreFault {
124 fn fails_writes(self) -> bool {
125 matches!(self, Self::FailWrites | Self::FailAll)
126 }
127 fn fails_reads(self) -> bool {
128 matches!(self, Self::FailReads | Self::FailAll)
129 }
130}
131
132pub struct FaultyObjectStore {
134 inner: Arc<dyn ObjectStore>,
135 fault: Mutex<ObjectStoreFault>,
136}
137
138impl std::fmt::Debug for FaultyObjectStore {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 f.debug_struct("FaultyObjectStore")
141 .field("fault", &self.fault())
142 .finish_non_exhaustive()
143 }
144}
145
146impl std::fmt::Display for FaultyObjectStore {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 write!(f, "FaultyObjectStore({:?})", self.fault())
149 }
150}
151
152impl FaultyObjectStore {
153 #[must_use]
155 pub fn new(inner: Arc<dyn ObjectStore>) -> Self {
156 Self {
157 inner,
158 fault: Mutex::new(ObjectStoreFault::None),
159 }
160 }
161
162 #[must_use]
164 pub fn fault(&self) -> ObjectStoreFault {
165 *self.fault.lock()
166 }
167
168 pub fn set_fault(&self, mode: ObjectStoreFault) {
170 *self.fault.lock() = mode;
171 }
172
173 fn check_write(&self) -> object_store::Result<()> {
174 if self.fault().fails_writes() {
175 return Err(object_store::Error::Generic {
176 store: "FaultyObjectStore",
177 source: "injected write failure".into(),
178 });
179 }
180 Ok(())
181 }
182
183 fn check_read(&self, path: &OsPath) -> object_store::Result<()> {
184 if self.fault().fails_reads() {
185 return Err(object_store::Error::NotFound {
186 path: path.to_string(),
187 source: "injected read failure".into(),
188 });
189 }
190 Ok(())
191 }
192}
193
194#[async_trait]
195impl ObjectStore for FaultyObjectStore {
196 async fn put_opts(
197 &self,
198 location: &OsPath,
199 payload: PutPayload,
200 opts: PutOptions,
201 ) -> object_store::Result<PutResult> {
202 self.check_write()?;
203 self.inner.put_opts(location, payload, opts).await
204 }
205
206 async fn put_multipart_opts(
207 &self,
208 location: &OsPath,
209 opts: PutMultipartOptions,
210 ) -> object_store::Result<Box<dyn MultipartUpload>> {
211 self.check_write()?;
212 self.inner.put_multipart_opts(location, opts).await
213 }
214
215 async fn get_opts(
216 &self,
217 location: &OsPath,
218 options: GetOptions,
219 ) -> object_store::Result<GetResult> {
220 self.check_read(location)?;
221 self.inner.get_opts(location, options).await
222 }
223
224 fn delete_stream(
225 &self,
226 locations: futures::stream::BoxStream<'static, object_store::Result<OsPath>>,
227 ) -> futures::stream::BoxStream<'static, object_store::Result<OsPath>> {
228 if self.fault().fails_writes() {
231 use futures::StreamExt;
232 locations
233 .map(|_| {
234 Err(object_store::Error::Generic {
235 store: "FaultyObjectStore",
236 source: "injected write failure (delete_stream)".into(),
237 })
238 })
239 .boxed()
240 } else {
241 self.inner.delete_stream(locations)
242 }
243 }
244
245 fn list(
246 &self,
247 prefix: Option<&OsPath>,
248 ) -> futures::stream::BoxStream<'static, object_store::Result<ObjectMeta>> {
249 self.inner.list(prefix)
250 }
251
252 async fn list_with_delimiter(
253 &self,
254 prefix: Option<&OsPath>,
255 ) -> object_store::Result<ListResult> {
256 self.inner.list_with_delimiter(prefix).await
257 }
258
259 async fn copy_opts(
260 &self,
261 from: &OsPath,
262 to: &OsPath,
263 options: CopyOptions,
264 ) -> object_store::Result<()> {
265 self.check_write()?;
266 self.inner.copy_opts(from, to, options).await
267 }
268}
269
270pub struct PartitionableTransport {
274 rules: Arc<NetworkRules>,
275 inner: UdpTransport,
276}
277
278impl PartitionableTransport {
279 #[must_use]
281 pub fn new(rules: Arc<NetworkRules>) -> Self {
282 Self {
283 rules,
284 inner: UdpTransport,
285 }
286 }
287}
288
289#[async_trait]
290impl Transport for PartitionableTransport {
291 async fn open(&self, listen_addr: SocketAddr) -> anyhow::Result<Box<dyn Socket>> {
292 let socket = self.inner.open(listen_addr).await?;
293 Ok(Box::new(PartitionableSocket {
294 my_addr: listen_addr,
295 rules: Arc::clone(&self.rules),
296 inner: socket,
297 }))
298 }
299}
300
301struct PartitionableSocket {
302 my_addr: SocketAddr,
303 rules: Arc<NetworkRules>,
304 inner: Box<dyn Socket>,
305}
306
307#[async_trait]
308impl Socket for PartitionableSocket {
309 async fn send(&mut self, to: SocketAddr, msg: chitchat::ChitchatMessage) -> anyhow::Result<()> {
310 if self.rules.is_dropped(self.my_addr, to) {
311 return Ok(());
315 }
316 self.inner.send(to, msg).await
317 }
318
319 async fn recv(&mut self) -> anyhow::Result<(SocketAddr, chitchat::ChitchatMessage)> {
320 self.inner.recv().await
321 }
322}
323
324fn available_gossip_addr() -> String {
326 let sock = UdpSocket::bind("127.0.0.1:0").expect("bind 127.0.0.1:0");
327 let port = sock.local_addr().expect("local_addr").port();
328 format!("127.0.0.1:{port}")
329}
330
331async fn start_discovery(
332 mut config: GossipDiscoveryConfig,
333 rules: Option<&Arc<NetworkRules>>,
334) -> (GossipDiscovery, String) {
335 let mut last_bind_error = None;
336 for _ in 0..GOSSIP_BIND_ATTEMPTS {
337 let gossip_addr = config.gossip_address.clone();
338 let mut discovery = GossipDiscovery::new(config.clone());
339 let result = match rules {
340 Some(rules) => {
341 let transport = PartitionableTransport::new(Arc::clone(rules));
342 discovery.start_with_transport(&transport).await
343 }
344 None => discovery.start().await,
345 };
346 match result {
347 Ok(()) => return (discovery, gossip_addr),
348 Err(DiscoveryError::Bind(error)) => {
349 last_bind_error = Some(error);
350 config.gossip_address = available_gossip_addr();
351 }
352 Err(error) => panic!("chitchat start failed: {error}"),
353 }
354 }
355 panic!(
356 "chitchat failed to bind a loopback port after {GOSSIP_BIND_ATTEMPTS} attempts: {}",
357 last_bind_error.as_deref().unwrap_or("unknown bind error")
358 );
359}
360
361pub struct NodeHandle {
363 pub instance_id: NodeId,
365 process_generation: u64,
366 pub gossip_addr: String,
368 pub controller: Arc<ClusterController>,
371 discovery: GossipDiscovery,
373}
374
375impl NodeHandle {
376 pub async fn kill(mut self) {
386 let left = NodeInfo {
387 state: NodeState::Left,
388 ..current_info(&self)
389 };
390 let _ = self.discovery.announce(left).await;
391 tokio::time::sleep(Duration::from_millis(150)).await;
394 let _ = self.discovery.stop().await;
395 }
396
397 pub async fn crash(mut self) {
410 let _ = self.discovery.stop().await;
411 }
412}
413
414fn current_info(node: &NodeHandle) -> NodeInfo {
415 NodeInfo {
416 id: node.instance_id,
417 name: format!("minicluster-n{}", node.instance_id.0),
418 rpc_address: String::new(),
419 state: NodeState::Active,
420 metadata: minicluster_metadata(node.instance_id, node.process_generation),
421 last_heartbeat_ms: 0,
422 }
423}
424
425fn minicluster_metadata(instance_id: NodeId, process_generation: u64) -> NodeMetadata {
426 let incarnation =
427 uuid::Uuid::from_u128((u128::from(process_generation) << 64) | u128::from(instance_id.0));
428 let mut metadata = NodeMetadata {
429 cores: 1,
430 version: env!("CARGO_PKG_VERSION").into(),
431 ..NodeMetadata::default()
432 };
433 metadata.tags.insert(
434 "laminardb.process-incarnation".into(),
435 incarnation.to_string(),
436 );
437 metadata
438}
439
440pub struct MiniCluster {
442 pub nodes: Vec<NodeHandle>,
445 pub rules: Option<Arc<NetworkRules>>,
449 pub snapshot: Option<Arc<AssignmentSnapshotStore>>,
453}
454
455impl MiniCluster {
456 pub async fn spawn(n: usize) -> Self {
464 Self::spawn_inner(n, None, None).await
465 }
466
467 pub async fn spawn_partitionable(n: usize) -> Self {
476 let rules = Arc::new(NetworkRules::new());
477 Self::spawn_inner(n, Some(rules), None).await
478 }
479
480 pub async fn spawn_with_snapshot(n: usize, snapshot: Arc<AssignmentSnapshotStore>) -> Self {
486 Self::spawn_inner(n, None, Some(snapshot)).await
487 }
488
489 pub async fn join_node(&mut self, instance_id: NodeId) {
498 assert!(!self.nodes.is_empty(), "cannot join empty cluster");
499 let seeds: Vec<String> = self.nodes.iter().map(|n| n.gossip_addr.clone()).collect();
503 let process_generation = 2;
504 let local_node = NodeInfo {
505 id: instance_id,
506 name: format!("minicluster-rejoin-{}", instance_id.0),
507 rpc_address: String::new(),
508 state: NodeState::Active,
509 metadata: minicluster_metadata(instance_id, process_generation),
510 last_heartbeat_ms: 0,
511 };
512
513 let cfg = GossipDiscoveryConfig {
514 gossip_address: available_gossip_addr(),
515 seed_nodes: seeds,
516 gossip_interval: Duration::from_millis(50),
517 phi_threshold: 3.0,
518 dead_node_grace_period: Duration::from_secs(1),
519 cluster_id: "minicluster".to_string(),
520 node_id: instance_id,
521 process_generation,
522 local_node,
523 advertise_host: None,
524 };
525 let (discovery, gossip_addr) = start_discovery(cfg, self.rules.as_ref()).await;
526
527 let handle = discovery
528 .chitchat_handle()
529 .expect("chitchat handle available after start");
530 let kv: Arc<dyn ClusterKv> = Arc::new(ChitchatKv::from_handle(handle));
531 let members_rx = discovery.membership_watch();
532 let controller = Arc::new(ClusterController::new(
533 instance_id,
534 kv,
535 self.snapshot.clone(),
536 members_rx,
537 ));
538
539 self.nodes.push(NodeHandle {
540 instance_id,
541 process_generation,
542 gossip_addr,
543 controller,
544 discovery,
545 });
546 }
547
548 async fn spawn_inner(
549 n: usize,
550 rules: Option<Arc<NetworkRules>>,
551 snapshot: Option<Arc<AssignmentSnapshotStore>>,
552 ) -> Self {
553 assert!(n >= 1, "MiniCluster needs at least one node");
554
555 let mut nodes: Vec<NodeHandle> = Vec::with_capacity(n);
556 for idx in 0..n {
557 let instance_id = NodeId((idx as u64) + 1); let process_generation = 1;
559
560 let local_node = NodeInfo {
561 id: instance_id,
562 name: format!("minicluster-n{idx}"),
563 rpc_address: String::new(),
564 state: NodeState::Active,
565 metadata: minicluster_metadata(instance_id, process_generation),
566 last_heartbeat_ms: 0,
567 };
568
569 let seeds = if idx == 0 {
570 Vec::new()
571 } else {
572 vec![nodes[0].gossip_addr.clone()]
573 };
574 let cfg = GossipDiscoveryConfig {
578 gossip_address: available_gossip_addr(),
579 seed_nodes: seeds,
580 gossip_interval: Duration::from_millis(50),
581 phi_threshold: 3.0,
582 dead_node_grace_period: Duration::from_secs(1),
583 cluster_id: "minicluster".to_string(),
584 node_id: instance_id,
585 process_generation,
586 local_node,
587 advertise_host: None,
588 };
589 let (discovery, gossip_addr) = start_discovery(cfg, rules.as_ref()).await;
590
591 let handle = discovery
592 .chitchat_handle()
593 .expect("chitchat handle available after start");
594 let kv: Arc<dyn ClusterKv> = Arc::new(ChitchatKv::from_handle(handle));
595 let members_rx: watch::Receiver<Vec<NodeInfo>> = discovery.membership_watch();
596 let controller = Arc::new(ClusterController::new(
597 instance_id,
598 kv,
599 snapshot.clone(),
600 members_rx,
601 ));
602
603 nodes.push(NodeHandle {
604 instance_id,
605 process_generation,
606 gossip_addr,
607 controller,
608 discovery,
609 });
610 }
611 Self {
612 nodes,
613 rules,
614 snapshot,
615 }
616 }
617
618 #[must_use]
626 pub fn addrs(&self) -> Vec<SocketAddr> {
627 self.nodes
628 .iter()
629 .map(|n| n.gossip_addr.parse().expect("valid gossip_addr"))
630 .collect()
631 }
632
633 pub async fn wait_for_convergence(&self, deadline: Duration) -> Result<(), String> {
640 let start = Instant::now();
641 loop {
642 let mut all_converged = true;
643 let mut missing_summary = Vec::new();
644 for node in &self.nodes {
645 let peers = node
646 .discovery
647 .peers()
648 .await
649 .map_err(|e| format!("peers() failed on {}: {e}", node.instance_id.0))?;
650 let expected = self.nodes.len() - 1;
651 if peers.len() < expected {
652 all_converged = false;
653 missing_summary.push(format!(
654 "node {} sees {} peers (expected {})",
655 node.instance_id.0,
656 peers.len(),
657 expected
658 ));
659 }
660 }
661 if all_converged {
662 return Ok(());
663 }
664 if start.elapsed() >= deadline {
665 return Err(format!(
666 "convergence timeout after {:?}: {}",
667 deadline,
668 missing_summary.join("; "),
669 ));
670 }
671 tokio::time::sleep(Duration::from_millis(100)).await;
672 }
673 }
674
675 pub async fn shutdown(mut self) {
677 for node in self.nodes.drain(..) {
678 node.kill().await;
679 }
680 }
681}