1use std::{
40 collections::HashSet,
41 sync::{Arc, Mutex, RwLock},
42 time::{Duration, Instant},
43};
44
45use stratum_core::mining_sv2::{
46 ERROR_CODE_SUBMIT_SHARES_BAD_EXTRANONCE_SIZE, ERROR_CODE_SUBMIT_SHARES_DIFFICULTY_TOO_LOW,
47 ERROR_CODE_SUBMIT_SHARES_DUPLICATE_SHARE, ERROR_CODE_SUBMIT_SHARES_INVALID_CHANNEL_ID,
48 ERROR_CODE_SUBMIT_SHARES_INVALID_JOB_ID,
49 ERROR_CODE_SUBMIT_SHARES_INVALID_NON_ROLLABLE_VERSION_BIT,
50 ERROR_CODE_SUBMIT_SHARES_INVALID_SHARE, ERROR_CODE_SUBMIT_SHARES_STALE_SHARE,
51};
52use tracing::debug;
53
54use super::{
55 client::{Sv2ClientInfo, Sv2ClientsMonitoring, Sv2ClientsSummary},
56 prometheus_metrics::PrometheusMetrics,
57 server::{ServerInfo, ServerMonitoring, ServerSummary},
58 sv1::{Sv1ClientInfo, Sv1ClientsMonitoring, Sv1ClientsSummary},
59};
60
61const SHARE_REJECTION_CODES: &[&str] = &[
68 ERROR_CODE_SUBMIT_SHARES_INVALID_CHANNEL_ID,
69 ERROR_CODE_SUBMIT_SHARES_INVALID_SHARE,
70 ERROR_CODE_SUBMIT_SHARES_STALE_SHARE,
71 ERROR_CODE_SUBMIT_SHARES_INVALID_JOB_ID,
72 ERROR_CODE_SUBMIT_SHARES_DIFFICULTY_TOO_LOW,
73 ERROR_CODE_SUBMIT_SHARES_DUPLICATE_SHARE,
74 ERROR_CODE_SUBMIT_SHARES_BAD_EXTRANONCE_SIZE,
75 ERROR_CODE_SUBMIT_SHARES_INVALID_NON_ROLLABLE_VERSION_BIT,
76];
77
78#[derive(Default)]
82struct PreviousPrometheusLabelSets {
83 server_channel_labels: HashSet<[String; 2]>,
85 server_rejected_share_labels: HashSet<[String; 3]>,
87 client_channel_labels: HashSet<[String; 3]>,
89 client_rejected_share_labels: HashSet<[String; 4]>,
92}
93
94#[derive(Debug, Clone, Default)]
99pub struct MonitoringSnapshot {
100 pub timestamp: Option<Instant>,
101 pub server_info: Option<ServerInfo>,
102 pub server_summary: Option<ServerSummary>,
103 pub sv2_clients: Option<Vec<Sv2ClientInfo>>,
104 pub sv2_clients_summary: Option<Sv2ClientsSummary>,
105 pub sv1_clients: Option<Vec<Sv1ClientInfo>>,
106 pub sv1_clients_summary: Option<Sv1ClientsSummary>,
107}
108
109pub struct SnapshotCache {
116 snapshot: RwLock<MonitoringSnapshot>,
117 refresh_interval: Duration,
118 server_source: Option<Arc<dyn ServerMonitoring + Send + Sync>>,
119 sv2_clients_source: Option<Arc<dyn Sv2ClientsMonitoring + Send + Sync>>,
120 sv1_clients_source: Option<Arc<dyn Sv1ClientsMonitoring + Send + Sync>>,
121 metrics: Option<PrometheusMetrics>,
122 previous_metrics_labels: Mutex<PreviousPrometheusLabelSets>,
123}
124
125impl Clone for SnapshotCache {
126 fn clone(&self) -> Self {
127 let current_snapshot = self.snapshot.read().unwrap().clone();
131 let previous_metrics_labels = self
138 .previous_metrics_labels
139 .lock()
140 .unwrap_or_else(|e| e.into_inner());
141 Self {
142 snapshot: RwLock::new(current_snapshot),
143 refresh_interval: self.refresh_interval,
144 server_source: self.server_source.clone(),
145 sv2_clients_source: self.sv2_clients_source.clone(),
146 sv1_clients_source: self.sv1_clients_source.clone(),
147 metrics: self.metrics.clone(),
148 previous_metrics_labels: Mutex::new(PreviousPrometheusLabelSets {
149 server_channel_labels: previous_metrics_labels.server_channel_labels.clone(),
150 server_rejected_share_labels: previous_metrics_labels
151 .server_rejected_share_labels
152 .clone(),
153 client_channel_labels: previous_metrics_labels.client_channel_labels.clone(),
154 client_rejected_share_labels: previous_metrics_labels
155 .client_rejected_share_labels
156 .clone(),
157 }),
158 }
159 }
160}
161
162impl SnapshotCache {
163 pub fn new(
171 refresh_interval: Duration,
172 server_source: Option<Arc<dyn ServerMonitoring + Send + Sync>>,
173 sv2_clients_source: Option<Arc<dyn Sv2ClientsMonitoring + Send + Sync>>,
174 ) -> Self {
175 Self {
176 snapshot: RwLock::new(MonitoringSnapshot::default()),
177 refresh_interval,
178 server_source,
179 sv2_clients_source,
180 sv1_clients_source: None,
181 metrics: None,
182 previous_metrics_labels: Mutex::new(PreviousPrometheusLabelSets::default()),
183 }
184 }
185
186 pub fn with_sv1_clients_source(
188 mut self,
189 sv1_source: Arc<dyn Sv1ClientsMonitoring + Send + Sync>,
190 ) -> Self {
191 self.sv1_clients_source = Some(sv1_source);
192 self
193 }
194
195 pub fn with_metrics(mut self, metrics: PrometheusMetrics) -> Self {
200 self.metrics = Some(metrics);
201 self
202 }
203
204 pub fn get_snapshot(&self) -> MonitoringSnapshot {
209 self.snapshot.read().unwrap().clone()
210 }
211
212 pub fn refresh(&self) {
221 let mut new_snapshot = MonitoringSnapshot {
222 timestamp: Some(Instant::now()),
223 ..Default::default()
224 };
225
226 if let Some(ref source) = self.server_source {
228 new_snapshot.server_info = Some(source.get_server());
229 new_snapshot.server_summary = Some(source.get_server_summary());
230 }
231
232 if let Some(ref source) = self.sv2_clients_source {
234 new_snapshot.sv2_clients = Some(source.get_sv2_clients());
235 new_snapshot.sv2_clients_summary = Some(source.get_sv2_clients_summary());
236 }
237
238 if let Some(ref source) = self.sv1_clients_source {
240 new_snapshot.sv1_clients = Some(source.get_sv1_clients());
241 new_snapshot.sv1_clients_summary = Some(source.get_sv1_clients_summary());
242 }
243
244 if let Some(ref metrics) = self.metrics {
246 self.update_metrics(metrics, &new_snapshot);
247 }
248
249 *self.snapshot.write().unwrap() = new_snapshot;
251 }
252
253 fn update_metrics(&self, metrics: &PrometheusMetrics, snapshot: &MonitoringSnapshot) {
256 let mut current_server_labels: HashSet<[String; 2]> = HashSet::new();
257 let mut current_server_rejected_labels: HashSet<[String; 3]> = HashSet::new();
258 let mut current_client_labels: HashSet<[String; 3]> = HashSet::new();
259 let mut current_client_rejected_labels: HashSet<[String; 4]> = HashSet::new();
260
261 if let Some(ref summary) = snapshot.server_summary {
263 if let Some(ref m) = metrics.sv2_server_channels {
264 m.with_label_values(&["extended"])
265 .set(summary.extended_channels as f64);
266 m.with_label_values(&["standard"])
267 .set(summary.standard_channels as f64);
268 }
269 if let Some(ref m) = metrics.sv2_server_hashrate_total {
270 m.set(summary.total_hashrate as f64);
271 }
272 }
273
274 if let Some(ref server) = snapshot.server_info {
275 for channel in &server.extended_channels {
276 let channel_id = channel.channel_id.to_string();
277 let user = &channel.user_identity;
278 let labels = [channel_id.clone(), user.clone()];
279
280 if let Some(ref m) = metrics.sv2_server_shares_accepted_total {
281 m.with_label_values(&[&channel_id, user])
282 .set(channel.shares_acknowledged as f64);
283 }
284 if let Some(ref m) = metrics.sv2_server_shares_rejected_total {
285 for &reason in SHARE_REJECTION_CODES {
288 m.with_label_values(&[&channel_id, user, reason]).set(0.0);
289 current_server_rejected_labels.insert([
290 channel_id.clone(),
291 user.clone(),
292 reason.to_string(),
293 ]);
294 }
295 for (error_code, count) in &channel.shares_rejected_by_reason {
296 m.with_label_values(&[&channel_id, user, error_code])
297 .set(*count as f64);
298 current_server_rejected_labels.insert([
299 channel_id.clone(),
300 user.clone(),
301 error_code.clone(),
302 ]);
303 }
304 }
305 if let (Some(m), Some(hashrate)) = (
306 &metrics.sv2_server_channel_hashrate,
307 channel.nominal_hashrate,
308 ) {
309 m.with_label_values(&[&channel_id, user])
310 .set(hashrate as f64);
311 }
312 current_server_labels.insert(labels);
313 }
314
315 for channel in &server.standard_channels {
316 let channel_id = channel.channel_id.to_string();
317 let user = &channel.user_identity;
318 let labels = [channel_id.clone(), user.clone()];
319
320 if let Some(ref m) = metrics.sv2_server_shares_accepted_total {
321 m.with_label_values(&[&channel_id, user])
322 .set(channel.shares_acknowledged as f64);
323 }
324 if let Some(ref m) = metrics.sv2_server_shares_rejected_total {
325 for &reason in SHARE_REJECTION_CODES {
328 m.with_label_values(&[&channel_id, user, reason]).set(0.0);
329 current_server_rejected_labels.insert([
330 channel_id.clone(),
331 user.clone(),
332 reason.to_string(),
333 ]);
334 }
335 for (error_code, count) in &channel.shares_rejected_by_reason {
336 m.with_label_values(&[&channel_id, user, error_code])
337 .set(*count as f64);
338 current_server_rejected_labels.insert([
339 channel_id.clone(),
340 user.clone(),
341 error_code.clone(),
342 ]);
343 }
344 }
345 if let (Some(m), Some(hashrate)) = (
346 &metrics.sv2_server_channel_hashrate,
347 channel.nominal_hashrate,
348 ) {
349 m.with_label_values(&[&channel_id, user])
350 .set(hashrate as f64);
351 }
352 current_server_labels.insert(labels);
353 }
354
355 if let Some(ref m) = metrics.sv2_server_blocks_found_total {
356 let total: u64 = server
357 .extended_channels
358 .iter()
359 .map(|c| c.blocks_found as u64)
360 .chain(
361 server
362 .standard_channels
363 .iter()
364 .map(|c| c.blocks_found as u64),
365 )
366 .sum();
367 m.set(total as f64);
368 }
369 }
370
371 if let Some(ref summary) = snapshot.sv2_clients_summary {
373 if let Some(ref m) = metrics.sv2_clients_total {
374 m.set(summary.total_clients as f64);
375 }
376 if let Some(ref m) = metrics.sv2_client_channels {
377 m.with_label_values(&["extended"])
378 .set(summary.extended_channels as f64);
379 m.with_label_values(&["standard"])
380 .set(summary.standard_channels as f64);
381 }
382 if let Some(ref m) = metrics.sv2_client_hashrate_total {
383 m.set(summary.total_hashrate as f64);
384 }
385
386 let mut client_blocks_total: u64 = 0;
387
388 for client in snapshot.sv2_clients.as_deref().unwrap_or(&[]) {
389 let client_id = client.client_id.to_string();
390
391 for channel in &client.extended_channels {
392 let channel_id = channel.channel_id.to_string();
393 let user = &channel.user_identity;
394 let labels = [client_id.clone(), channel_id.clone(), user.clone()];
395
396 if let Some(ref m) = metrics.sv2_client_shares_accepted_total {
397 m.with_label_values(&[&client_id, &channel_id, user])
398 .set(channel.shares_accepted as f64);
399 }
400 if let Some(ref m) = metrics.sv2_client_shares_rejected_total {
401 for &reason in SHARE_REJECTION_CODES {
404 m.with_label_values(&[&client_id, &channel_id, user, reason])
405 .set(0.0);
406 current_client_rejected_labels.insert([
407 client_id.clone(),
408 channel_id.clone(),
409 user.clone(),
410 reason.to_string(),
411 ]);
412 }
413 for (error_code, count) in &channel.shares_rejected_by_reason {
414 m.with_label_values(&[&client_id, &channel_id, user, error_code])
415 .set(*count as f64);
416 current_client_rejected_labels.insert([
417 client_id.clone(),
418 channel_id.clone(),
419 user.clone(),
420 error_code.clone(),
421 ]);
422 }
423 }
424 if let Some(ref m) = metrics.sv2_client_channel_hashrate {
425 m.with_label_values(&[&client_id, &channel_id, user])
426 .set(channel.nominal_hashrate as f64);
427 }
428 current_client_labels.insert(labels);
429 client_blocks_total += channel.blocks_found as u64;
430 }
431
432 for channel in &client.standard_channels {
433 let channel_id = channel.channel_id.to_string();
434 let user = &channel.user_identity;
435 let labels = [client_id.clone(), channel_id.clone(), user.clone()];
436
437 if let Some(ref m) = metrics.sv2_client_shares_accepted_total {
438 m.with_label_values(&[&client_id, &channel_id, user])
439 .set(channel.shares_accepted as f64);
440 }
441 if let Some(ref m) = metrics.sv2_client_shares_rejected_total {
442 for &reason in SHARE_REJECTION_CODES {
445 m.with_label_values(&[&client_id, &channel_id, user, reason])
446 .set(0.0);
447 current_client_rejected_labels.insert([
448 client_id.clone(),
449 channel_id.clone(),
450 user.clone(),
451 reason.to_string(),
452 ]);
453 }
454 for (error_code, count) in &channel.shares_rejected_by_reason {
455 m.with_label_values(&[&client_id, &channel_id, user, error_code])
456 .set(*count as f64);
457 current_client_rejected_labels.insert([
458 client_id.clone(),
459 channel_id.clone(),
460 user.clone(),
461 error_code.clone(),
462 ]);
463 }
464 }
465 if let Some(ref m) = metrics.sv2_client_channel_hashrate {
466 m.with_label_values(&[&client_id, &channel_id, user])
467 .set(channel.nominal_hashrate as f64);
468 }
469 current_client_labels.insert(labels);
470 client_blocks_total += channel.blocks_found as u64;
471 }
472 }
473
474 if let Some(ref m) = metrics.sv2_client_blocks_found_total {
475 m.set(client_blocks_total as f64);
476 }
477 }
478
479 if let Some(ref summary) = snapshot.sv1_clients_summary {
481 if let Some(ref m) = metrics.sv1_clients_total {
482 m.set(summary.total_clients as f64);
483 }
484 if let Some(ref m) = metrics.sv1_hashrate_total {
485 m.set(summary.total_hashrate as f64);
486 }
487 }
488
489 let mut previous_metrics_labels = self
491 .previous_metrics_labels
492 .lock()
493 .unwrap_or_else(|e| e.into_inner());
494
495 for stale in previous_metrics_labels
496 .server_channel_labels
497 .difference(¤t_server_labels)
498 {
499 let label_refs: Vec<&str> = stale.iter().map(|s| s.as_str()).collect();
500 if let Some(ref m) = metrics.sv2_server_shares_accepted_total {
501 if let Err(e) = m.remove_label_values(&label_refs) {
502 debug!(labels = ?label_refs, error = %e, "failed to remove stale server shares label");
503 }
504 }
505 if let Some(ref m) = metrics.sv2_server_channel_hashrate {
506 if let Err(e) = m.remove_label_values(&label_refs) {
507 debug!(labels = ?label_refs, error = %e, "failed to remove stale server hashrate label");
508 }
509 }
510 }
511
512 for stale in previous_metrics_labels
513 .server_rejected_share_labels
514 .difference(¤t_server_rejected_labels)
515 {
516 let label_refs: Vec<&str> = stale.iter().map(|s| s.as_str()).collect();
517 if let Some(ref m) = metrics.sv2_server_shares_rejected_total {
518 if let Err(e) = m.remove_label_values(&label_refs) {
519 debug!(labels = ?label_refs, error = %e, "failed to remove stale server rejected shares label");
520 }
521 }
522 }
523
524 for stale in previous_metrics_labels
525 .client_channel_labels
526 .difference(¤t_client_labels)
527 {
528 let label_refs: Vec<&str> = stale.iter().map(|s| s.as_str()).collect();
529 if let Some(ref m) = metrics.sv2_client_shares_accepted_total {
530 if let Err(e) = m.remove_label_values(&label_refs) {
531 debug!(labels = ?label_refs, error = %e, "failed to remove stale client shares label");
532 }
533 }
534 if let Some(ref m) = metrics.sv2_client_channel_hashrate {
535 if let Err(e) = m.remove_label_values(&label_refs) {
536 debug!(labels = ?label_refs, error = %e, "failed to remove stale client hashrate label");
537 }
538 }
539 }
540
541 for stale in previous_metrics_labels
542 .client_rejected_share_labels
543 .difference(¤t_client_rejected_labels)
544 {
545 let label_refs: Vec<&str> = stale.iter().map(|s| s.as_str()).collect();
546 if let Some(ref m) = metrics.sv2_client_shares_rejected_total {
547 if let Err(e) = m.remove_label_values(&label_refs) {
548 debug!(labels = ?label_refs, error = %e, "failed to remove stale client rejected shares label");
549 }
550 }
551 }
552
553 previous_metrics_labels.server_channel_labels = current_server_labels;
554 previous_metrics_labels.server_rejected_share_labels = current_server_rejected_labels;
555 previous_metrics_labels.client_channel_labels = current_client_labels;
556 previous_metrics_labels.client_rejected_share_labels = current_client_rejected_labels;
557 }
558
559 pub fn refresh_interval(&self) -> Duration {
561 self.refresh_interval
562 }
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568
569 struct MockServerMonitoring;
570 impl ServerMonitoring for MockServerMonitoring {
571 fn get_server(&self) -> ServerInfo {
572 ServerInfo {
573 extended_channels: vec![],
574 standard_channels: vec![],
575 }
576 }
577 }
578
579 struct MockSv2ClientsMonitoring;
580 impl Sv2ClientsMonitoring for MockSv2ClientsMonitoring {
581 fn get_sv2_clients(&self) -> Vec<Sv2ClientInfo> {
582 vec![]
583 }
584 }
585
586 #[test]
587 fn test_snapshot_cache_creation() {
588 let cache = SnapshotCache::new(
589 Duration::from_secs(5),
590 Some(Arc::new(MockServerMonitoring)),
591 Some(Arc::new(MockSv2ClientsMonitoring)),
592 );
593
594 let snapshot = cache.get_snapshot();
596 assert!(snapshot.timestamp.is_none());
597 assert_eq!(cache.refresh_interval(), Duration::from_secs(5));
598 }
599
600 #[test]
601 fn test_snapshot_refresh() {
602 let cache = SnapshotCache::new(
603 Duration::from_secs(5),
604 Some(Arc::new(MockServerMonitoring)),
605 Some(Arc::new(MockSv2ClientsMonitoring)),
606 );
607
608 let snapshot = cache.get_snapshot();
610 assert!(snapshot.timestamp.is_none());
611 assert!(snapshot.server_info.is_none());
612
613 cache.refresh();
615 let snapshot = cache.get_snapshot();
616 assert!(snapshot.timestamp.is_some());
617 assert!(snapshot.server_info.is_some());
618 assert!(snapshot.sv2_clients.is_some());
619 assert!(snapshot.sv2_clients_summary.is_some());
620 }
621
622 struct ContendedMonitoring {
627 lock_hold_duration: Duration,
628 monitoring_lock_acquisitions: std::sync::atomic::AtomicU64,
629 business_lock: std::sync::Mutex<()>,
630 }
631
632 impl ContendedMonitoring {
633 fn new(lock_hold_duration: Duration) -> Self {
634 Self {
635 lock_hold_duration,
636 monitoring_lock_acquisitions: std::sync::atomic::AtomicU64::new(0),
637 business_lock: std::sync::Mutex::new(()),
638 }
639 }
640
641 fn simulate_business_logic(&self) {
642 let _guard = self.business_lock.lock().unwrap();
643 std::thread::sleep(self.lock_hold_duration);
644 }
645
646 fn get_monitoring_acquisitions(&self) -> u64 {
647 self.monitoring_lock_acquisitions
648 .load(std::sync::atomic::Ordering::SeqCst)
649 }
650 }
651
652 impl Sv2ClientsMonitoring for ContendedMonitoring {
653 fn get_sv2_clients(&self) -> Vec<Sv2ClientInfo> {
654 let _guard = self.business_lock.lock().unwrap();
655 self.monitoring_lock_acquisitions
656 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
657 std::thread::sleep(Duration::from_micros(10));
659 vec![]
660 }
661 }
662
663 impl ServerMonitoring for ContendedMonitoring {
664 fn get_server(&self) -> ServerInfo {
665 let _guard = self.business_lock.lock().unwrap();
666 self.monitoring_lock_acquisitions
667 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
668 std::thread::sleep(Duration::from_micros(10));
670 ServerInfo {
671 extended_channels: vec![],
672 standard_channels: vec![],
673 }
674 }
675 }
676
677 #[test]
684 fn test_snapshot_cache_eliminates_lock_contention() {
685 let real_monitoring = Arc::new(ContendedMonitoring::new(Duration::from_millis(1)));
686
687 let cache = Arc::new(SnapshotCache::new(
688 Duration::from_secs(5),
689 None,
690 Some(real_monitoring.clone() as Arc<dyn Sv2ClientsMonitoring + Send + Sync>),
691 ));
692
693 cache.refresh();
694
695 let business_mon = Arc::clone(&real_monitoring);
697 let business_handle = std::thread::spawn(move || {
698 let start = std::time::Instant::now();
699 let mut ops = 0u64;
700 while start.elapsed() < Duration::from_millis(100) {
701 business_mon.simulate_business_logic();
702 ops += 1;
703 }
704 ops
705 });
706
707 let mut monitoring_handles = vec![];
709 for _ in 0..16 {
710 let cache_ref = Arc::clone(&cache);
711 monitoring_handles.push(std::thread::spawn(move || {
712 let start = std::time::Instant::now();
713 let mut requests = 0u64;
714 while start.elapsed() < Duration::from_millis(100) {
716 let _ = cache_ref.get_snapshot();
717 requests += 1;
718 }
719 requests
720 }));
721 }
722
723 let _business_ops = business_handle.join().unwrap();
724 let total_cache_requests: u64 = monitoring_handles
725 .into_iter()
726 .map(|h| h.join().unwrap())
727 .sum();
728
729 let real_lock_acquisitions = real_monitoring.get_monitoring_acquisitions();
730
731 assert!(
733 real_lock_acquisitions <= 2,
734 "Cache acquired lock {} times, expected ≤2 (refresh only)",
735 real_lock_acquisitions
736 );
737
738 assert!(
740 total_cache_requests > 2,
741 "Cache should have processed requests",
742 );
743 }
744}