Skip to main content

oxirs_tdb/distributed/
integration.rs

1//! Distributed Features Integration Layer
2//!
3//! This module provides a high-level integration layer that connects distributed
4//! transaction features (2PC, 3PC, Paxos, Saga) with the TdbStore, making it easy
5//! to use distributed capabilities without managing low-level details.
6//!
7//! # Features
8//!
9//! - **Unified API**: Single interface for all distributed transaction protocols
10//! - **Automatic Protocol Selection**: Choose optimal protocol based on requirements
11//! - **Built-in Deadlock Prevention**: Integrated deadlock detection and resolution
12//! - **Replication Integration**: Coordinate transactions with replication
13//! - **Monitoring**: Comprehensive metrics and health tracking
14//!
15//! # Example
16//!
17//! ```rust,ignore
18//! use oxirs_tdb::distributed::integration::{DistributedTdbStore, DistributedConfig};
19//! use oxirs_tdb::consensus::transport::LoopbackSimulationTransport;
20//!
21//! // Create distributed store. A real multi-node deployment must supply a
22//! // transport backed by real network I/O; LoopbackSimulationTransport is
23//! // only for single-process tests/local development, and `new()` (no
24//! // transport) will make every commit fail loudly instead of fabricating
25//! // success.
26//! let config = DistributedConfig::default();
27//! let mut store = DistributedTdbStore::with_transport(
28//!     "node1".to_string(),
29//!     config,
30//!     LoopbackSimulationTransport::arc(),
31//! );
32//!
33//! // Register nodes
34//! store.register_node("node1", "http://node1:8080").await?;
35//! store.register_node("node2", "http://node2:8080").await?;
36//!
37//! // Execute distributed transaction
38//! let txn_id = store.begin_distributed_transaction().await?;
39//! // ... perform operations ...
40//! store.commit_distributed_transaction(&txn_id).await?;
41//! ```
42
43use crate::consensus::transport::NetworkTransport;
44use crate::distributed::coordinator::{CommitProtocol, CoordinatorConfig, TransactionCoordinator};
45use crate::distributed::deadlock::{DeadlockDetectorConfig, DistributedDeadlockDetector};
46use crate::distributed::replication::{ReplicationConfig, ReplicationManager};
47use crate::distributed::saga::{SagaConfig, SagaOrchestrator};
48use crate::error::{Result, TdbError};
49use chrono::Utc;
50use parking_lot::{Mutex, RwLock};
51use serde::{Deserialize, Serialize};
52use std::collections::HashMap;
53use std::sync::Arc;
54
55/// Distributed store configuration
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct DistributedConfig {
58    /// Transaction coordinator configuration
59    pub coordinator_config: CoordinatorConfig,
60    /// Deadlock detector configuration
61    pub deadlock_config: DeadlockDetectorConfig,
62    /// Replication configuration
63    pub replication_config: ReplicationConfig,
64    /// Saga configuration
65    pub saga_config: SagaConfig,
66    /// Default commit protocol
67    pub default_protocol: CommitProtocol,
68    /// Enable automatic deadlock detection
69    pub enable_deadlock_detection: bool,
70    /// Enable replication
71    pub enable_replication: bool,
72}
73
74impl Default for DistributedConfig {
75    fn default() -> Self {
76        Self {
77            coordinator_config: CoordinatorConfig::default(),
78            deadlock_config: DeadlockDetectorConfig::default(),
79            replication_config: ReplicationConfig::default(),
80            saga_config: SagaConfig::default(),
81            default_protocol: CommitProtocol::TwoPhase,
82            enable_deadlock_detection: true,
83            enable_replication: true,
84        }
85    }
86}
87
88/// Distributed transaction handle
89#[derive(Debug, Clone)]
90pub struct DistributedTransaction {
91    /// Transaction ID
92    pub txn_id: String,
93    /// Protocol used
94    pub protocol: CommitProtocol,
95    /// Participating nodes
96    pub nodes: Vec<String>,
97}
98
99/// Distributed TDB Store
100///
101/// High-level interface for distributed RDF storage with integrated
102/// transaction coordination, deadlock detection, and replication.
103pub struct DistributedTdbStore {
104    /// Node ID
105    node_id: String,
106    /// Configuration
107    config: DistributedConfig,
108    /// Transaction coordinator
109    coordinator: Arc<Mutex<TransactionCoordinator>>,
110    /// Deadlock detector (optional)
111    deadlock_detector: Option<Arc<Mutex<DistributedDeadlockDetector>>>,
112    /// Replication manager (optional)
113    replication_manager: Option<Arc<Mutex<ReplicationManager>>>,
114    /// Active distributed transactions
115    active_transactions: Arc<RwLock<HashMap<String, DistributedTransaction>>>,
116    /// Statistics
117    stats: Arc<Mutex<DistributedStoreStats>>,
118}
119
120/// Distributed store statistics
121#[derive(Debug, Clone, Default, Serialize, Deserialize)]
122pub struct DistributedStoreStats {
123    /// Total distributed transactions
124    pub total_distributed_txns: u64,
125    /// Successful distributed transactions
126    pub successful_distributed_txns: u64,
127    /// Failed distributed transactions
128    pub failed_distributed_txns: u64,
129    /// Deadlocks detected and resolved
130    pub deadlocks_resolved: u64,
131    /// Replications performed
132    pub replications_performed: u64,
133    /// Average transaction latency (milliseconds)
134    pub avg_txn_latency_ms: f64,
135    /// Total latency
136    total_latency_ms: f64,
137}
138
139impl DistributedTdbStore {
140    /// Create a new Distributed TDB Store with no network transport
141    /// configured.
142    ///
143    /// **Note:** because no [`NetworkTransport`] is supplied, the underlying
144    /// [`TransactionCoordinator`] and [`ReplicationManager`] cannot honestly
145    /// reach any other node: `commit_distributed_transaction()` will fail
146    /// with [`TdbError::DistributedTransportNotConfigured`] for every
147    /// protocol rather than fabricating success. Use
148    /// [`DistributedTdbStore::with_transport`] (with a real network-backed
149    /// transport, or
150    /// [`crate::consensus::transport::LoopbackSimulationTransport`] for
151    /// single-node tests) to actually drive distributed commits.
152    pub fn new(node_id: String, config: DistributedConfig) -> Self {
153        let coordinator = Arc::new(Mutex::new(TransactionCoordinator::new(
154            node_id.clone(),
155            config.coordinator_config.clone(),
156        )));
157
158        let deadlock_detector = if config.enable_deadlock_detection {
159            Some(Arc::new(Mutex::new(DistributedDeadlockDetector::new(
160                format!("{}-deadlock-detector", node_id),
161                config.deadlock_config.clone(),
162            ))))
163        } else {
164            None
165        };
166
167        let replication_manager = if config.enable_replication {
168            Some(Arc::new(Mutex::new(ReplicationManager::new(
169                node_id.clone(),
170                config.replication_config.clone(),
171            ))))
172        } else {
173            None
174        };
175
176        Self {
177            node_id,
178            config,
179            coordinator,
180            deadlock_detector,
181            replication_manager,
182            active_transactions: Arc::new(RwLock::new(HashMap::new())),
183            stats: Arc::new(Mutex::new(DistributedStoreStats::default())),
184        }
185    }
186
187    /// Create a new Distributed TDB Store backed by a real (or
188    /// explicitly-simulated) [`NetworkTransport`], shared by both the
189    /// transaction coordinator and the replication manager. This is the
190    /// constructor production code should use to actually drive distributed
191    /// commits and replication.
192    pub fn with_transport(
193        node_id: String,
194        config: DistributedConfig,
195        transport: Arc<dyn NetworkTransport>,
196    ) -> Self {
197        let coordinator = Arc::new(Mutex::new(TransactionCoordinator::with_transport(
198            node_id.clone(),
199            config.coordinator_config.clone(),
200            transport.clone(),
201        )));
202
203        let deadlock_detector = if config.enable_deadlock_detection {
204            Some(Arc::new(Mutex::new(DistributedDeadlockDetector::new(
205                format!("{}-deadlock-detector", node_id),
206                config.deadlock_config.clone(),
207            ))))
208        } else {
209            None
210        };
211
212        let replication_manager = if config.enable_replication {
213            Some(Arc::new(Mutex::new(ReplicationManager::with_transport(
214                node_id.clone(),
215                config.replication_config.clone(),
216                transport,
217            ))))
218        } else {
219            None
220        };
221
222        Self {
223            node_id,
224            config,
225            coordinator,
226            deadlock_detector,
227            replication_manager,
228            active_transactions: Arc::new(RwLock::new(HashMap::new())),
229            stats: Arc::new(Mutex::new(DistributedStoreStats::default())),
230        }
231    }
232
233    /// Register a node in the distributed system
234    #[allow(clippy::await_holding_lock)]
235    pub async fn register_node(&mut self, node_id: &str, endpoint: &str) -> Result<()> {
236        // Register with coordinator
237        {
238            self.coordinator
239                .lock()
240                .register_participant(node_id.to_string(), endpoint.to_string())
241                .await?;
242        }
243
244        // Register with deadlock detector
245        if let Some(ref detector) = self.deadlock_detector {
246            detector.lock().register_node(node_id.to_string()).await?;
247        }
248
249        // Register with replication manager
250        if let Some(ref manager) = self.replication_manager {
251            manager
252                .lock()
253                .add_replica(node_id.to_string(), endpoint.to_string())
254                .await?;
255        }
256
257        Ok(())
258    }
259
260    /// Begin a distributed transaction
261    pub async fn begin_distributed_transaction(&mut self) -> Result<String> {
262        self.begin_distributed_transaction_with_protocol(self.config.default_protocol)
263            .await
264    }
265
266    /// Begin a distributed transaction with specific protocol
267    #[allow(clippy::await_holding_lock)]
268    pub async fn begin_distributed_transaction_with_protocol(
269        &mut self,
270        protocol: CommitProtocol,
271    ) -> Result<String> {
272        let txn_id = self.coordinator.lock().begin_transaction(protocol).await?;
273
274        let metadata = {
275            let coordinator = self.coordinator.lock();
276            coordinator
277                .get_transaction(&txn_id)
278                .ok_or_else(|| TdbError::Other("Transaction not found".to_string()))?
279        };
280
281        let txn = DistributedTransaction {
282            txn_id: txn_id.clone(),
283            protocol,
284            nodes: metadata.participants.clone(),
285        };
286
287        self.active_transactions.write().insert(txn_id.clone(), txn);
288
289        let mut stats = self.stats.lock();
290        stats.total_distributed_txns += 1;
291
292        Ok(txn_id)
293    }
294
295    /// Commit a distributed transaction
296    #[allow(clippy::await_holding_lock)]
297    pub async fn commit_distributed_transaction(&mut self, txn_id: &str) -> Result<bool> {
298        let start = std::time::Instant::now();
299
300        // Check for deadlocks before commit
301        if let Some(ref detector) = self.deadlock_detector {
302            let deadlocks = detector.lock().detect_deadlocks().await?;
303
304            if !deadlocks.is_empty() {
305                // Abort victims
306                for deadlock in &deadlocks {
307                    if let Some(ref victim) = deadlock.victim {
308                        detector.lock().abort_victim(victim).await?;
309
310                        let mut stats = self.stats.lock();
311                        stats.deadlocks_resolved += 1;
312                    }
313                }
314            }
315        }
316
317        // Execute commit
318        let result = self.coordinator.lock().commit_transaction(txn_id).await?;
319
320        // Replicate if enabled and successful
321        if result {
322            if let Some(ref manager) = self.replication_manager {
323                // NOTE: Transport is in-process simulation.
324                // Record the committed transaction changes in the replication log.
325                manager
326                    .lock()
327                    .record_change(txn_id.to_string(), txn_id.as_bytes().to_vec())
328                    .await?;
329                let mut stats = self.stats.lock();
330                stats.replications_performed += 1;
331            }
332        }
333
334        // Remove from active transactions
335        self.active_transactions.write().remove(txn_id);
336
337        // Update statistics
338        let latency = start.elapsed().as_millis() as f64;
339        let mut stats = self.stats.lock();
340
341        if result {
342            stats.successful_distributed_txns += 1;
343        } else {
344            stats.failed_distributed_txns += 1;
345        }
346
347        stats.total_latency_ms += latency;
348        stats.avg_txn_latency_ms = stats.total_latency_ms / stats.total_distributed_txns as f64;
349
350        Ok(result)
351    }
352
353    /// Abort a distributed transaction
354    #[allow(clippy::await_holding_lock)]
355    pub async fn abort_distributed_transaction(&mut self, txn_id: &str) -> Result<()> {
356        self.coordinator.lock().abort_transaction(txn_id).await?;
357
358        self.active_transactions.write().remove(txn_id);
359
360        let mut stats = self.stats.lock();
361        stats.failed_distributed_txns += 1;
362
363        Ok(())
364    }
365
366    /// Create and execute a saga
367    #[allow(clippy::await_holding_lock)]
368    pub async fn execute_saga(&mut self, mut saga: SagaOrchestrator) -> Result<bool> {
369        // NOTE: Transport is in-process simulation.
370        // Execute the saga; replicate committed steps, and the compensation path if rolled back.
371        let result = saga.execute().await?;
372
373        // Replicate saga outcome to replication log
374        if let Some(ref manager) = self.replication_manager {
375            let saga_key = Utc::now().timestamp_millis().to_string();
376            let change_data = if result {
377                format!("saga:committed:{}", saga_key).into_bytes()
378            } else {
379                format!("saga:compensated:{}", saga_key).into_bytes()
380            };
381            manager.lock().record_change(saga_key, change_data).await?;
382        }
383
384        // Update coordinator stats
385        {
386            let mut stats = self.stats.lock();
387            if result {
388                stats.successful_distributed_txns += 1;
389            } else {
390                stats.failed_distributed_txns += 1;
391            }
392            stats.total_distributed_txns += 1;
393        }
394
395        Ok(result)
396    }
397
398    /// Get node ID
399    pub fn node_id(&self) -> &str {
400        &self.node_id
401    }
402
403    /// Get active transaction count
404    pub fn active_transaction_count(&self) -> usize {
405        self.active_transactions.read().len()
406    }
407
408    /// Get statistics
409    pub fn stats(&self) -> DistributedStoreStats {
410        self.stats.lock().clone()
411    }
412
413    /// Get coordinator statistics
414    pub fn coordinator_stats(&self) -> crate::distributed::coordinator::CoordinatorStats {
415        self.coordinator.lock().stats()
416    }
417
418    /// Get replication statistics (if enabled)
419    pub fn replication_stats(&self) -> Option<crate::distributed::replication::ReplicationStats> {
420        self.replication_manager.as_ref().map(|m| m.lock().stats())
421    }
422
423    /// Check system health
424    pub async fn check_health(&self) -> Result<HealthStatus> {
425        let coordinator_health = self.coordinator.lock().healthy_participant_count() > 0;
426
427        let replication_health = if let Some(ref manager) = self.replication_manager {
428            manager.lock().healthy_replica_count() > 0
429        } else {
430            true
431        };
432
433        let overall_health = coordinator_health && replication_health;
434
435        Ok(HealthStatus {
436            healthy: overall_health,
437            coordinator_nodes: self.coordinator.lock().participant_count(),
438            healthy_coordinators: self.coordinator.lock().healthy_participant_count(),
439            replica_count: self
440                .replication_manager
441                .as_ref()
442                .map(|m| m.lock().replica_count())
443                .unwrap_or(0),
444            healthy_replicas: self
445                .replication_manager
446                .as_ref()
447                .map(|m| m.lock().healthy_replica_count())
448                .unwrap_or(0),
449            active_transactions: self.active_transaction_count(),
450        })
451    }
452}
453
454/// System health status
455#[derive(Debug, Clone, Serialize, Deserialize)]
456pub struct HealthStatus {
457    /// Overall health
458    pub healthy: bool,
459    /// Total coordinator nodes
460    pub coordinator_nodes: usize,
461    /// Healthy coordinator nodes
462    pub healthy_coordinators: usize,
463    /// Total replicas
464    pub replica_count: usize,
465    /// Healthy replicas
466    pub healthy_replicas: usize,
467    /// Active transactions
468    pub active_transactions: usize,
469}
470
471#[cfg(test)]
472mod tests {
473    use super::*;
474
475    #[tokio::test]
476    #[ignore = "Distributed tests hang - needs network mock or investigation"]
477    async fn test_distributed_store_creation() {
478        let config = DistributedConfig::default();
479        let store = DistributedTdbStore::new("node1".to_string(), config);
480
481        assert_eq!(store.node_id(), "node1");
482        assert_eq!(store.active_transaction_count(), 0);
483    }
484
485    #[tokio::test]
486    #[ignore = "Distributed tests hang - needs network mock or investigation"]
487    async fn test_register_node() {
488        let config = DistributedConfig::default();
489        let mut store = DistributedTdbStore::new("node1".to_string(), config);
490
491        store
492            .register_node("node2", "http://node2:8080")
493            .await
494            .unwrap();
495
496        // Node should be registered with coordinator
497        assert!(store.coordinator.lock().participant_count() > 0);
498    }
499
500    #[tokio::test]
501    #[ignore = "Distributed tests hang - needs network mock or investigation"]
502    async fn test_begin_distributed_transaction() {
503        let config = DistributedConfig::default();
504        let mut store = DistributedTdbStore::new("node1".to_string(), config);
505
506        store
507            .register_node("node2", "http://node2:8080")
508            .await
509            .unwrap();
510
511        let txn_id = store.begin_distributed_transaction().await.unwrap();
512
513        assert!(!txn_id.is_empty());
514        assert_eq!(store.active_transaction_count(), 1);
515    }
516
517    #[tokio::test]
518    #[ignore = "Distributed tests hang - needs network mock or investigation"]
519    async fn test_commit_distributed_transaction() {
520        let config = DistributedConfig::default();
521        let mut store = DistributedTdbStore::new("node1".to_string(), config);
522
523        store
524            .register_node("node2", "http://node2:8080")
525            .await
526            .unwrap();
527
528        let txn_id = store.begin_distributed_transaction().await.unwrap();
529
530        let result = store.commit_distributed_transaction(&txn_id).await.unwrap();
531
532        assert!(result);
533        assert_eq!(store.active_transaction_count(), 0);
534
535        let stats = store.stats();
536        assert_eq!(stats.successful_distributed_txns, 1);
537    }
538
539    #[tokio::test]
540    #[ignore = "Distributed tests hang - needs network mock or investigation"]
541    async fn test_abort_distributed_transaction() {
542        let config = DistributedConfig::default();
543        let mut store = DistributedTdbStore::new("node1".to_string(), config);
544
545        store
546            .register_node("node2", "http://node2:8080")
547            .await
548            .unwrap();
549
550        let txn_id = store.begin_distributed_transaction().await.unwrap();
551
552        store.abort_distributed_transaction(&txn_id).await.unwrap();
553
554        assert_eq!(store.active_transaction_count(), 0);
555
556        let stats = store.stats();
557        assert_eq!(stats.failed_distributed_txns, 1);
558    }
559
560    #[tokio::test]
561    #[ignore = "Distributed tests hang - needs network mock or investigation"]
562    async fn test_health_check() {
563        let config = DistributedConfig::default();
564        let store = DistributedTdbStore::new("node1".to_string(), config);
565
566        let health = store.check_health().await.unwrap();
567
568        assert_eq!(health.active_transactions, 0);
569    }
570
571    #[tokio::test]
572    #[ignore = "Distributed tests hang - needs network mock or investigation"]
573    async fn test_statistics() {
574        let config = DistributedConfig::default();
575        let store = DistributedTdbStore::new("node1".to_string(), config);
576
577        let stats = store.stats();
578        assert_eq!(stats.total_distributed_txns, 0);
579
580        let coord_stats = store.coordinator_stats();
581        assert_eq!(coord_stats.total_transactions, 0);
582    }
583
584    #[tokio::test]
585    #[ignore = "Distributed tests hang - needs network mock or investigation"]
586    async fn test_protocol_selection() {
587        let config = DistributedConfig::default();
588        let mut store = DistributedTdbStore::new("node1".to_string(), config);
589
590        store
591            .register_node("node2", "http://node2:8080")
592            .await
593            .unwrap();
594
595        // Test with 3PC
596        let txn_id = store
597            .begin_distributed_transaction_with_protocol(CommitProtocol::ThreePhase)
598            .await
599            .unwrap();
600
601        let txn = store.active_transactions.read().get(&txn_id).cloned();
602        assert!(txn.is_some());
603        assert_eq!(txn.unwrap().protocol, CommitProtocol::ThreePhase);
604    }
605
606    #[tokio::test]
607    #[ignore = "Distributed tests hang - needs network mock or investigation"]
608    async fn test_multiple_transactions() {
609        let config = DistributedConfig::default();
610        let mut store = DistributedTdbStore::new("node1".to_string(), config);
611
612        store
613            .register_node("node2", "http://node2:8080")
614            .await
615            .unwrap();
616
617        let txn1 = store.begin_distributed_transaction().await.unwrap();
618        let txn2 = store.begin_distributed_transaction().await.unwrap();
619
620        assert_eq!(store.active_transaction_count(), 2);
621
622        store.commit_distributed_transaction(&txn1).await.unwrap();
623        assert_eq!(store.active_transaction_count(), 1);
624
625        store.abort_distributed_transaction(&txn2).await.unwrap();
626        assert_eq!(store.active_transaction_count(), 0);
627    }
628
629    #[tokio::test]
630    async fn test_commit_triggers_replication() {
631        use crate::consensus::transport::LoopbackSimulationTransport;
632
633        let config = DistributedConfig {
634            enable_deadlock_detection: false,
635            ..Default::default()
636        };
637        let mut store = DistributedTdbStore::with_transport(
638            "node1".to_string(),
639            config,
640            LoopbackSimulationTransport::arc(),
641        );
642        store
643            .register_node("node2", "http://node2:8080")
644            .await
645            .unwrap();
646
647        let txn_id = store.begin_distributed_transaction().await.unwrap();
648        let result = store.commit_distributed_transaction(&txn_id).await.unwrap();
649        assert!(result);
650
651        let stats = store.stats();
652        assert_eq!(stats.replications_performed, 1);
653    }
654
655    /// Regression test: a `DistributedTdbStore` built via the transport-less
656    /// `new()` constructor must fail loudly on commit instead of fabricating
657    /// success, since there is no way to honestly reach any participant.
658    #[tokio::test]
659    async fn test_commit_without_transport_fails_loudly() {
660        let config = DistributedConfig {
661            enable_deadlock_detection: false,
662            ..Default::default()
663        };
664        let mut store = DistributedTdbStore::new("node1".to_string(), config);
665        store
666            .register_node("node2", "http://node2:8080")
667            .await
668            .unwrap();
669
670        let txn_id = store.begin_distributed_transaction().await.unwrap();
671        let err = store
672            .commit_distributed_transaction(&txn_id)
673            .await
674            .expect_err("commit without a configured transport must fail, not fabricate success");
675        assert!(
676            matches!(err, TdbError::DistributedTransportNotConfigured { .. }),
677            "expected DistributedTransportNotConfigured, got {err:?}"
678        );
679    }
680
681    #[tokio::test]
682    async fn test_execute_saga_success_replicates() {
683        use crate::distributed::saga::{SagaConfig, SagaOrchestrator, SagaStep};
684        use std::sync::atomic::{AtomicU32, Ordering};
685        use std::sync::Arc as StdArc;
686
687        let config = DistributedConfig {
688            enable_deadlock_detection: false,
689            ..Default::default()
690        };
691        let mut store = DistributedTdbStore::new("node1".to_string(), config);
692        store
693            .register_node("node2", "http://node2:8080")
694            .await
695            .unwrap();
696
697        let mut saga = SagaOrchestrator::new("test-saga".to_string(), SagaConfig::default());
698        let counter = StdArc::new(AtomicU32::new(0));
699        let counter_clone = counter.clone();
700        saga.add_step(SagaStep {
701            name: "step-1".to_string(),
702            ..Default::default()
703        });
704        saga.register_step_callbacks(
705            "step-1",
706            move || {
707                counter_clone.fetch_add(1, Ordering::SeqCst);
708                Ok(())
709            },
710            || Ok(()),
711        );
712
713        let result = store.execute_saga(saga).await.unwrap();
714        assert!(result);
715        assert_eq!(counter.load(Ordering::SeqCst), 1);
716
717        // Check replication manager received the saga record
718        let rep_stats = store.replication_stats();
719        assert!(rep_stats.is_some());
720        let rs = rep_stats.unwrap();
721        assert_eq!(rs.total_changes, 1);
722    }
723}