|
| 1 | +//! Two clocks, one truth: Clock reconciliation and Latency Clock Basis. |
| 2 | +//! |
| 3 | +//! # Clock Architecture |
| 4 | +//! |
| 5 | +//! Stellar payment analytics fundamentally operates across two distinct clock domains: |
| 6 | +//! |
| 7 | +//! 1. **Event-Time Domain ($T_{\text{ledger}}$)**: |
| 8 | +//! - The authoritative, deterministic consensus timestamp recorded in the Stellar ledger header. |
| 9 | +//! - **Role**: All time-windowing, watermarking, historical replays, and reliability/SLA metrics |
| 10 | +//! MUST strictly use $T_{\text{ledger}}$ as the time basis. This guarantees that analytics results |
| 11 | +//! are 100% deterministic and reproducible across node restarts, backlog re-indexing, and shard merges. |
| 12 | +//! |
| 13 | +//! 2. **Processing-Time Domain ($T_{\text{ingest}}$)**: |
| 14 | +//! - Local system wall-clock time when an event is received and processed by the indexer. |
| 15 | +//! - **Role**: Used exclusively for pipeline health observability, ingestion lag monitoring |
| 16 | +//! ($T_{\text{ingest}} - T_{\text{ledger}}$), and detecting upstream RPC replication backpressure. |
| 17 | +//! |
| 18 | +//! # Latency Clock Basis |
| 19 | +//! |
| 20 | +//! Cross-border payment latency is categorized into three explicit metrics: |
| 21 | +//! |
| 22 | +//! - **Settlement Latency ($L_{\text{settle}}$)**: |
| 23 | +//! $$L_{\text{settle}} = T_{\text{ledger}} - T_{\text{client\_submitted}}$$ |
| 24 | +//! Measures the true on-chain settlement duration experienced by users. If $T_{\text{client\_submitted}}$ |
| 25 | +//! is unavailable, the event's recorded transaction execution latency or ledger interval is used. |
| 26 | +//! |
| 27 | +//! - **Ingestion Lag ($L_{\text{ingest}}$)**: |
| 28 | +//! $$L_{\text{ingest}} = T_{\text{ingest}} - T_{\text{ledger}}$$ |
| 29 | +//! Measures indexer delay and Horizon/RPC propagation lag. |
| 30 | +//! |
| 31 | +//! - **End-to-End Latency ($L_{\text{e2e}}$)**: |
| 32 | +//! $$L_{\text{e2e}} = T_{\text{ingest}} - T_{\text{client\_submitted}}$$ |
| 33 | +//! Total latency from client submission to backend indexing. |
| 34 | +//! |
| 35 | +//! # Clock Skew Handling |
| 36 | +//! |
| 37 | +//! When event timestamps arrive in the future relative to wall-clock time ($T_{\text{ledger}} > T_{\text{ingest}} + \Delta_{\text{skew}}$), |
| 38 | +//! a clock skew incident is flagged and recorded without dropping data or halting pipeline execution. |
| 39 | +
|
| 40 | +use crate::analytics::reliability::{PaymentCorridor, PaymentStatus}; |
| 41 | +use serde::{Deserialize, Serialize}; |
| 42 | + |
| 43 | +/// Maximum tolerable clock skew before generating an alert (60 seconds). |
| 44 | +pub const DEFAULT_MAX_CLOCK_SKEW_SECS: u64 = 60; |
| 45 | + |
| 46 | +/// Default SLA latency threshold (5,000 milliseconds / 5 seconds). |
| 47 | +pub const DEFAULT_SLA_THRESHOLD_MS: f64 = 5_000.0; |
| 48 | + |
| 49 | +/// Clock domain selector for queries. |
| 50 | +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
| 51 | +pub enum ClockDomain { |
| 52 | + /// Ledger consensus timestamp (Event time). |
| 53 | + EventTime, |
| 54 | + /// Host wall-clock timestamp (Processing time). |
| 55 | + ProcessingTime, |
| 56 | +} |
| 57 | + |
| 58 | +/// Payment event ingested into the analytics engine. |
| 59 | +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] |
| 60 | +pub struct PaymentEvent { |
| 61 | + /// Unique payment or transaction hash. |
| 62 | + pub payment_id: String, |
| 63 | + /// Stellar ledger sequence number. |
| 64 | + pub ledger_sequence: u64, |
| 65 | + /// Deterministic ledger close time (Unix timestamp in seconds). |
| 66 | + pub ledger_closed_at: u64, |
| 67 | + /// Client submission time (Unix timestamp in seconds), if available. |
| 68 | + pub client_submitted_at: Option<u64>, |
| 69 | + /// Indexer wall-clock ingestion time (Unix timestamp in seconds). |
| 70 | + pub ingested_at: u64, |
| 71 | + /// Payment outcome. |
| 72 | + pub status: PaymentStatus, |
| 73 | + /// Optional payment corridor (asset pair). |
| 74 | + pub corridor: Option<PaymentCorridor>, |
| 75 | + /// Explicitly measured execution/settlement latency in milliseconds. |
| 76 | + pub latency_ms: Option<f64>, |
| 77 | +} |
| 78 | + |
| 79 | +impl PaymentEvent { |
| 80 | + /// Returns the authoritative event timestamp in seconds ($T_{\text{ledger}}$). |
| 81 | + pub fn event_time(&self) -> u64 { |
| 82 | + self.ledger_closed_at |
| 83 | + } |
| 84 | + |
| 85 | + /// Computes or retrieves the settlement latency in milliseconds. |
| 86 | + pub fn effective_latency_ms(&self, default_sla_threshold: f64) -> f64 { |
| 87 | + if let Some(lat) = self.latency_ms { |
| 88 | + return lat.max(0.0); |
| 89 | + } |
| 90 | + |
| 91 | + if let Some(submitted) = self.client_submitted_at { |
| 92 | + if self.ledger_closed_at >= submitted { |
| 93 | + let diff_secs = self.ledger_closed_at - submitted; |
| 94 | + return (diff_secs as f64) * 1000.0; |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + // If timed out or failed without explicit latency, use default SLA threshold |
| 99 | + if self.status == PaymentStatus::TimedOut { |
| 100 | + default_sla_threshold * 1.5 |
| 101 | + } else { |
| 102 | + 0.0 |
| 103 | + } |
| 104 | + } |
| 105 | + |
| 106 | + /// Computes the ingestion lag ($T_{\text{ingest}} - T_{\text{ledger}}$) in seconds. |
| 107 | + pub fn ingestion_lag_secs(&self) -> i64 { |
| 108 | + (self.ingested_at as i64) - (self.ledger_closed_at as i64) |
| 109 | + } |
| 110 | + |
| 111 | + /// Determines if there is significant clock skew (ledger time > ingest time + max skew). |
| 112 | + pub fn is_clock_skewed(&self, max_skew_secs: u64) -> bool { |
| 113 | + self.ledger_closed_at > self.ingested_at + max_skew_secs |
| 114 | + } |
| 115 | + |
| 116 | + /// Checks if this payment breached the specified latency SLA threshold. |
| 117 | + pub fn is_sla_breached(&self, sla_threshold_ms: f64) -> bool { |
| 118 | + if self.status == PaymentStatus::TimedOut { |
| 119 | + return true; |
| 120 | + } |
| 121 | + self.effective_latency_ms(sla_threshold_ms) > sla_threshold_ms |
| 122 | + } |
| 123 | +} |
| 124 | + |
| 125 | +#[cfg(test)] |
| 126 | +mod tests { |
| 127 | + use super::*; |
| 128 | + |
| 129 | + #[test] |
| 130 | + fn test_payment_event_latency_calculation() { |
| 131 | + let event = PaymentEvent { |
| 132 | + payment_id: "tx_123".into(), |
| 133 | + ledger_sequence: 100, |
| 134 | + ledger_closed_at: 1700000005, |
| 135 | + client_submitted_at: Some(1700000000), |
| 136 | + ingested_at: 1700000007, |
| 137 | + status: PaymentStatus::Success, |
| 138 | + corridor: None, |
| 139 | + latency_ms: None, |
| 140 | + }; |
| 141 | + |
| 142 | + assert_eq!(event.event_time(), 1700000005); |
| 143 | + assert_eq!(event.effective_latency_ms(5000.0), 5000.0); |
| 144 | + assert_eq!(event.ingestion_lag_secs(), 2); |
| 145 | + assert!(!event.is_clock_skewed(60)); |
| 146 | + assert!(!event.is_sla_breached(5000.0)); |
| 147 | + } |
| 148 | + |
| 149 | + #[test] |
| 150 | + fn test_clock_skew_detection() { |
| 151 | + let skewed_event = PaymentEvent { |
| 152 | + payment_id: "tx_skew".into(), |
| 153 | + ledger_sequence: 100, |
| 154 | + ledger_closed_at: 1700000200, // 200s in the future |
| 155 | + client_submitted_at: None, |
| 156 | + ingested_at: 1700000000, |
| 157 | + status: PaymentStatus::Success, |
| 158 | + corridor: None, |
| 159 | + latency_ms: Some(150.0), |
| 160 | + }; |
| 161 | + |
| 162 | + assert!(skewed_event.is_clock_skewed(60)); |
| 163 | + assert_eq!(skewed_event.effective_latency_ms(5000.0), 150.0); |
| 164 | + } |
| 165 | +} |
0 commit comments