Skip to content

Commit 041f128

Browse files
feat(analytics): implement payment reliability and latency percentile engine (#383)
- Implemented high-throughput DDSketch streaming quantile estimator with provable relative error bound alpha = 0.01 (1%) and exact mergeability - Implemented multi-status payment reliability counter (Success, Failed, TimedOut, Rejected) with SLA tracking - Implemented strict two-clock domain separation (consensus event-time vs ingestion wall-clock time) and clock skew detection - Implemented watermark tracker with configurable delay, window lifecycle states, and explicit late-event policies (DropAndRecord, SideOutput, RetroactiveUpdate) - Implemented WatermarkedAggregateStore integrating with the backend reconciliation engine with deterministic cryptographic hashing - Added Prometheus metrics registration and comprehensive integration test suite
1 parent d8bae43 commit 041f128

13 files changed

Lines changed: 2671 additions & 26 deletions

File tree

backend/Cargo.lock

Lines changed: 20 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ lazy_static = "1.4"
1717
async-trait = "0.1"
1818
chrono = "0.4"
1919
hex = "0.4"
20-
stellar-xdr = "=28.0.0"
20+
sha2 = "0.10"
21+
stellar-xdr = "=27.0.0"
2122
tracing = "0.1.44"
2223
tracing-futures = "0.2.5"
2324
opentelemetry = "0.32.0"
@@ -28,3 +29,5 @@ opentelemetry-stdout = "0.32.0"
2829
[dev-dependencies]
2930
soroban-sdk = { version = "=27.0.6", features = ["testutils"] }
3031
stellar-insights = { path = "../contracts/stellar_insights", features = ["testutils"] }
32+
tracing-subscriber = "0.3"
33+

backend/src/analytics/clock.rs

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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

Comments
 (0)