Skip to content

Commit db17ff2

Browse files
author
kenbak wan
committed
Rolling window errors and stale error message clearing
Health endpoint now shows errors from the last hour (rolling window) instead of since startup. Error message clears from the dashboard after 10 minutes of healthy operation. Admin endpoint exposes both errors_last_hour and total_errors for full visibility.
1 parent bca655e commit db17ff2

4 files changed

Lines changed: 62 additions & 26 deletions

File tree

src/api/admin.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,7 @@ pub async fn scanner_metrics(req: actix_web::HttpRequest) -> actix_web::HttpResp
848848
let m = crate::scanner::metrics::global();
849849
let snap = m.snapshot();
850850
let uptime = m.uptime_secs().await;
851+
let recent_errors = m.recent_errors().await;
851852
let last_error = m.last_error().await;
852853

853854
let mut body = serde_json::json!({
@@ -859,7 +860,8 @@ pub async fn scanner_metrics(req: actix_web::HttpRequest) -> actix_web::HttpResp
859860
"blocks_behind": snap.blocks_behind(),
860861
"payments_detected": snap.payments_detected,
861862
"mempool_txs_checked": snap.mempool_txs_checked,
862-
"scan_errors": snap.scan_errors,
863+
"errors_last_hour": recent_errors,
864+
"total_errors": snap.total_errors,
863865
"last_block_scan_ms": snap.last_block_scan_ms,
864866
"last_mempool_scan_ms": snap.last_mempool_scan_ms,
865867
});

src/api/system.rs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,19 @@ pub async fn health(
4444
let m = crate::scanner::metrics::global();
4545
let snap = m.snapshot();
4646
let scanner_status = snap.status();
47-
let scan_errors = snap.scan_errors;
47+
let recent_errors = m.recent_errors().await;
4848
let blocks_behind = snap.blocks_behind();
4949
let last_error = m.last_error().await;
5050

51-
if scanner_status == "behind" || (scan_errors > 0 && snap.last_block_height == 0) {
51+
if scanner_status == "behind" || (recent_errors > 0 && snap.last_block_height == 0) {
5252
unhealthy = true;
5353
} else if scanner_status == "catching_up" || blocks_behind > 5 {
5454
degraded = true;
5555
}
5656
let mut scanner_json = serde_json::json!({
5757
"status": scanner_status,
5858
"blocks_behind": blocks_behind,
59-
"scan_errors": scan_errors,
59+
"scan_errors": recent_errors,
6060
"last_block_height": snap.last_block_height,
6161
});
6262
if let Some((msg, ago_secs)) = last_error {
@@ -116,6 +116,11 @@ pub async fn well_known_payment(config: web::Data<crate::config::Config>) -> Htt
116116
} else {
117117
"zcash:mainnet"
118118
};
119+
let base_url = if config.is_testnet() {
120+
"https://testnet.api.cipherpay.app"
121+
} else {
122+
"https://api.cipherpay.app"
123+
};
119124
HttpResponse::Ok()
120125
.insert_header(("Access-Control-Allow-Origin", "*"))
121126
.insert_header(("Cache-Control", "public, max-age=3600"))
@@ -125,13 +130,25 @@ pub async fn well_known_payment(config: web::Data<crate::config::Config>) -> Htt
125130
"methods": ["zcash"],
126131
"currencies": ["ZEC"],
127132
"network": network,
133+
"asset": "ZEC",
134+
"scheme": "exact",
128135
"protocols": ["x402", "mpp"],
129136
"capabilities": {
130137
"sessions": true,
131138
"streaming": true,
132139
"replay_protection": true,
140+
"idempotency": true,
141+
"auto_session": true,
142+
},
143+
"facilitator": base_url,
144+
"facilitatorUrl": format!("{}/api/x402", base_url),
145+
"endpoints": {
146+
"verify": format!("{}/api/x402/v2/verify", base_url),
147+
"settle": format!("{}/api/x402/v2/settle", base_url),
148+
"supported": format!("{}/api/x402/supported", base_url),
149+
"session_prepare": format!("{}/api/sessions/prepare", base_url),
150+
"session_validate": format!("{}/api/sessions/validate", base_url),
133151
},
134-
"facilitator": "https://api.cipherpay.app",
135152
"documentation": "https://cipherpay.app/docs",
136153
}))
137154
}

src/scanner/metrics.rs

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,21 @@ use tokio::sync::RwLock;
55

66
static INSTANCE: OnceLock<ScannerMetrics> = OnceLock::new();
77

8+
const ERROR_WINDOW_SECS: u64 = 3600;
9+
const STALE_ERROR_MSG_SECS: u64 = 600;
10+
811
pub struct ScannerMetrics {
912
pub blocks_scanned: AtomicU64,
1013
pub last_block_height: AtomicU64,
1114
pub chain_tip_height: AtomicU64,
1215
pub payments_detected: AtomicU64,
1316
pub mempool_txs_checked: AtomicU64,
14-
pub scan_errors: AtomicU64,
17+
total_errors: AtomicU64,
1518
pub last_block_scan_ms: AtomicU64,
1619
pub last_mempool_scan_ms: AtomicU64,
1720
started_at: RwLock<Option<Instant>>,
1821
last_error: RwLock<Option<(String, Instant)>>,
22+
error_timestamps: RwLock<Vec<Instant>>,
1923
}
2024

