Files
catena/crates/bibfetch/src/ask.rs
T

138 lines
4.5 KiB
Rust
Raw Normal View History

//! Asking the registers, which is the part that touches the network.
//!
//! Lifted out of the command line so that the Python bindings ask in exactly
//! the same order, with the same retries and the same backoff. Two
//! implementations of "which register answers about this identifier" would
//! drift, and the one nobody runs from a terminal would drift silently.
use std::time::Duration;
use crate::{Error, Record, Source};
/// How to ask: who to say you are, how long to wait, how often to retry.
#[derive(Debug, Clone)]
pub struct Ask {
/// An address the registers ask for, and answer faster when they get.
pub polite: Option<String>,
/// How long to wait on any one register.
pub timeout: Duration,
/// How many times to try a register that is unreachable.
pub attempts: u32,
}
impl Default for Ask {
fn default() -> Self {
Self { polite: None, timeout: crate::TIMEOUT, attempts: crate::ATTEMPTS }
}
}
impl Ask {
/// An agent configured to ask this way.
pub fn agent(&self) -> ureq::Agent {
ureq::Agent::config_builder()
.timeout_global(Some(self.timeout))
.user_agent(user_agent(self.polite.as_deref()))
.build()
.into()
}
}
/// Ask every register that answers about this kind of identifier, in turn,
/// and stop at the first complete answer.
///
/// An incomplete answer is kept in case nothing better comes: a title with no
/// authors is still more than nothing, and a register being down is only a
/// failure when every register is. `Ok(None)` means no register here answers
/// about that kind at all.
pub fn lookup(
agent: &ureq::Agent,
id: &str,
kind: &str,
attempts: u32,
) -> Result<Option<Record>, Error> {
let chain = Source::chain_for_kind(kind);
if chain.is_empty() {
return Ok(None);
}
let mut best: Option<Record> = None;
let mut last_error = None;
for source in chain {
match ask(agent, *source, id, kind, attempts) {
Ok(record) => {
let complete = record.found && record.title.is_some() && !record.authors.is_empty();
let better = best.as_ref().is_none_or(|kept| record.found && !kept.found);
if complete {
return Ok(Some(record));
}
if better {
best = Some(record);
}
}
Err(why) => last_error = Some(why),
}
}
match best {
Some(record) => Ok(Some(record)),
None => Err(last_error.unwrap_or_else(|| Error::Unreachable("no register answered".into()))),
}
}
/// Ask a register, retrying a failure that might not be one.
///
/// Waits a second before the second attempt and two before the third: long
/// enough for a register that is rate-limiting, short enough that one which
/// is simply down does not hold up a pipeline.
fn ask(
agent: &ureq::Agent,
source: Source,
id: &str,
kind: &str,
attempts: u32,
) -> Result<crate::Record, Error> {
let mut last = Error::Unreachable("no attempt was made".into());
for attempt in 0..attempts.max(1) {
match fetch(agent, source, id, kind) {
Ok(record) => return Ok(record),
// A shape this cannot read will not read any better next time.
Err(e @ Error::Shape(_)) => return Err(e),
Err(e) => last = e,
}
std::thread::sleep(Duration::from_secs(1 << attempt));
}
Err(last)
}
fn fetch(
agent: &ureq::Agent,
source: Source,
id: &str,
kind: &str,
) -> Result<crate::Record, Error> {
let response = agent.get(source.url(id)).call();
let mut response = match response {
Ok(response) => response,
// A register that does not have it says so with a status, and that is
// an answer rather than a failure.
Err(ureq::Error::StatusCode(404)) => return Ok(crate::missing(id, kind, source)),
Err(e) => return Err(Error::Unreachable(e.to_string())),
};
let body = response
.body_mut()
.read_to_string()
.map_err(|e| Error::Unreachable(e.to_string()))?;
source.read(id, kind, &body)
}
/// Who is asking.
///
/// The registers ask for an address and answer faster when they get one; a
/// tool that does not say who it is gets the slow pool.
fn user_agent(polite: Option<&str>) -> String {
let version = env!("CARGO_PKG_VERSION");
match polite {
Some(address) => format!("bibfetch/{version} (mailto:{address})"),
None => format!("bibfetch/{version}"),
}
}