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