Skip to main content

rings_node/
measure.rs

1//! Runtime adapter for the pure `rings-measure` state relation.
2
3use 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
34// Legacy `PeriodicMeasure/counters/...` values intentionally remain unread:
35// a bare count proves neither byte-credit direction nor a live epoch timestamp.
36const 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// The first mutation and every later coalesced snapshot intentionally wait for
43// this interval. A hard crash may lose at most one interval of advisory state.
44#[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
55/// Shared peer-quality thresholds used by measurement and route selection.
56pub(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/// Storage used for one versioned complete measurement snapshot.
69#[cfg(all(feature = "browser", target_family = "wasm"))]
70pub type MeasureStorage = Box<dyn KvStorageInterface<MeasurementSnapshot<Did>>>;
71
72/// Storage used for one versioned complete measurement snapshot.
73#[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/// Failure while loading or explicitly flushing the runtime measurement adapter.
82#[derive(Debug, thiserror::Error)]
83pub enum MeasureRuntimeError {
84    /// The configured key-value backend failed.
85    #[error("measurement storage failed: {0}")]
86    Storage(#[from] rings_core::error::Error),
87    /// Persisted or live state violated the pure measurement model.
88    #[error("measurement model failed: {0}")]
89    Model(#[from] MeasureError),
90    /// A bounded explicit flush did not complete before its deadline.
91    #[error("measurement persistence flush timed out")]
92    FlushTimeout,
93    /// The runtime stopped the owned flush task before it returned a result.
94    #[error("measurement persistence flush task stopped")]
95    FlushTaskStopped,
96    /// The browser could not schedule a measurement timer.
97    #[error("measurement timer failed: {0}")]
98    Timer(String),
99    /// Native construction was attempted without a live Tokio runtime.
100    #[cfg(not(all(feature = "browser", target_family = "wasm")))]
101    #[error("measurement persistence requires a live Tokio runtime: {0}")]
102    RuntimeUnavailable(String),
103}
104
105/// Pure-ledger runtime adapter with coalesced asynchronous snapshot persistence.
106///
107/// Network callbacks update only in-memory state and replace the pending full
108/// snapshot. A runtime task serializes storage writes. The algorithm, time
109/// projection, pruning, and snapshot schema remain in `rings-measure`.
110pub 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    /// Serialize clock sampling with the ledger transition it timestamps.
126    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
142// Boundary: the adapter supplies wall-clock seconds to the pure state relation.
143trait 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    /// Load the complete ledger once and start the coalescing persistence task.
171    ///
172    /// On native targets this captures the active Tokio runtime handle. The
173    /// constructor returns `MeasureRuntimeError::RuntimeUnavailable` instead
174    /// of panicking when called outside a live runtime.
175    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    /// Persist a snapshot containing every update visible when this method starts.
231    ///
232    /// On native targets, the Tokio runtime captured by [`Self::new`] must
233    /// remain alive until the returned future completes.
234    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    /// Persist all applied updates unless the supplied deadline expires first.
244    ///
245    /// On native targets, the Tokio runtime captured by [`Self::new`] must
246    /// remain alive until the returned future completes or reaches `timeout`.
247    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                        // A broken browser timer cannot provide bounded autonomous retry. Wait for
527                        // a later semantic mutation instead of spinning the JS microtask queue.
528                        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    // Shutdown retries are deliberately back-to-back: this path must remain
542    // bounded and can recover immediate backend races, but must not add another
543    // timer dependency while the owning runtime is stopping.
544    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;