2125
impl ScannerMetrics {
@@ -26,11 +30,12 @@ impl ScannerMetrics {
2630
chain_tip_height: AtomicU64::new(0),
2731
payments_detected: AtomicU64::new(0),
2832
mempool_txs_checked: AtomicU64::new(0),
29-
scan_errors: AtomicU64::new(0),
33+
total_errors: AtomicU64::new(0),
3034
last_block_scan_ms: AtomicU64::new(0),
3135
last_mempool_scan_ms: AtomicU64::new(0),
3236
started_at: RwLock::new(None),
3337
last_error: RwLock::new(None),
38+
error_timestamps: RwLock::new(Vec::new()),
3439
}
3540
}
3641

@@ -67,30 +72,42 @@ impl ScannerMetrics {
6772
}
6873

6974
pub fn record_scan_error(&self, msg: &str) {
70-
self.scan_errors.fetch_add(1, Ordering::Relaxed);
75+
self.total_errors.fetch_add(1, Ordering::Relaxed);
7176
if let Ok(mut guard) = self.last_error.try_write() {
7277
*guard = Some((msg.to_string(), Instant::now()));
7378
}
79+
if let Ok(mut ts) = self.error_timestamps.try_write() {
80+
ts.push(Instant::now());
81+
}
7482
}
7583

76-
/// Clear error state after sustained healthy operation (10 min without errors).
77-
pub async fn clear_stale_errors(&self) {
78-
const STALE_THRESHOLD_SECS: u64 = 600;
79-
let should_clear = self
80-
.last_error
81-
.read()
82-
.await
83-
.as_ref()
84-
.map_or(false, |(_, when)| when.elapsed().as_secs() > STALE_THRESHOLD_SECS);
85-
if should_clear {
86-
self.scan_errors.store(0, Ordering::Relaxed);
87-
*self.last_error.write().await = None;
88-
}
84+
/// Errors in the last hour (rolling window).
85+
pub async fn recent_errors(&self) -> u64 {
86+
let cutoff = Instant::now() - std::time::Duration::from_secs(ERROR_WINDOW_SECS);
87+
let ts = self.error_timestamps.read().await;
88+
ts.iter().filter(|t| **t > cutoff).count() as u64
89+
}
90+
91+
pub fn total_errors(&self) -> u64 {
92+
self.total_errors.load(Ordering::Relaxed)
93+
}
94+
95+
/// Evict old timestamps from the rolling window to avoid unbounded growth.
96+
pub async fn evict_old_errors(&self) {
97+
let cutoff = Instant::now() - std::time::Duration::from_secs(ERROR_WINDOW_SECS);
98+
let mut ts = self.error_timestamps.write().await;
99+
ts.retain(|t| *t > cutoff);
89100
}
90101

102+
/// Returns last error message and age — only if the error is recent (< 10 min).
91103
pub async fn last_error(&self) -> Option<(String, u64)> {
92-
self.last_error.read().await.as_ref().map(|(msg, when)| {
93-
(msg.clone(), when.elapsed().as_secs())
104+
self.last_error.read().await.as_ref().and_then(|(msg, when)| {
105+
let ago = when.elapsed().as_secs();
106+
if ago < STALE_ERROR_MSG_SECS {
107+
Some((msg.clone(), ago))
108+
} else {
109+
None
110+
}
94111
})
95112
}
96113

@@ -109,7 +126,7 @@ impl ScannerMetrics {
109126
chain_tip_height: self.chain_tip_height.load(Ordering::Relaxed),
110127
payments_detected: self.payments_detected.load(Ordering::Relaxed),
111128
mempool_txs_checked: self.mempool_txs_checked.load(Ordering::Relaxed),
112-
scan_errors: self.scan_errors.load(Ordering::Relaxed),
129+
total_errors: self.total_errors.load(Ordering::Relaxed),
113130
last_block_scan_ms: self.last_block_scan_ms.load(Ordering::Relaxed),
114131
last_mempool_scan_ms: self.last_mempool_scan_ms.load(Ordering::Relaxed),
115132
}
@@ -123,7 +140,7 @@ pub struct MetricsSnapshot {
123140
pub chain_tip_height: u64,
124141
pub payments_detected: u64,
125142
pub mempool_txs_checked: u64,
126-
pub scan_errors: u64,
143+
pub total_errors: u64,
127144
pub last_block_scan_ms: u64,
128145
pub last_mempool_scan_ms: u64,
129146
}

src/scanner/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,6 @@ pub async fn run(config: Config, pool: SqlitePool, scanner_http: reqwest::Client
188188
Ok(_) => {
189189
block_cb.record_success();
190190
metrics::global().set_last_block_scan_ms(block_start.elapsed().as_millis() as u64);
191-
metrics::global().clear_stale_errors().await;
192191
}
193192
Err(e) => {
194193
block_cb.record_failure();
@@ -213,6 +212,7 @@ pub async fn run(config: Config, pool: SqlitePool, scanner_http: reqwest::Client
213212
if evicted > 0 {
214213
tracing::debug!(evicted, remaining = set.len(), "Evicted stale seen_txids");
215214
}
215+
metrics::global().evict_old_errors().await;
216216
}
217217
});
218218

0 commit comments

Comments
 (0)