From 1e9647bf8d2ac4cddcf3fb3604072ccece729ddf Mon Sep 17 00:00:00 2001 From: dawn <90008@klbr.net> Date: Mon, 5 Oct 2026 19:01:57 +0300 Subject: [PATCH] [api] type the resync failure kind and stop reporting 1970 retries ResyncInfo::Error's kind is a FailureKind serializing to the same strings instead of a hand-matched &'static str, and a stored retry time outside chrono's range is an error rather than silently the epoch. a listed row reads its repo metadata once, for both tracked and the pending key, and RepoHandle::info builds its row through the same repo_row the listing uses. --- src/control/repos/mod.rs | 90 +++++++++++++++--------------- tests/representation_inventory.tsv | 2 + 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/src/control/repos/mod.rs b/src/control/repos/mod.rs index 385d0b7..f278456 100644 --- a/src/control/repos/mod.rs +++ b/src/control/repos/mod.rs @@ -83,9 +83,7 @@ pub struct RepoInfo { pub enum ResyncInfo { /// the last backfill failed and will be retried at `next_retry`. Error { - /// `ratelimited` (the pds asked to slow down), `transport` (the pds - /// couldn't be reached) or `generic` (anything else). - kind: &'static str, + kind: FailureKind, retry_count: u32, next_retry: DateTime, /// transport failures in a row the repository's pds has had since @@ -101,42 +99,49 @@ pub enum ResyncInfo { }, } +/// why the last backfill of a repository failed, see [`ResyncInfo::Error`]. +#[cfg(feature = "indexer")] +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum FailureKind { + /// the pds asked to slow down + Ratelimited, + /// the pds couldn't be reached + Transport, + /// anything else + Generic, +} + #[cfg(feature = "indexer")] impl ResyncInfo { - fn new(state: crate::types::ResyncState, host_failures: usize) -> Self { + fn new(state: crate::types::ResyncState, host_failures: usize) -> Result { use crate::types::{ResyncErrorKind, ResyncState}; - match state { + Ok(match state { ResyncState::Error { kind, retry_count, next_retry, } => ResyncInfo::Error { kind: match kind { - ResyncErrorKind::Ratelimited => "ratelimited", - ResyncErrorKind::Transport => "transport", - ResyncErrorKind::Generic => "generic", + ResyncErrorKind::Ratelimited => FailureKind::Ratelimited, + ResyncErrorKind::Transport => FailureKind::Transport, + ResyncErrorKind::Generic => FailureKind::Generic, }, retry_count, - next_retry: DateTime::from_timestamp_secs(next_retry).unwrap_or_default(), + next_retry: DateTime::from_timestamp_secs(next_retry).ok_or_else(|| { + miette::miette!("resync entry retries at an impossible time: {next_retry}") + })?, host_failures, }, ResyncState::Gone { status } => ResyncInfo::Gone { status }, - } + }) } } /// fills in [`RepoInfo::pending`] and [`RepoInfo::resync`], which live in the /// indexer's queues rather than in the repo state. #[cfg(feature = "indexer")] -fn read_queues(state: &AppState, info: &mut RepoInfo) -> Result<()> { - let index_id = state - .db - .repo_metadata - .get(keys::repo_metadata_key(&info.did)) - .into_diagnostic()? - .map(|bytes| db::deser_repo_meta(&bytes)) - .transpose()? - .map(|metadata| metadata.index_id); +fn read_queues(state: &AppState, info: &mut RepoInfo, index_id: Option) -> Result<()> { if let Some(index_id) = index_id { info.pending = state .db @@ -161,7 +166,7 @@ fn read_queues(state: &AppState, info: &mut RepoInfo) -> Result<()> { .clone() .or_else(|| state.resolver.cached_pds(&info.did)); let host_failures = pds.map_or(0, |pds| state.throttler.snapshot(&pds).consecutive_failures); - info.resync = Some(ResyncInfo::new(resync, host_failures)); + info.resync = Some(ResyncInfo::new(resync, host_failures)?); Ok(()) } @@ -248,20 +253,21 @@ impl ReposControl { #[cfg_attr(not(feature = "indexer"), allow(unused_variables))] fn repo_row(state: &AppState, did: Did, s: RepoState<'_>) -> Result { #[cfg(feature = "indexer")] - let tracked = state + let metadata = state .db .repo_metadata .get(keys::repo_metadata_key(&did)) .into_diagnostic()? .map(|b| crate::db::deser_repo_meta(&b)) - .transpose()? - .is_none_or(|m| m.tracked); + .transpose()?; + #[cfg(feature = "indexer")] + let tracked = metadata.as_ref().is_none_or(|m| m.tracked); #[cfg(not(feature = "indexer"))] let tracked = true; #[allow(unused_mut)] let mut info = repo_state_to_info(did, s, tracked); #[cfg(feature = "indexer")] - read_queues(state, &mut info)?; + read_queues(state, &mut info, metadata.map(|m| m.index_id))?; Ok(info) } @@ -415,6 +421,18 @@ mod tests { assert_eq!(encoded["last_message_at"], "2026-07-11T16:00:00.123Z"); } + #[cfg(feature = "indexer")] + #[test] + fn a_retry_time_out_of_range_is_an_error_rather_than_1970() { + use crate::types::{ResyncErrorKind, ResyncState}; + let state = ResyncState::Error { + kind: ResyncErrorKind::Generic, + retry_count: 0, + next_retry: i64::MAX, + }; + assert!(ResyncInfo::new(state, 0).is_err()); + } + #[cfg(feature = "indexer")] #[tokio::test] async fn repo_info_shows_why_a_repo_waits_for_resync() -> miette::Result<()> { @@ -701,36 +719,16 @@ impl RepoHandle { pub async fn info(&self) -> Result> { let did = self.did.clone().into_static(); let did_key = keys::repo_key(&did); - #[cfg(feature = "indexer")] - let metadata_key = keys::repo_metadata_key(&did); - #[cfg(feature = "indexer")] let state = self.state.clone(); self.state .db .run(move |db| { - let state_bytes = db.repos.get(&did_key).into_diagnostic()?; - let Some(state_bytes) = state_bytes else { + let Some(state_bytes) = db.repos.get(&did_key).into_diagnostic()? else { return Ok(None); }; let repo_state = crate::db::deser_repo_state(&state_bytes)?; - - #[cfg(feature = "indexer")] - let tracked = db - .repo_metadata - .get(&metadata_key) - .into_diagnostic()? - .map(|b| crate::db::deser_repo_meta(&b)) - .transpose()? - .is_none_or(|m| m.tracked); - #[cfg(not(feature = "indexer"))] - let tracked = true; - - #[allow(unused_mut)] - let mut info = repo_state_to_info(did, repo_state, tracked); - #[cfg(feature = "indexer")] - read_queues(&state, &mut info)?; - Ok(Some(info)) + repo_row(&state, did, repo_state).map(Some) }) .await } diff --git a/tests/representation_inventory.tsv b/tests/representation_inventory.tsv index f79af31..7709852 100644 --- a/tests/representation_inventory.tsv +++ b/tests/representation_inventory.tsv @@ -519,6 +519,7 @@ serde-attribute:src/control/firehose.rs::FirehoseSourceInfo::pds::serde(skip_ser serde-attribute:src/control/firehose.rs::FirehoseSourceInfo::retry_in_secs::serde(skip_serializing_if="Option::is_none") nu:tests/api_firehose_sources.nu serde-attribute:src/control/firehose.rs::FirehoseSourceInfo::stats::serde(skip_serializing_if="Option::is_none") nu:tests/api_firehose_sources.nu serde-attribute:src/control/firehose.rs::FirehoseSourceInfo::throttled_until::serde(skip_serializing_if="Option::is_none") nu:tests/api_firehose_sources.nu +serde-attribute:src/control/repos/mod.rs::FailureKind::serde(rename_all="snake_case") rust:src/control/repos/mod.rs::repo_info_shows_why_a_repo_waits_for_resync serde-attribute:src/control/repos/mod.rs::RepoInfo::data::serde(serialize_with="crate::util::opt_cid_serialize_str") rust:src/control/repos/mod.rs::repo_info_json_contract serde-attribute:src/control/repos/mod.rs::RepoInfo::data::serde(skip_serializing_if="Option::is_none") rust:src/control/repos/mod.rs::repo_info_json_contract serde-attribute:src/control/repos/mod.rs::RepoInfo::handle::serde(skip_serializing_if="Option::is_none") rust:src/control/repos/mod.rs::repo_info_json_contract @@ -710,6 +711,7 @@ serde-type:src/control/firehose.rs::FirehosePdsInfo nu:tests/api_firehose_source serde-type:src/control/firehose.rs::FirehoseSourceInfo nu:tests/api_firehose_sources.nu serde-type:src/control/pds.rs::PdsTierAssignment nu:tests/pds_host_status_transitions.nu serde-type:src/control/pds.rs::PdsTierDefinition nu:tests/pds_host_status_transitions.nu +serde-type:src/control/repos/mod.rs::FailureKind rust:src/control/repos/mod.rs::repo_info_shows_why_a_repo_waits_for_resync serde-type:src/control/repos/mod.rs::RepoInfo rust:src/control/repos/mod.rs::repo_info_json_contract serde-type:src/control/repos/mod.rs::ResyncInfo rust:src/control/repos/mod.rs::repo_info_shows_why_a_repo_waits_for_resync serde-type:src/control/stats.rs::StatsResponse nu:tests/debug_endpoints.nu -- 2.54.0