1use std::num::NonZeroU64;
4use std::num::NonZeroUsize;
5use std::sync::Arc;
6use std::sync::Mutex;
7use std::time::Duration;
8
9use async_trait::async_trait;
10use futures::channel::mpsc;
11use futures::lock::Mutex as AsyncMutex;
12use futures::FutureExt;
13use futures::StreamExt;
14use rings_core::dht::Did;
15use rings_core::measure;
16use rings_core::measure::Measure;
17use rings_core::measure::MeasureCounter;
18use rings_core::measure::PeerMeasurement;
19use rings_core::measure::PeerMeasurementPage;
20use rings_core::measure::PeerQuality;
21use rings_core::measure::PeerQualityThresholds;
22use rings_core::storage::KvStorageInterface;
23use rings_measure::ApplyOutcome;
24use rings_measure::Authentication;
25use rings_measure::CreditPolicy;
26use rings_measure::MeasureError;
27use rings_measure::MeasurementBatch;
28use rings_measure::MeasurementEvent;
29use rings_measure::MeasurementLedger;
30use rings_measure::MeasurementSnapshot;
31use rings_measure::ReliabilityPolicy;
32use rings_measure::UnixTime;
33
34const SNAPSHOT_KEY: &str = "MeasurementLedger/v1";
37const PERSISTENCE_WAKE_CAPACITY: usize = 1;
38const PERSISTENCE_SHUTDOWN_ATTEMPTS: usize = 3;
39const PRUNE_INTERVAL_SECONDS: u64 = 60 * 60;
40#[cfg(test)]
41const PERSISTENCE_MIN_INTERVAL: Duration = Duration::from_millis(50);
42#[cfg(not(test))]
45const PERSISTENCE_MIN_INTERVAL: Duration = Duration::from_secs(60);
46#[cfg(test)]
47const RELIABILITY_WINDOW: NonZeroU64 = NonZeroU64::MIN;
48#[cfg(not(test))]
49#[allow(
50 clippy::unwrap_used,
51 reason = "the non-zero integer literal is validated during const evaluation"
52)]
53const RELIABILITY_WINDOW: NonZeroU64 = NonZeroU64::new(3_600).unwrap();
54
55pub(crate) const fn peer_quality_thresholds() -> PeerQualityThresholds {
57 PeerQualityThresholds::new(
58 crate::consts::CONNECT_FAILED_LIMIT,
59 crate::consts::MSG_SEND_FAILED_LIMIT,
60 crate::consts::MSG_RECV_FAILED_LIMIT,
61 )
62}
63
64const fn reliability_policy() -> ReliabilityPolicy {
65 ReliabilityPolicy::from_nonzero_window(RELIABILITY_WINDOW, 1, peer_quality_thresholds())
66}
67
68#[cfg(all(feature = "browser", target_family = "wasm"))]
70pub type MeasureStorage = Box<dyn KvStorageInterface<MeasurementSnapshot<Did>>>;
71
72#[cfg(not(all(feature = "browser", target_family = "wasm")))]
74pub type MeasureStorage = Box<dyn KvStorageInterface<MeasurementSnapshot<Did>> + Sync + Send>;
75
76#[cfg(all(feature = "browser", target_family = "wasm"))]
77type SharedMeasureStorage = Arc<dyn KvStorageInterface<MeasurementSnapshot<Did>>>;
78#[cfg(not(all(feature = "browser", target_family = "wasm")))]
79type SharedMeasureStorage = Arc<dyn KvStorageInterface<MeasurementSnapshot<Did>> + Sync + Send>;
80
81#[derive(Debug, thiserror::Error)]
83pub enum MeasureRuntimeError {
84 #[error("measurement storage failed: {0}")]
86 Storage(#[from] rings_core::error::Error),
87 #[error("measurement model failed: {0}")]
89 Model(#[from] MeasureError),
90 #[error("measurement persistence flush timed out")]
92 FlushTimeout,
93 #[error("measurement persistence flush task stopped")]
95 FlushTaskStopped,
96 #[error("measurement timer failed: {0}")]
98 Timer(String),
99 #[cfg(not(all(feature = "browser", target_family = "wasm")))]
101 #[error("measurement persistence requires a live Tokio runtime: {0}")]
102 RuntimeUnavailable(String),
103}
104
105pub struct PeriodicMeasure {
111 state: Arc<MeasureState>,
112 persistence_wake: mpsc::Sender<()>,
113}
114
115struct MeasureState {
116 storage: SharedMeasureStorage,
117 runtime: Mutex<RuntimeLedger>,
118 persistence_lock: AsyncMutex<()>,
119 clock: Arc<dyn MeasureClock>,
120 #[cfg(not(all(feature = "browser", target_family = "wasm")))]
121 runtime_handle: tokio::runtime::Handle,
122}
123
124impl MeasureState {
125 fn runtime_at_now(&self) -> (std::sync::MutexGuard<'_, RuntimeLedger>, UnixTime) {
127 let runtime = lock_or_recover(&self.runtime);
128 let now = self.clock.now();
129 (runtime, now)
130 }
131}
132
133struct RuntimeLedger {
134 ledger: MeasurementLedger<Did>,
135 dirty: bool,
136 persisting: bool,
137 mutated_while_persisting: bool,
138 last_clock: UnixTime,
139 next_prune_at: UnixTime,
140}
141
142trait MeasureClock: Send + Sync {
144 fn now(&self) -> UnixTime;
145}
146
147struct SystemMeasureClock;
148
149impl MeasureClock for SystemMeasureClock {
150 #[cfg(not(all(feature = "browser", target_family = "wasm")))]
151 fn now(&self) -> UnixTime {
152 let seconds = std::time::SystemTime::now()
153 .duration_since(std::time::UNIX_EPOCH)
154 .map(|duration| duration.as_secs())
155 .unwrap_or(0);
156 UnixTime::from_secs(seconds)
157 }
158
159 #[cfg(all(feature = "browser", target_family = "wasm"))]
160 fn now(&self) -> UnixTime {
161 let milliseconds = js_sys::Date::now();
162 if !milliseconds.is_finite() || milliseconds <= 0.0 {
163 return UnixTime::EPOCH;
164 }
165 UnixTime::from_secs((milliseconds / 1_000.0) as u64)
166 }
167}
168
169impl PeriodicMeasure {
170 pub async fn new(storage: MeasureStorage) -> Result<Self, MeasureRuntimeError> {
176 Self::new_with_clock(storage, Arc::new(SystemMeasureClock)).await
177 }
178
179 async fn new_with_clock(
180 storage: MeasureStorage,
181 clock: Arc<dyn MeasureClock>,
182 ) -> Result<Self, MeasureRuntimeError> {
183 #[cfg(not(all(feature = "browser", target_family = "wasm")))]
184 let runtime_handle = tokio::runtime::Handle::try_current()
185 .map_err(|error| MeasureRuntimeError::RuntimeUnavailable(error.to_string()))?;
186 let storage = SharedMeasureStorage::from(storage);
187 let mut ledger = match storage.get(SNAPSHOT_KEY).await? {
188 Some(snapshot) => MeasurementLedger::from_snapshot(snapshot)?,
189 None => MeasurementLedger::new(),
190 };
191 let now = clock.now();
192 let reconciliation = ledger.reconcile_runtime(now, reliability_policy());
193 if reconciliation.is_adjusted() {
194 tracing::warn!(
195 clock_adjusted_records = reconciliation.clock_adjusted_records(),
196 reliability_reset_records = reconciliation.reliability_reset_records(),
197 "reconciled measurement state during startup"
198 );
199 }
200 let pruning = ledger.prune(now, CreditPolicy::amule());
201 log_prune_failures(&pruning);
202 let dirty = reconciliation.is_adjusted() || pruning.removed_count() > 0;
203 let next_prune_at = next_prune_time(&ledger, now);
204 let state = Arc::new(MeasureState {
205 storage,
206 runtime: Mutex::new(RuntimeLedger {
207 ledger,
208 dirty,
209 persisting: false,
210 mutated_while_persisting: false,
211 last_clock: now,
212 next_prune_at,
213 }),
214 persistence_lock: AsyncMutex::new(()),
215 clock,
216 #[cfg(not(all(feature = "browser", target_family = "wasm")))]
217 runtime_handle,
218 });
219 let (mut persistence_wake, receiver) = mpsc::channel(PERSISTENCE_WAKE_CAPACITY);
220 spawn_persistence_worker(state.clone(), receiver);
221 if dirty {
222 let _ = persistence_wake.try_send(());
223 }
224 Ok(Self {
225 state,
226 persistence_wake,
227 })
228 }
229
230 pub async fn flush(&self) -> Result<(), MeasureRuntimeError> {
235 let (sender, flush) = futures::channel::oneshot::channel();
236 spawn_bounded_flush(self.state.clone(), sender);
237 match flush.await {
238 Ok(result) => result,
239 Err(_) => Err(MeasureRuntimeError::FlushTaskStopped),
240 }
241 }
242
243 pub async fn flush_with_timeout(&self, timeout: Duration) -> Result<(), MeasureRuntimeError> {
248 let (sender, flush) = futures::channel::oneshot::channel();
249 spawn_bounded_flush(self.state.clone(), sender);
250 let flush = flush.fuse();
251 let deadline = measurement_delay(timeout).fuse();
252 futures::pin_mut!(flush, deadline);
253 futures::select! {
254 result = flush => match result {
255 Ok(result) => result,
256 Err(_) => Err(MeasureRuntimeError::FlushTaskStopped),
257 },
258 deadline = deadline => match deadline {
259 Ok(()) => Err(MeasureRuntimeError::FlushTimeout),
260 Err(error) => Err(error),
261 },
262 }
263 }
264
265 fn count(&self, did: Did, counter: MeasureCounter) -> u64 {
266 let (measurement, reconciled) = {
267 let (mut runtime, now) = self.state.runtime_at_now();
268 let reconciled = match maintain_runtime(&mut runtime, now) {
269 Ok(reconciled) => reconciled,
270 Err(error) => {
271 tracing::error!(peer = %did, %error, "failed to maintain measurement state");
272 return 0;
273 }
274 };
275 let measurement =
276 runtime
277 .ledger
278 .measurement(&did, now, CreditPolicy::amule(), reliability_policy());
279 (measurement, reconciled)
280 };
281 if reconciled {
282 self.wake_persistence();
283 }
284 let measurement = match measurement {
285 Ok(Some(measurement)) => measurement,
286 Ok(None) => return 0,
287 Err(error) => {
288 tracing::warn!(peer = %did, %error, "failed to project measurement counter");
289 return 0;
290 }
291 };
292 let evidence = measurement.reliability;
293 match counter {
294 MeasureCounter::Sent => evidence.sent,
295 MeasureCounter::FailedToSend => evidence.failed_to_send,
296 MeasureCounter::Received => evidence.received,
297 MeasureCounter::FailedToReceive => evidence.failed_to_receive,
298 MeasureCounter::Connect => evidence.connected,
299 MeasureCounter::Disconnected => evidence.disconnected,
300 }
301 }
302
303 fn wake_persistence(&self) {
304 let mut sender = self.persistence_wake.clone();
305 match sender.try_send(()) {
306 Ok(()) => {}
307 Err(error) if error.is_full() => {}
308 Err(error) => tracing::error!(%error, "measurement persistence worker stopped"),
309 }
310 }
311}
312
313async fn flush_state(state: &MeasureState) -> Result<(), MeasureRuntimeError> {
314 let _guard = state.persistence_lock.lock().await;
315 let snapshot = {
316 let mut runtime = lock_or_recover(&state.runtime);
317 prepare_snapshot(&mut runtime)
318 };
319 let result = state.storage.put(SNAPSHOT_KEY, &snapshot).await;
320 finish_persist(&mut lock_or_recover(&state.runtime), result.is_ok());
321 result.map_err(MeasureRuntimeError::from)
322}
323
324type FlushSender = futures::channel::oneshot::Sender<Result<(), MeasureRuntimeError>>;
325
326#[cfg(not(all(feature = "browser", target_family = "wasm")))]
327fn spawn_bounded_flush(state: Arc<MeasureState>, sender: FlushSender) {
328 let runtime_handle = state.runtime_handle.clone();
329 runtime_handle.spawn(async move {
330 let _ = sender.send(flush_state(&state).await);
331 });
332}
333
334#[cfg(not(all(feature = "browser", target_family = "wasm")))]
335async fn measurement_delay(duration: Duration) -> Result<(), MeasureRuntimeError> {
336 futures_timer::Delay::new(duration).await;
337 Ok(())
338}
339
340#[cfg(all(feature = "browser", target_family = "wasm"))]
341async fn measurement_delay(duration: Duration) -> Result<(), MeasureRuntimeError> {
342 let millis = i32::try_from(duration.as_millis()).unwrap_or(i32::MAX);
343 rings_core::utils::js_utils::window_sleep(millis)
344 .await
345 .map_err(|error| MeasureRuntimeError::Timer(format!("{error:?}")))?;
346 Ok(())
347}
348
349#[cfg(all(feature = "browser", target_family = "wasm"))]
350fn spawn_bounded_flush(state: Arc<MeasureState>, sender: FlushSender) {
351 wasm_bindgen_futures::spawn_local(async move {
352 let _ = sender.send(flush_state(&state).await);
353 });
354}
355
356fn next_prune_time(ledger: &MeasurementLedger<Did>, now: UnixTime) -> UnixTime {
357 ledger
358 .next_retention_boundary(CreditPolicy::amule())
359 .unwrap_or_else(|| {
360 UnixTime::from_secs(now.as_secs().saturating_add(PRUNE_INTERVAL_SECONDS))
361 })
362}
363
364fn reconcile_runtime_clock(
365 runtime: &mut RuntimeLedger,
366 now: UnixTime,
367) -> Result<bool, MeasureError> {
368 if now >= runtime.last_clock {
369 runtime.last_clock = now;
370 return Ok(false);
371 }
372 runtime.last_clock = now;
373 let reconciliation = runtime.ledger.reconcile_runtime(now, reliability_policy());
374 runtime.next_prune_at = next_prune_time(&runtime.ledger, now);
375 if !reconciliation.is_adjusted() {
376 return Ok(false);
377 }
378 mark_runtime_dirty(runtime);
379 tracing::warn!(
380 clock_adjusted_records = reconciliation.clock_adjusted_records(),
381 reliability_reset_records = reconciliation.reliability_reset_records(),
382 "reconciled measurement state after wall-clock regression"
383 );
384 Ok(true)
385}
386
387fn maintain_runtime(runtime: &mut RuntimeLedger, now: UnixTime) -> Result<bool, MeasureError> {
388 let mut persistence_required = reconcile_runtime_clock(runtime, now)?;
389 if now < runtime.next_prune_at {
390 return Ok(persistence_required);
391 }
392
393 let pruning = runtime.ledger.prune(now, CreditPolicy::amule());
394 log_prune_failures(&pruning);
395 runtime.next_prune_at = next_prune_time(&runtime.ledger, now);
396 if pruning.removed_count() > 0 {
397 mark_runtime_dirty(runtime);
398 persistence_required = true;
399 }
400 Ok(persistence_required)
401}
402
403fn mark_runtime_dirty(runtime: &mut RuntimeLedger) {
404 runtime.dirty = true;
405 if runtime.persisting {
406 runtime.mutated_while_persisting = true;
407 }
408}
409
410fn prepare_snapshot(runtime: &mut RuntimeLedger) -> MeasurementSnapshot<Did> {
411 runtime.persisting = true;
412 runtime.mutated_while_persisting = false;
413 runtime.ledger.snapshot()
414}
415
416fn finish_persist(runtime: &mut RuntimeLedger, succeeded: bool) {
417 runtime.persisting = false;
418 if succeeded && !runtime.mutated_while_persisting {
419 runtime.dirty = false;
420 }
421 runtime.mutated_while_persisting = false;
422}
423
424fn log_prune_failures(report: &rings_measure::PruneReport<Did>) {
425 for failure in report.failures() {
426 tracing::warn!(
427 peer = %failure.peer(),
428 error = %failure.error(),
429 "retained peer measurement that could not be pruned"
430 );
431 }
432}
433
434fn lock_or_recover<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
435 mutex
436 .lock()
437 .unwrap_or_else(std::sync::PoisonError::into_inner)
438}
439
440#[cfg(not(all(feature = "browser", target_family = "wasm")))]
441fn spawn_persistence_worker(state: Arc<MeasureState>, receiver: mpsc::Receiver<()>) {
442 let runtime_handle = state.runtime_handle.clone();
443 runtime_handle.spawn(run_persistence_worker(state, receiver));
444}
445
446#[cfg(all(feature = "browser", target_family = "wasm"))]
447fn spawn_persistence_worker(state: Arc<MeasureState>, receiver: mpsc::Receiver<()>) {
448 wasm_bindgen_futures::spawn_local(run_persistence_worker(state, receiver));
449}
450
451async fn run_persistence_worker(state: Arc<MeasureState>, mut receiver: mpsc::Receiver<()>) {
452 let mut retrying = false;
453 loop {
454 let should_attempt = if retrying {
455 wait_for_retry_or_close(&mut receiver).await
456 } else {
457 match receiver.next().await {
458 Some(()) => wait_for_debounce_or_close(&mut receiver).await,
459 None => false,
460 }
461 };
462 if !should_attempt {
463 break;
464 }
465 retrying = match persist_pending_once(&state).await {
466 Ok(()) => false,
467 Err(error) => {
468 tracing::error!(%error, "failed to persist measurement snapshot; retrying");
469 true
470 }
471 };
472 }
473 persist_final_with_retries(&state).await;
474}
475
476async fn wait_for_debounce_or_close(receiver: &mut mpsc::Receiver<()>) -> bool {
477 wait_for_debounce_or_close_with_delay(receiver, measurement_delay(PERSISTENCE_MIN_INTERVAL))
478 .await
479}
480
481async fn wait_for_debounce_or_close_with_delay(
482 receiver: &mut mpsc::Receiver<()>,
483 delay: impl std::future::Future<Output = Result<(), MeasureRuntimeError>>,
484) -> bool {
485 let delay = delay.fuse();
486 futures::pin_mut!(delay);
487 loop {
488 let signal = receiver.next().fuse();
489 futures::pin_mut!(signal);
490 futures::select! {
491 result = delay => {
492 log_persistence_delay_error(result);
493 return true;
494 }
495 signal = signal => match signal {
496 Some(()) => {}
497 None => return false,
498 }
499 }
500 }
501}
502
503async fn wait_for_retry_or_close(receiver: &mut mpsc::Receiver<()>) -> bool {
504 wait_for_retry_or_close_with_delay(receiver, measurement_delay(PERSISTENCE_MIN_INTERVAL)).await
505}
506
507async fn wait_for_retry_or_close_with_delay(
508 receiver: &mut mpsc::Receiver<()>,
509 delay: impl std::future::Future<Output = Result<(), MeasureRuntimeError>>,
510) -> bool {
511 let delay = delay.fuse();
512 futures::pin_mut!(delay);
513 let mut wake_observed = false;
514 loop {
515 let signal = receiver.next().fuse();
516 futures::pin_mut!(signal);
517 futures::select! {
518 result = delay => {
519 match result {
520 Ok(()) => return true,
521 Err(error) => {
522 tracing::error!(%error, "failed to schedule measurement persistence retry");
523 if wake_observed {
524 return true;
525 }
526 return receiver.next().await.is_some();
529 }
530 }
531 }
532 signal = signal => match signal {
533 Some(()) => wake_observed = true,
534 None => return false,
535 }
536 }
537 }
538}
539
540async fn persist_final_with_retries(state: &MeasureState) {
541 for attempt in 1..=PERSISTENCE_SHUTDOWN_ATTEMPTS {
545 match persist_pending_once(state).await {
546 Ok(()) => return,
547 Err(error) => {
548 tracing::error!(
549 %error,
550 attempt,
551 max_attempts = PERSISTENCE_SHUTDOWN_ATTEMPTS,
552 "failed to persist final measurement snapshot"
553 );
554 }
555 }
556 }
557}
558
559fn log_persistence_delay_error(result: Result<(), MeasureRuntimeError>) {
560 if let Err(error) = result {
561 tracing::error!(%error, "failed to debounce measurement persistence");
562 }
563}
564
565async fn persist_pending_once(state: &MeasureState) -> Result<(), MeasureRuntimeError> {
566 let _guard = state.persistence_lock.lock().await;
567 let snapshot = {
568 let mut runtime = lock_or_recover(&state.runtime);
569 if !runtime.dirty {
570 return Ok(());
571 }
572 prepare_snapshot(&mut runtime)
573 };
574 let result = state.storage.put(SNAPSHOT_KEY, &snapshot).await;
575 finish_persist(&mut lock_or_recover(&state.runtime), result.is_ok());
576 result.map_err(MeasureRuntimeError::from)
577}
578
579#[cfg_attr(feature = "node", async_trait)]
580#[cfg_attr(all(feature = "browser", target_family = "wasm"), async_trait(?Send))]
581impl Measure for PeriodicMeasure {
582 async fn incr(&self, did: Did, counter: MeasureCounter) {
583 if let Err(error) = self
584 .record(did, Authentication::Authenticated, counter.into_event())
585 .await
586 {
587 tracing::error!(peer = %did, %error, "failed to apply compatibility measurement");
588 }
589 }
590
591 async fn get_count(&self, did: Did, counter: MeasureCounter) -> u64 {
592 self.count(did, counter)
593 }
594
595 async fn record(
596 &self,
597 did: Did,
598 authentication: Authentication,
599 event: MeasurementEvent,
600 ) -> Result<ApplyOutcome, MeasureError> {
601 self.record_batch(did, authentication, MeasurementBatch::single(event))
602 .await
603 }
604
605 async fn record_batch(
606 &self,
607 did: Did,
608 authentication: Authentication,
609 batch: MeasurementBatch,
610 ) -> Result<ApplyOutcome, MeasureError> {
611 let (outcome, persistence_required) = {
612 let (mut runtime, now) = self.state.runtime_at_now();
613 let persistence_required = maintain_runtime(&mut runtime, now)?;
614 let outcome = runtime
615 .ledger
616 .apply_batch(did, authentication, batch, now, reliability_policy())
617 .map(|report| {
618 if let Some(evicted_peer) = report.evicted_peer() {
619 tracing::warn!(
620 ?evicted_peer,
621 replacement_peer = ?did,
622 "measurement ledger evicted its stalest authenticated peer"
623 );
624 }
625 report.outcome()
626 });
627 let applied = matches!(outcome, Ok(ApplyOutcome::Applied));
628 if applied {
629 mark_runtime_dirty(&mut runtime);
630 }
631 (outcome, persistence_required || applied)
632 };
633 if persistence_required {
634 self.wake_persistence();
635 }
636 outcome
637 }
638
639 async fn peer_measurement(&self, did: Did) -> Result<Option<PeerMeasurement>, MeasureError> {
640 let (projected, reconciled) = {
641 let (mut runtime, now) = self.state.runtime_at_now();
642 let reconciled = maintain_runtime(&mut runtime, now)?;
643 let projected =
644 runtime
645 .ledger
646 .measurement(&did, now, CreditPolicy::amule(), reliability_policy());
647 (projected, reconciled)
648 };
649 if reconciled {
650 self.wake_persistence();
651 }
652 Ok(projected?.map(PeerMeasurement::from_projected))
653 }
654
655 async fn peer_measurements(&self) -> Result<Vec<PeerMeasurement>, MeasureError> {
656 let (projection, reconciled) = {
657 let (mut runtime, now) = self.state.runtime_at_now();
658 let reconciled = maintain_runtime(&mut runtime, now)?;
659 let projection =
660 runtime
661 .ledger
662 .measurements(now, CreditPolicy::amule(), reliability_policy());
663 (projection, reconciled)
664 };
665 if reconciled {
666 self.wake_persistence();
667 }
668 let (measurements, failures) = projection.into_parts();
669 for failure in failures {
670 tracing::warn!(
671 peer = %failure.peer(),
672 error = %failure.error(),
673 "omitted invalid peer from measurement projection"
674 );
675 }
676 Ok(measurements
677 .into_iter()
678 .map(PeerMeasurement::from_projected)
679 .collect())
680 }
681
682 async fn peer_measurements_page(
683 &self,
684 after: Option<Did>,
685 limit: NonZeroUsize,
686 ) -> Result<PeerMeasurementPage, MeasureError> {
687 let (page, reconciled) = {
688 let (mut runtime, now) = self.state.runtime_at_now();
689 let reconciled = maintain_runtime(&mut runtime, now)?;
690 let page = runtime.ledger.measurements_page(
691 after.as_ref(),
692 limit,
693 now,
694 CreditPolicy::amule(),
695 reliability_policy(),
696 );
697 (page, reconciled)
698 };
699 if reconciled {
700 self.wake_persistence();
701 }
702 let (measurements, failures, next_cursor) = page.into_parts();
703 for failure in failures {
704 tracing::warn!(
705 peer = %failure.peer(),
706 error = %failure.error(),
707 "omitted invalid peer from bounded measurement projection"
708 );
709 }
710 Ok(PeerMeasurementPage {
711 measurements: measurements
712 .into_iter()
713 .map(PeerMeasurement::from_projected)
714 .collect(),
715 next_cursor,
716 })
717 }
718}
719
720#[cfg_attr(feature = "node", async_trait)]
721#[cfg_attr(all(feature = "browser", target_family = "wasm"), async_trait(?Send))]
722impl measure::BehaviourJudgement for PeriodicMeasure {
723 async fn quality(&self, did: Did) -> PeerQuality {
724 match self.peer_measurement(did).await {
725 Ok(Some(measurement)) => measurement.quality,
726 Ok(None) => PeerQuality::Unknown,
727 Err(error) => {
728 tracing::error!(peer = %did, %error, "failed to project peer reliability");
729 PeerQuality::Unknown
730 }
731 }
732 }
733}
734
735#[cfg(test)]
736#[cfg(feature = "node")]
737#[allow(clippy::panic)]
738mod authentication_tests;
739#[cfg(test)]
740#[cfg(feature = "node")]
741#[allow(clippy::panic)]
742mod tests;
743#[cfg(test)]
744#[cfg(feature = "node")]
745#[allow(clippy::panic)]
746mod worker_tests;