use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, PoisonError};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tracing::{debug, info, warn};
use super::GeoIpDownloadError;
const STAMP_EXT: &str = "retry";
pub(super) const FLOOR_SECS: u64 = 60 * 60;
pub(super) const CEILING_SECS: u64 = 24 * 60 * 60;
static ANNOUNCED: Mutex<Option<HashSet<(PathBuf, u64)>>> = Mutex::new(None);
static SUPPRESSED: Mutex<Option<HashMap<PathBuf, Suppressed>>> = Mutex::new(None);
struct Suppressed {
fingerprint: u64,
until: u64,
reason: String,
announced: bool,
}
pub(super) fn stamp_path(dest: &Path) -> PathBuf {
let mut name = dest.as_os_str().to_os_string();
name.push(".");
name.push(STAMP_EXT);
PathBuf::from(name)
}
fn now_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |elapsed| elapsed.as_secs())
}
pub(super) fn wait_for(dest: &Path, kind: &'static str, fingerprint: u64) -> Option<Duration> {
if let Some(wait) = suppressed(dest, kind, fingerprint) {
return Some(wait);
}
let stamp = stamp_path(dest);
let until: u64 = fs::read_to_string(&stamp).ok()?.trim().parse().ok()?;
let now = now_secs();
let until = until.min(now + CEILING_SECS);
let remaining = until.checked_sub(now).filter(|left| *left > 0)?;
let first = ANNOUNCED
.lock()
.unwrap_or_else(PoisonError::into_inner)
.get_or_insert_default()
.insert((stamp, until));
if first {
info!(
kind,
dest = %dest.display(),
wait_secs = remaining,
"the provider asked us to wait, serving the copy on disk until then"
);
}
Some(Duration::from_secs(remaining))
}
fn suppressed(dest: &Path, kind: &'static str, fingerprint: u64) -> Option<Duration> {
let mut held = SUPPRESSED.lock().unwrap_or_else(PoisonError::into_inner);
let held = held.get_or_insert_default();
let entry = held.get_mut(dest)?;
let remaining = entry.until.saturating_sub(now_secs());
if entry.fingerprint != fingerprint || remaining == 0 {
held.remove(dest);
return None;
}
if !std::mem::replace(&mut entry.announced, true) {
warn!(
kind,
dest = %dest.display(),
reason = %entry.reason,
wait_secs = remaining,
"not asking again until the configuration changes, serving the copy on disk"
);
}
Some(Duration::from_secs(remaining))
}
pub(super) fn begin(dest: &Path, error: &GeoIpDownloadError, fingerprint: u64) {
if error.is_permanent() {
SUPPRESSED
.lock()
.unwrap_or_else(PoisonError::into_inner)
.get_or_insert_default()
.insert(
dest.to_path_buf(),
Suppressed {
fingerprint,
until: now_secs() + CEILING_SECS,
reason: error.to_string(),
announced: false,
},
);
return;
}
let wait = match error {
GeoIpDownloadError::RateLimited {
retry_after_secs, ..
} => retry_after_secs.unwrap_or(FLOOR_SECS),
other if other.outcome() == "refused" => FLOOR_SECS,
_ => return,
};
let until = now_secs() + wait.min(CEILING_SECS);
if let Err(e) = fs::write(stamp_path(dest), until.to_string()) {
debug!(dest = %dest.display(), error = %e, "could not record the cool-down");
}
}
pub(super) fn clear(dest: &Path) {
let _ = fs::remove_file(stamp_path(dest));
if let Some(held) = SUPPRESSED
.lock()
.unwrap_or_else(PoisonError::into_inner)
.as_mut()
{
held.remove(dest);
}
}