use std::sync::OnceLock;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UpdaterKind {
Pool,
Aave,
}
impl UpdaterKind {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Pool => "pool",
Self::Aave => "aave",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UpdaterStage {
Chunk,
Fetch,
Compute,
Verify,
Apply,
Cleanup,
}
impl UpdaterStage {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Chunk => "chunk",
Self::Fetch => "fetch",
Self::Compute => "compute",
Self::Verify => "verify",
Self::Apply => "apply",
Self::Cleanup => "cleanup",
}
}
}
#[derive(Clone, Copy)]
pub struct UpdaterInstruments {
pub observe_stage: fn(updater: UpdaterKind, stage: UpdaterStage, seconds: f64),
pub observe_lock_hold: fn(updater: UpdaterKind, seconds: f64),
pub add_rpc_round_trips: fn(updater: UpdaterKind, count: u64),
}
static PORT: OnceLock<Option<UpdaterInstruments>> = OnceLock::new();
pub fn register(instruments: Option<UpdaterInstruments>) {
let _ = PORT.set(instruments);
}
#[must_use]
pub fn updaters() -> Option<&'static UpdaterInstruments> {
PORT.get().and_then(|p| p.as_ref())
}
impl UpdaterInstruments {
pub fn observe_stage(&self, updater: UpdaterKind, stage: UpdaterStage, seconds: f64) {
(self.observe_stage)(updater, stage, seconds);
}
pub fn observe_lock_hold(&self, updater: UpdaterKind, seconds: f64) {
(self.observe_lock_hold)(updater, seconds);
}
pub fn add_rpc_round_trips(&self, updater: UpdaterKind, count: u64) {
(self.add_rpc_round_trips)(updater, count);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static OBSERVED_STAGES: Mutex<Vec<(UpdaterKind, UpdaterStage)>> = Mutex::new(Vec::new());
static OBSERVED_HOLDS: Mutex<Vec<(UpdaterKind, f64)>> = Mutex::new(Vec::new());
static OBSERVED_ROUNDS: Mutex<Vec<(UpdaterKind, u64)>> = Mutex::new(Vec::new());
#[expect(
clippy::unwrap_used,
reason = "test fixtures fail loudly on a poisoned lock"
)]
fn capture_stage(updater: UpdaterKind, stage: UpdaterStage, _seconds: f64) {
OBSERVED_STAGES.lock().unwrap().push((updater, stage));
}
#[expect(
clippy::unwrap_used,
reason = "test fixtures fail loudly on a poisoned lock"
)]
fn capture_hold(updater: UpdaterKind, seconds: f64) {
OBSERVED_HOLDS.lock().unwrap().push((updater, seconds));
}
#[expect(
clippy::unwrap_used,
reason = "test fixtures fail loudly on a poisoned lock"
)]
fn capture_rounds(updater: UpdaterKind, count: u64) {
OBSERVED_ROUNDS.lock().unwrap().push((updater, count));
}
#[test]
#[expect(clippy::unwrap_used, reason = "test fixtures fail loudly")]
fn delegates_forward_to_the_fn_pointers() {
let bundle = UpdaterInstruments {
observe_stage: capture_stage,
observe_lock_hold: capture_hold,
add_rpc_round_trips: capture_rounds,
};
bundle.observe_stage(UpdaterKind::Pool, UpdaterStage::Fetch, 0.25);
bundle.observe_stage(UpdaterKind::Aave, UpdaterStage::Apply, 90.0);
bundle.observe_lock_hold(UpdaterKind::Pool, 0.01);
bundle.add_rpc_round_trips(UpdaterKind::Aave, 7);
assert_eq!(
OBSERVED_STAGES.lock().unwrap().as_slice(),
&[
(UpdaterKind::Pool, UpdaterStage::Fetch),
(UpdaterKind::Aave, UpdaterStage::Apply),
]
);
assert_eq!(
OBSERVED_HOLDS.lock().unwrap().as_slice(),
&[(UpdaterKind::Pool, 0.01)]
);
assert_eq!(
OBSERVED_ROUNDS.lock().unwrap().as_slice(),
&[(UpdaterKind::Aave, 7)]
);
}
#[test]
fn label_strings_match_the_reviewed_closed_sets() {
assert_eq!(
[UpdaterKind::Pool.label(), UpdaterKind::Aave.label()],
["pool", "aave"]
);
assert_eq!(
[
UpdaterStage::Chunk.label(),
UpdaterStage::Fetch.label(),
UpdaterStage::Compute.label(),
UpdaterStage::Verify.label(),
UpdaterStage::Apply.label(),
UpdaterStage::Cleanup.label(),
],
["chunk", "fetch", "compute", "verify", "apply", "cleanup"]
);
}
#[test]
fn unregistered_builds_observe_every_hook_as_absent() {
assert!(updaters().is_none());
}
}