ethl 0.1.42

Tools for capturing, processing, archiving, and replaying Ethereum events
Documentation
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};

#[derive(Debug, Default)]
struct ScanProgressInner {
    scanned_to: AtomicU64,
    observed_tip: AtomicU64,
}

/// Where the stream has actually scanned to, and the tip ethl last observed, readable by the
/// consumer at any time.
///
/// ethl tracks both internally already; this hands out a cloneable read view so a consumer isn't
/// forced to reimplement cursor tracking from the stream's own yields, or (for tip) issue its own
/// redundant RPC call moments after ethl already made one for its own loop. Hand a clone to a
/// metrics task and read it on whatever schedule that task wants — ethl deliberately does not log
/// or emit on the consumer's behalf.
#[derive(Clone, Debug, Default)]
pub struct ScanProgress(Arc<ScanProgressInner>);

impl ScanProgress {
    /// Highest block the stream has actually scanned, or `None` if nothing has been observed yet.
    ///
    /// `0` is the unset sentinel: block 0 is never scanned (`backfill.rs`'s `.max(1)` clamp), and
    /// no supported chain's real tip is ever block 0 while ethl is running.
    pub fn scanned_to(&self) -> Option<u64> {
        match self.0.scanned_to.load(Ordering::Relaxed) {
            0 => None,
            block => Some(block),
        }
    }

    /// The tip ethl last observed while scanning, or `None` if no tip has been observed yet.
    ///
    /// Best-effort: populated only at the sites that already fetch a tip for their own loop, not
    /// on a schedule of its own. See the sites in `stream.rs`/`heads.rs` for which paths populate
    /// it.
    pub fn observed_tip(&self) -> Option<u64> {
        match self.0.observed_tip.load(Ordering::Relaxed) {
            0 => None,
            block => Some(block),
        }
    }

    /// Record a newly scanned block. `fetch_max` (not `store`) so an out-of-order or retried
    /// update can never move progress backwards — monotonicity is the property a consumer relies
    /// on, and it's exactly what a `0`-initialized `fetch_max` gives you.
    ///
    /// `Ordering::Relaxed` is sufficient: these are advisory counters with no happens-before
    /// contract to anything else. Do not "fix" this to `SeqCst`.
    pub(crate) fn record_scanned_to(&self, block: u64) {
        self.0.scanned_to.fetch_max(block, Ordering::Relaxed);
    }

    /// Record a newly observed chain tip. Same `fetch_max`/`Relaxed` reasoning as
    /// `record_scanned_to`.
    pub(crate) fn record_observed_tip(&self, block: u64) {
        self.0.observed_tip.fetch_max(block, Ordering::Relaxed);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// G.2.2 §4.2 — unset: both fields read `None` before any observation is recorded.
    #[test]
    fn unset_before_any_observation() {
        let progress = ScanProgress::default();
        assert_eq!(progress.scanned_to(), None);
        assert_eq!(progress.observed_tip(), None);
    }

    /// G.2.2 §4.2 — monotonicity: `fetch_max` semantics mean recording a lower block after a
    /// higher one must never move progress backwards. This is the property a consumer relies
    /// on; an out-of-order or retried record call must not un-observe progress already made.
    #[test]
    fn record_scanned_to_never_moves_backwards() {
        let progress = ScanProgress::default();
        progress.record_scanned_to(100);
        assert_eq!(progress.scanned_to(), Some(100));

        progress.record_scanned_to(50);
        assert_eq!(
            progress.scanned_to(),
            Some(100),
            "a lower block recorded after a higher one must not move progress backwards"
        );

        progress.record_scanned_to(150);
        assert_eq!(progress.scanned_to(), Some(150));
    }

    /// Same `fetch_max` monotonicity property, for the tip field.
    #[test]
    fn record_observed_tip_never_moves_backwards() {
        let progress = ScanProgress::default();
        progress.record_observed_tip(100);
        progress.record_observed_tip(50);
        assert_eq!(
            progress.observed_tip(),
            Some(100),
            "a lower tip recorded after a higher one must not move progress backwards"
        );
    }
}