use crate::disk::{self, DiskState, Ident, LINEAGE_FILE};
use crate::lease::{new_instance_id, BucketLease, Exclusion, LeaseError, LeaseWatch};
use crate::manifest::{
archive_key, crc32_hex, format_crc32, latest_key, manifest_key, object_key, parse_archive_name,
parse_segment_key, parse_versioned_generation, segment_key, side_key, snapshot_key, FileEntry,
Manifest, SegmentEntry, MANIFEST_VERSION,
};
use crate::segment::{complete_frames, read_new_segment, FrameCounter};
use crate::store::BackupConfig;
use crate::BackupError;
use mushroomdb::{SharedDb, WalPosition};
use object_store::buffered::BufWriter;
use object_store::path::Path as ObjectPath;
use object_store::{ObjectStore, PutMode, PutPayload, PutResult, UpdateVersion};
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
use std::future::Future;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::watch;
use tokio::task::JoinHandle;
use tokio::time::Instant;
pub const SIDE_FILES: [&str; 7] = [
LINEAGE_FILE,
"commit_times.bin",
"roles.json",
"keys.json",
"wal.genesis",
"wal.genesis.v11",
"wal.floor",
];
const CHAIN_FILES: [&str; 3] = ["wal.genesis", "wal.genesis.v11", "wal.floor"];
pub const UNHEALTHY_AFTER_FAILURES: u32 = 3;
pub const FIRST_BACKOFF: Duration = Duration::from_secs(1);
pub const MAX_BACKOFF: Duration = Duration::from_secs(60);
pub const MIN_UNHEALTHY_LAG: Duration = Duration::from_secs(30);
pub const RENEW_RETRY: Duration = Duration::from_secs(1);
const UPLOAD_CHUNK: usize = 8 * 1024 * 1024;
pub trait WalSource: Send + Sync + 'static {
fn durable_wal_position(&self) -> WalPosition;
fn catch_up(&self) {}
}
impl WalSource for SharedDb {
fn durable_wal_position(&self) -> WalPosition {
SharedDb::durable_wal_position(self)
}
fn catch_up(&self) {
self.refresh_if_stale_now();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BackupTrigger {
#[default]
Idle,
Snapshot,
Shutdown,
}
#[derive(Debug)]
pub struct BackupHandle {
pub error_count: Arc<AtomicU64>,
pub last_success_ts: Arc<AtomicU64>,
pub healthy: Arc<AtomicBool>,
pub lag_s: Arc<AtomicU64>,
pub lease_held: Arc<AtomicBool>,
pub reason: Arc<Mutex<Option<String>>>,
pub join: JoinHandle<()>,
}
#[derive(Debug, Clone)]
pub struct BackupCounters {
pub error_count: Arc<AtomicU64>,
pub last_success_ts: Arc<AtomicU64>,
pub healthy: Arc<AtomicBool>,
pub lag_s: Arc<AtomicU64>,
pub lease_held: Arc<AtomicBool>,
pub reason: Arc<Mutex<Option<String>>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct WalKey {
engine: u64,
snapshot: Option<Ident>,
wal: Option<u64>,
}
impl WalKey {
fn of(pos: &WalPosition) -> Self {
Self {
engine: pos.generation,
snapshot: pos.snapshot,
wal: pos.wal,
}
}
}
#[derive(Debug, Clone)]
struct Generation {
number: u64,
key: WalKey,
cursor: u64,
floor: u64,
chain: Vec<FileEntry>,
segments: Vec<SegmentEntry>,
}
#[derive(Debug, Default)]
struct HealthState {
shipped: Option<(WalKey, u64)>,
consecutive_failures: u32,
deferral: Option<String>,
seen: VecDeque<(u64, Instant)>,
seen_key: Option<WalKey>,
base_due_since: Option<Instant>,
lease: Option<String>,
lineage: Option<String>,
stopped: Option<String>,
}
#[derive(Clone)]
struct Health {
state: Arc<Mutex<HealthState>>,
healthy: Arc<AtomicBool>,
lag_s: Arc<AtomicU64>,
last_success_ts: Arc<AtomicU64>,
lease_held: Arc<AtomicBool>,
reason: Arc<Mutex<Option<String>>>,
limit: Duration,
}
impl Health {
fn lock(&self) -> std::sync::MutexGuard<'_, HealthState> {
self.state.lock().unwrap_or_else(|e| e.into_inner())
}
fn observe(&self, pos: &WalPosition, now: Instant) {
let mut s = self.lock();
let key = WalKey::of(pos);
let lag = match s.shipped {
Some((k, cursor)) if k == key => {
s.base_due_since = None;
if s.seen_key != Some(key) {
s.seen.clear();
s.seen_key = Some(key);
}
if pos.len > s.seen.back().map_or(cursor, |(l, _)| *l) {
s.seen.push_back((pos.len, now));
}
while s.seen.front().is_some_and(|(l, _)| *l <= cursor) {
s.seen.pop_front();
}
s.seen
.front()
.map(|(_, t)| now.saturating_duration_since(*t))
}
_ => {
let since = *s.base_due_since.get_or_insert(now);
Some(now.saturating_duration_since(since))
}
};
let lag = lag.unwrap_or(Duration::ZERO);
if lag.is_zero() {
self.last_success_ts.store(unix_now(), Ordering::Release);
}
self.lag_s.store(lag.as_secs(), Ordering::Release);
let reason = if let Some(r) = &s.stopped {
Some(r.clone())
} else if let Some(r) = &s.lineage {
Some(r.clone())
} else if let Some(r) = &s.lease {
Some(r.clone())
} else if s.consecutive_failures >= UNHEALTHY_AFTER_FAILURES {
Some(format!(
"{} consecutive failed backup steps",
s.consecutive_failures
))
} else if lag > self.limit {
Some(match &s.deferral {
Some(d) => format!("durable data unshipped for {}s: {d}", lag.as_secs()),
None => format!("durable data unshipped for {}s", lag.as_secs()),
})
} else {
None
};
self.healthy.store(reason.is_none(), Ordering::Release);
*self.reason.lock().unwrap_or_else(|e| e.into_inner()) = reason;
}
fn stop(&self, why: String) {
self.lock().stopped = Some(why.clone());
self.healthy.store(false, Ordering::Release);
self.lease_held.store(false, Ordering::Release);
*self.reason.lock().unwrap_or_else(|e| e.into_inner()) = Some(why);
}
}
pub struct BackupWorker {
config: BackupConfig,
store: Arc<dyn ObjectStore>,
source: Arc<dyn WalSource>,
dir: PathBuf,
prefix: String,
error_count: Arc<AtomicU64>,
health: Health,
backoff: Duration,
resumed: bool,
next_generation: u64,
generations: BTreeSet<u64>,
current: Option<Generation>,
snapshot: Option<FileEntry>,
archives: BTreeMap<u64, Archive>,
base_archives: Vec<u64>,
side: BTreeMap<&'static str, (Ident, FileEntry)>,
manifest_dirty: bool,
last_manifest_at: Option<Instant>,
write_budget: Option<u64>,
panic_next: bool,
lists: u64,
lease: Option<Arc<BucketLease>>,
renewer: Option<JoinHandle<()>>,
tenures: u64,
foreign: Option<String>,
instance_id: String,
watch: LeaseWatch,
pending_lease: Option<PendingLease>,
lineage: Option<String>,
parent_lineage: Option<String>,
parent_keys: HashSet<String>,
refused: bool,
latest_version: Option<UpdateVersion>,
latest_target: Option<String>,
manifest_versions: HashMap<String, UpdateVersion>,
allocated: BTreeSet<u64>,
}
type LeaseAttempt = Result<Result<BucketLease, LeaseError>, tokio::time::error::Elapsed>;
type PendingLease = Mutex<Pin<Box<dyn Future<Output = (LeaseWatch, LeaseAttempt)> + Send>>>;
#[derive(Debug, Clone)]
struct Archive {
len: u64,
entry: FileEntry,
verified: bool,
}
impl BackupWorker {
pub fn new(
config: BackupConfig,
store: Arc<dyn ObjectStore>,
source: Arc<dyn WalSource>,
store_dir: PathBuf,
) -> Result<Self, BackupError> {
let prefix = config.location()?.prefix;
let limit = MIN_UNHEALTHY_LAG.max(Duration::from_millis(config.wal_interval_ms) * 10);
Ok(Self {
config,
store,
source,
dir: store_dir,
prefix,
error_count: Arc::new(AtomicU64::new(0)),
health: Health {
state: Arc::default(),
healthy: Arc::new(AtomicBool::new(true)),
lag_s: Arc::new(AtomicU64::new(0)),
last_success_ts: Arc::new(AtomicU64::new(0)),
lease_held: Arc::new(AtomicBool::new(false)),
reason: Arc::default(),
limit,
},
backoff: FIRST_BACKOFF,
resumed: false,
next_generation: 0,
generations: BTreeSet::new(),
current: None,
snapshot: None,
archives: BTreeMap::new(),
base_archives: Vec::new(),
side: BTreeMap::new(),
manifest_dirty: false,
last_manifest_at: None,
write_budget: None,
panic_next: false,
lists: 0,
lease: None,
renewer: None,
tenures: 0,
foreign: None,
instance_id: new_instance_id(),
watch: LeaseWatch::default(),
pending_lease: None,
lineage: None,
parent_lineage: None,
parent_keys: HashSet::new(),
refused: false,
latest_version: None,
latest_target: None,
manifest_versions: HashMap::new(),
allocated: BTreeSet::new(),
})
}
pub fn spawn(
config: BackupConfig,
store: Arc<dyn ObjectStore>,
source: Arc<dyn WalSource>,
store_dir: PathBuf,
snapshot_rx: watch::Receiver<BackupTrigger>,
) -> Result<BackupHandle, BackupError> {
Ok(Self::new(config, store, source, store_dir)?.start(snapshot_rx))
}
pub fn start(self, snapshot_rx: watch::Receiver<BackupTrigger>) -> BackupHandle {
let BackupCounters {
error_count,
last_success_ts,
healthy,
lag_s,
lease_held,
reason,
} = self.counters();
let health = self.health.clone();
let source = Arc::clone(&self.source);
let cadence = Duration::from_millis(self.config.wal_interval_ms);
let worker = tokio::spawn(self.run(snapshot_rx));
let join = tokio::spawn(async move {
let mut tick = tokio::time::interval(cadence);
tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
tokio::pin!(worker);
let ended = loop {
tokio::select! {
ended = &mut worker => break ended,
_ = tick.tick() => {
let pos = position_of(&source).await;
health.observe(&pos, Instant::now());
}
}
};
let why = match ended {
Ok(()) => "stopped".to_string(),
Err(e) if e.is_panic() => {
let p = e.into_panic();
let msg = p
.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| p.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "a non-string panic".into());
tracing::error!(panic = %msg, "backup worker panicked; backup has stopped");
format!("backup worker panicked: {msg}")
}
Err(e) => format!("backup worker ended: {e}"),
};
health.stop(why);
});
BackupHandle {
error_count,
last_success_ts,
healthy,
lag_s,
lease_held,
reason,
join,
}
}
pub fn counters(&self) -> BackupCounters {
BackupCounters {
error_count: Arc::clone(&self.error_count),
last_success_ts: Arc::clone(&self.health.last_success_ts),
healthy: Arc::clone(&self.health.healthy),
lag_s: Arc::clone(&self.health.lag_s),
lease_held: Arc::clone(&self.health.lease_held),
reason: Arc::clone(&self.health.reason),
}
}
pub fn instance_id(&self) -> &str {
&self.instance_id
}
pub fn adopt_lease(&mut self, lease: Arc<BucketLease>) {
self.instance_id = lease.instance_id().to_string();
self.hold(lease, Duration::ZERO);
}
#[doc(hidden)]
pub fn fail_after_writes(&mut self, n: u64) {
self.write_budget = Some(n);
}
#[doc(hidden)]
pub fn panic_on_next_step(&mut self) {
self.panic_next = true;
}
#[doc(hidden)]
pub fn list_requests(&self) -> u64 {
self.lists
}
async fn run(mut self, mut rx: watch::Receiver<BackupTrigger>) {
#[cfg(test)]
let probe_control = self.watch.probe_control.clone();
let mut delay = Duration::ZERO;
loop {
let shutdown = tokio::select! {
_ = tokio::time::sleep(delay) => false,
changed = rx.changed() => is_shutdown(changed, &mut rx),
};
if shutdown || *rx.borrow() == BackupTrigger::Shutdown || rx.has_changed().is_err() {
break;
}
if !self.holds_lease() {
match until_shutdown(&mut rx, self.take_lease()).await {
None => break,
Some(Some(wait)) => {
delay = wait;
continue;
}
Some(None) => {}
}
}
match until_shutdown(&mut rx, self.step()).await {
Some(d) => delay = d,
None => break,
}
if self.refused {
break;
}
}
#[cfg(test)]
if let Some(control) = &probe_control {
control.shutdown.notify_one();
}
if self.refused {
self.release_lease().await;
until_shutdown(&mut rx, std::future::pending::<()>()).await;
return;
}
let grace = Duration::from_millis(self.config.shutdown_grace_ms);
let flush_deadline = Instant::now() + grace;
if self.pending_lease.is_some()
&& tokio::time::timeout_at(flush_deadline, self.finish_lease_attempt(true))
.await
.is_err()
{
self.error_count.fetch_add(1, Ordering::AcqRel);
tracing::warn!("backup lease acquisition exceeded the shutdown grace; canceled");
}
self.pending_lease = None;
if self.holds_lease()
&& Instant::now() < flush_deadline
&& tokio::time::timeout_at(flush_deadline, self.flush())
.await
.is_err()
{
self.error_count.fetch_add(1, Ordering::AcqRel);
tracing::warn!(
grace_ms = self.config.shutdown_grace_ms,
"backup flush did not finish within the shutdown grace; abandoned"
);
}
self.release_lease().await;
}
fn holds_lease(&self) -> bool {
self.lease.as_ref().is_some_and(|l| !l.is_lost())
}
async fn take_lease(&mut self) -> Option<Duration> {
if self.lease.as_ref().is_some_and(|l| l.is_lost()) {
if let Some(r) = self.renewer.take() {
r.abort();
}
self.lease = None;
self.health.lease_held.store(false, Ordering::Release);
}
if self.pending_lease.is_none() {
let store = Arc::clone(&self.store);
let prefix = self.prefix.clone();
let instance_id = self.instance_id.clone();
let ttl_s = self.config.lease_ttl_s;
let mut watch = std::mem::take(&mut self.watch);
let limit = self.op_timeout() * 4;
self.pending_lease = Some(Mutex::new(Box::pin(async move {
let attempt =
BucketLease::acquire_as(store, &prefix, ttl_s, &instance_id, &mut watch);
let outcome = tokio::time::timeout(limit, attempt).await;
(watch, outcome)
})));
}
let wait = self.finish_lease_attempt(false).await;
let pos = self.position().await;
self.health.observe(&pos, Instant::now());
wait
}
async fn finish_lease_attempt(&mut self, shutting_down: bool) -> Option<Duration> {
let (watch, outcome) = self
.pending_lease
.as_mut()?
.get_mut()
.unwrap_or_else(|error| error.into_inner())
.await;
self.pending_lease = None;
self.watch = watch;
match outcome {
Ok(Ok(lease)) => {
let first = if shutting_down {
Duration::ZERO
} else {
lease.renew_interval()
};
self.hold(Arc::new(lease), first);
None
}
Ok(Err(LeaseError::ForeignActive { instance_id })) => {
if self.foreign.as_deref() != Some(instance_id.as_str()) {
tracing::warn!(
holder = %instance_id,
"the backup bucket's lease is held by another instance; \
this one uploads nothing while that lease is fresh"
);
self.foreign = Some(instance_id.clone());
}
self.health.lease_held.store(false, Ordering::Release);
self.health.lock().lease = Some(format!(
"the backup bucket's lease is held by another instance ({instance_id}); \
this instance uploads nothing while it is"
));
Some(poll_interval(self.config.lease_ttl_s))
}
Ok(Err(e @ LeaseError::PreconditionsIgnored { .. })) => {
tracing::error!(error = %e, "backup is not started");
self.health.lease_held.store(false, Ordering::Release);
self.health.lock().lease = Some(e.to_string());
Some(MAX_BACKOFF)
}
Ok(Err(LeaseError::Store(e))) if self.config.lease_conditional => {
let why = format!(
"taking the bucket lease: {} (if this service rejects conditional PUT \
headers, set MUSHROOMDB_BACKUP_LEASE_CONDITIONAL=off)",
crate::redact(&e.to_string())
);
Some(self.failed(&BackupError::Lease(why)))
}
Ok(Err(e)) => Some(self.failed(&BackupError::Lease(e.to_string()))),
Err(_) => Some(self.failed(&BackupError::Timeout("taking the bucket lease".into()))),
}
}
fn hold(&mut self, lease: Arc<BucketLease>, first: Duration) {
if self.tenures > 0 {
self.reset_tenure();
}
self.tenures += 1;
self.foreign = None;
self.health.lock().lease = None;
self.health.lease_held.store(true, Ordering::Release);
match lease.exclusion() {
Exclusion::Conditional => tracing::info!(
instance_id = lease.instance_id(),
"took the backup bucket lease (conditional writes: one writer at a time)"
),
Exclusion::BestEffort => tracing::warn!(
instance_id = lease.instance_id(),
"took the backup bucket lease BEST-EFFORT: conditional writes are off, so two \
instances started together may both upload and overwrite each other's objects"
),
}
self.renewer = Some(tokio::spawn(renew_loop(
Arc::clone(&lease),
first,
self.op_timeout(),
self.health.clone(),
Arc::clone(&self.error_count),
)));
self.lease = Some(lease);
}
fn reset_tenure(&mut self) {
self.resumed = false;
self.lineage = None;
self.parent_lineage = None;
self.parent_keys.clear();
self.latest_version = None;
self.latest_target = None;
self.manifest_versions.clear();
self.allocated.clear();
self.next_generation = 0;
self.generations.clear();
self.current = None;
self.snapshot = None;
self.archives.clear();
self.base_archives.clear();
self.side.clear();
self.manifest_dirty = false;
self.last_manifest_at = None;
self.health.lock().shipped = None;
}
async fn release_lease(&mut self) {
self.pending_lease = None;
self.health.lease_held.store(false, Ordering::Release);
let deadline = Instant::now() + self.op_timeout();
let release = async {
if let Some(renewer) = self.renewer.take() {
renewer.abort();
let _ = renewer.await;
}
if let Some(lease) = self.lease.take() {
if lease.is_lost() {
return Ok(());
}
if let Ok(lease) = Arc::try_unwrap(lease) {
return lease.release().await;
}
}
crate::lease::release_own_record(
&self.store,
&ObjectPath::from(crate::lease::lease_key(&self.prefix)),
&self.instance_id,
)
.await
};
match tokio::time::timeout_at(deadline, release).await {
Ok(Ok(())) => tracing::info!("released the backup bucket lease"),
Ok(Err(e)) => {
tracing::warn!(error = %e, "could not release the backup bucket lease; it expires by itself")
}
Err(_) => {
tracing::warn!("releasing the backup bucket lease timed out; it expires by itself")
}
}
}
pub async fn step(&mut self) -> Duration {
let result = self.run_once().await;
let delay = match result {
Ok(()) => {
self.health.lock().consecutive_failures = 0;
self.backoff = FIRST_BACKOFF;
Duration::from_millis(self.config.wal_interval_ms)
}
Err(e) => self.failed(&e),
};
let pos = self.position().await;
self.health.observe(&pos, Instant::now());
delay
}
fn failed(&mut self, e: &BackupError) -> Duration {
#[cfg(test)]
if let Some(control) = &self.watch.probe_control {
control.failed.notify_one();
}
self.error_count.fetch_add(1, Ordering::AcqRel);
let failures = {
let mut s = self.health.lock();
s.consecutive_failures += 1;
s.consecutive_failures
};
let delay = self.backoff;
self.backoff = (self.backoff * 2).min(MAX_BACKOFF);
tracing::warn!(
error = %e,
consecutive_failures = failures,
retry_in_s = delay.as_secs(),
"backup step failed"
);
delay
}
pub async fn flush(&mut self) {
for attempt in 0..3u32 {
let done = match self.run_once().await {
Ok(()) => self.write_manifest().await,
other => other,
};
match done {
Ok(()) => {
let pos = self.position().await;
self.health.observe(&pos, Instant::now());
return;
}
Err(e) => {
self.error_count.fetch_add(1, Ordering::AcqRel);
tracing::warn!(error = %e, attempt, "backup flush failed");
if attempt < 2 {
tokio::time::sleep(FIRST_BACKOFF * (1 << attempt)).await;
}
}
}
}
}
fn defer(&mut self, why: &str) -> Result<(), BackupError> {
self.health.lock().deferral = Some(why.to_string());
Ok(())
}
pub async fn run_once(&mut self) -> Result<(), BackupError> {
if std::mem::take(&mut self.panic_next) {
panic!("test hook: backup worker panics");
}
if !self.resumed && !self.resume().await? {
return self.defer("the store's commit count is not known yet");
}
let mut disk = self.observe().await?;
if disk.intent_nonempty {
return self.defer("a snapshot is in progress (snapshot.intent is not empty)");
}
let mut pos = self.position().await;
if pos.snapshot != disk.snapshot || pos.wal != disk.wal || disk.wal_len != pos.len {
self.catch_up().await?;
let after = self.observe().await?;
if !disk.unchanged_until(&after) {
return self.defer("the store's files changed while catching up");
}
disk = after;
pos = self.position().await;
}
if pos.snapshot_in_progress {
return self.defer("this handle is taking a snapshot");
}
if pos.snapshot != disk.snapshot || pos.wal != disk.wal || disk.wal_len < pos.len {
return self.defer("the handle has not caught up with the files on disk");
}
self.health.lock().deferral = None;
if self.current.as_ref().map(|g| g.key) != Some(WalKey::of(&pos)) {
return self.upload_base(pos, disk).await;
}
self.ship_segments().await?;
if self.manifest_due() {
self.write_manifest().await?;
}
Ok(())
}
fn manifest_due(&self) -> bool {
let interval = Duration::from_secs(self.config.manifest_interval_s);
self.manifest_dirty
&& self
.last_manifest_at
.is_none_or(|t| t.elapsed() >= interval)
}
async fn observe(&self) -> Result<DiskState, BackupError> {
let dir = self.dir.clone();
Ok(blocking(move || disk::observe(&dir)).await?)
}
async fn position(&self) -> WalPosition {
position_of(&self.source).await
}
async fn catch_up(&self) -> Result<(), BackupError> {
let source = Arc::clone(&self.source);
blocking(move || {
source.catch_up();
Ok(())
})
.await?;
Ok(())
}
async fn resume(&mut self) -> Result<bool, BackupError> {
let latest = self.latest_manifest().await?;
let dir = self.dir.clone();
let (local, parent) =
blocking(move || Ok((disk::read_lineage(&dir)?, disk::read_parent(&dir)?))).await?;
let adopt = match (&local, &latest, &parent) {
(Some(ours), Some(m), Some(p)) if *ours != m.lineage && p.lineage == m.lineage => {
if self.latest_target.as_deref() != Some(p.manifest.as_str()) {
return Err(self.refuse(format!(
"this store was restored from {} of lineage {}, but the backup prefix \
has moved on since (LATEST names {}): another instance has written \
to it. Nothing is uploaded or deleted. Point MUSHROOMDB_BACKUP_S3_URL \
at a new prefix",
p.manifest,
p.lineage,
self.latest_target.as_deref().unwrap_or("nothing")
)));
}
true
}
_ => false,
};
let lineage = match (local, &latest) {
(Some(ours), Some(m)) if ours != m.lineage && !adopt => {
return Err(self.refuse(format!(
"this backup prefix holds the backups of another store (lineage {}), \
not this one (lineage {ours}); nothing is uploaded or deleted. Point \
MUSHROOMDB_BACKUP_S3_URL at a new prefix, or restore this store from \
that prefix first",
m.lineage
)))
}
(None, Some(m)) => {
return Err(self.refuse(format!(
"this backup prefix holds the backups of another store (lineage {}), and \
this store has never been backed up; nothing is uploaded or deleted. \
Point MUSHROOMDB_BACKUP_S3_URL at a new prefix, or restore this store \
from that prefix first",
m.lineage
)))
}
(Some(ours), Some(m)) => match self.position().await.frames {
None => return Ok(false),
Some(have) if have < m.commit_seq => {
return Err(self.refuse(format!(
"this store holds {have} commits, fewer than the {} of the newest \
restore point in the backup prefix: it was restored from an older \
backup, or rewound. Uploading would bury the newer history, so \
nothing is uploaded or deleted. Point MUSHROOMDB_BACKUP_S3_URL at \
a new prefix",
m.commit_seq
)))
}
Some(_) => ours,
},
(Some(ours), None) => ours,
(None, None) => {
let id = new_instance_id();
let (dir, new) = (self.dir.clone(), id.clone());
blocking(move || disk::create_lineage(&dir, &new)).await?;
tracing::info!(lineage = %id, "this store's first backup: a new backup lineage");
id
}
};
if adopt {
tracing::info!(
lineage = %lineage,
parent = parent.as_ref().map_or("", |p| p.lineage.as_str()),
"continuing the backup prefix this store was restored from, as a new lineage"
);
}
self.parent_lineage = parent.map(|p| p.lineage);
let wal_dir = ObjectPath::from(object_key(&self.prefix, "wal"));
for p in self.list(&wal_dir).await?.common_prefixes {
if let Some(g) = p.filename().and_then(|s| s.parse::<u64>().ok()) {
self.generations.insert(g);
}
}
for dir in ["snapshots", "archives"] {
let path = ObjectPath::from(object_key(&self.prefix, dir));
for o in self.list(&path).await?.objects {
if let Some(g) = parse_versioned_generation(&self.prefix, o.location.as_ref()) {
self.generations.insert(g);
}
}
}
let mut highest = self.generations.last().copied();
if let Some(m) = latest {
highest = highest.max(Some(m.wal_generation));
let files = if m.lineage == lineage {
m.files
} else {
Vec::new()
};
for f in &files {
if let Some(n) = parse_archive_name(&f.name) {
self.archives.insert(
n,
Archive {
len: f.size,
entry: f.clone(),
verified: false,
},
);
}
}
}
self.next_generation = highest.map_or(0, |g| g + 1);
self.lineage = Some(lineage);
self.resumed = true;
Ok(true)
}
fn refuse(&mut self, why: String) -> BackupError {
tracing::error!(reason = %why, "backup refuses this prefix");
self.health.lock().lineage = Some(why.clone());
self.refused = true;
BackupError::Lineage(why)
}
fn lineage(&self) -> String {
self.lineage.clone().unwrap_or_default()
}
async fn latest_manifest(&mut self) -> Result<Option<Manifest>, BackupError> {
let key = match self.get_versioned(&latest_key(&self.prefix)).await? {
Some((b, v)) => {
self.latest_version = Some(v);
String::from_utf8(b)
.map_err(|_| BackupError::Manifest("LATEST is not UTF-8".into()))?
}
None => {
self.latest_version = None;
return Ok(None);
}
};
self.latest_target = Some(key.trim().to_string());
let bytes = self.get(key.trim()).await?.ok_or_else(|| {
BackupError::Manifest(format!("LATEST names {key}, which does not exist"))
})?;
Manifest::from_json(&bytes)
.map(Some)
.map_err(BackupError::Manifest)
}
async fn upload_base(&mut self, pos: WalPosition, disk: DiskState) -> Result<(), BackupError> {
let dir = self.dir.clone();
let read = blocking(move || {
let snapshot = disk::open_snapshot(&dir)?;
let mut archives = Vec::new();
for (n, len) in disk::local_archives(&dir)? {
if let Ok(f) = std::fs::File::open(dir.join(format!("wal.{n}.archive"))) {
archives.push((n, len, f));
}
}
let mut chain = Vec::new();
for name in CHAIN_FILES {
if let Some((_, Some(bytes))) = disk::read_side(&dir, name, None)? {
chain.push((name, bytes));
}
}
Ok((snapshot, archives, chain))
})
.await?;
let (snapshot, archives, chain) = read;
let after = self.observe().await?;
let pos_after = self.position().await;
let opened_ident = snapshot.as_ref().map(|(_, i)| *i);
if !disk.unchanged_until(&after)
|| opened_ident != disk.snapshot
|| WalKey::of(&pos_after) != WalKey::of(&pos)
|| pos_after.snapshot_in_progress
{
return self.defer("the store's files changed while a restore point was read");
}
let number = self.next_generation;
self.next_generation += 1;
self.generations.insert(number);
self.allocated.insert(number);
let mut present = Vec::new();
for (n, len, mut file) in archives {
let reusable = match self.archives.get(&n) {
Some(a) if a.len == len && a.verified => true,
Some(a) if a.len == len => {
let want = a.entry.crc32.clone();
let (crc, f) =
blocking(move || disk::crc32_of(&mut file).map(|c| (c, file))).await?;
file = f;
crc == want
}
_ => false,
};
if reusable {
if let Some(a) = self.archives.get_mut(&n) {
a.verified = true;
}
} else {
let name = format!("wal.{n}.archive");
let key = archive_key(&self.prefix, number, n);
let entry = self.put_file(&name, &key, file, true).await?;
self.archives.insert(
n,
Archive {
len: entry.size,
entry,
verified: true,
},
);
}
present.push(n);
}
let mut floor = 0;
let mut chain_entries = Vec::new();
for (name, bytes) in chain {
if name == "wal.floor" {
floor = bytes
.get(..8)
.map_or(0, |b| u64::from_le_bytes(b.try_into().unwrap()));
}
chain_entries.push(self.put_side(name, bytes).await?);
}
let archive_frames: u64 = present
.iter()
.filter_map(|n| self.archives.get(n))
.map(|a| a.entry.frames)
.sum();
let snapshot = match snapshot {
Some((file, _)) => {
let key = snapshot_key(&self.prefix, number, floor + archive_frames);
Some(self.put_file("snapshot.bin", &key, file, false).await?)
}
None => None,
};
self.sync_side_files().await?;
self.snapshot = snapshot;
self.base_archives = present;
self.current = Some(Generation {
number,
key: WalKey::of(&pos),
cursor: 0,
floor,
chain: chain_entries,
segments: Vec::new(),
});
self.record_shipped();
self.manifest_dirty = true;
self.ship_segments().await?;
self.write_manifest().await
}
async fn put_side(&mut self, name: &str, bytes: Vec<u8>) -> Result<FileEntry, BackupError> {
let crc32 = crc32_hex(&bytes);
let size = bytes.len() as u64;
let key = side_key(&self.prefix, name, &crc32, size);
self.put_immutable(&key, bytes, &crc32).await?;
Ok(FileEntry {
name: name.to_string(),
key,
size,
crc32,
frames: 0,
lineage: self.lineage(),
})
}
async fn sync_side_files(&mut self) -> Result<(), BackupError> {
for name in SIDE_FILES {
if CHAIN_FILES.contains(&name) {
continue;
}
let known = self.side.get(name).map(|(i, _)| *i);
let dir = self.dir.clone();
let read = blocking(move || disk::read_side(&dir, name, known)).await?;
let Some((ident, bytes)) = read else {
if self.side.remove(name).is_some() {
self.manifest_dirty = true;
}
continue;
};
let Some(bytes) = bytes else {
continue;
};
let crc32 = crc32_hex(&bytes);
let size = bytes.len() as u64;
let unchanged = self
.side
.get(name)
.is_some_and(|(_, e)| e.key == side_key(&self.prefix, name, &crc32, size));
let entry = if unchanged {
self.side[name].1.clone()
} else {
self.manifest_dirty = true;
self.put_side(name, bytes).await?
};
self.side.insert(name, (ident, entry));
}
Ok(())
}
async fn ship_segments(&mut self) -> Result<(), BackupError> {
loop {
let Some(g) = self.current.as_ref() else {
return Ok(());
};
let (key, cursor, number) = (g.key, g.cursor, g.number);
let before = self.observe().await?;
let pos = self.position().await;
if before.intent_nonempty
|| before.snapshot != key.snapshot
|| before.wal != key.wal
|| WalKey::of(&pos) != key
{
return Ok(());
}
if pos.len <= cursor {
return Ok(());
}
let wal = self.dir.join("wal.bin");
let len = pos.len;
let read = blocking(move || read_new_segment(&wal, cursor, len)).await?;
let after = self.observe().await?;
if !before.unchanged_until(&after) || WalKey::of(&self.position().await) != key {
return Ok(());
}
let Some((start, end, bytes)) = read else {
return Ok(());
};
let (_, frames) = complete_frames(&bytes);
let seg_key = segment_key(&self.prefix, number, start, end);
let crc32 = crc32_hex(&bytes);
self.put_immutable(&seg_key, bytes, &crc32).await?;
let lineage = self.lineage();
if let Some(g) = self.current.as_mut() {
g.cursor = end;
g.segments.push(SegmentEntry {
key: seg_key,
crc32,
frames,
lineage,
});
}
self.record_shipped();
self.manifest_dirty = true;
}
}
fn record_shipped(&self) {
if let Some(g) = &self.current {
self.health.lock().shipped = Some((g.key, g.cursor));
}
}
async fn write_manifest(&mut self) -> Result<(), BackupError> {
self.sync_side_files().await?;
let Some(generation) = self.current.as_ref() else {
return Ok(());
};
let mut files = Vec::new();
files.extend(self.snapshot.clone());
for n in &self.base_archives {
if let Some(a) = self.archives.get(n) {
files.push(a.entry.clone());
}
}
files.extend(generation.chain.iter().cloned());
files.extend(self.side.values().map(|(_, e)| e.clone()));
let commit_seq = generation.floor
+ files.iter().map(|f| f.frames).sum::<u64>()
+ generation.segments.iter().map(|s| s.frames).sum::<u64>();
let manifest = Manifest {
version: MANIFEST_VERSION,
lineage: self.lineage(),
created_at: now_rfc3339(),
commit_seq,
oldest_commit_seq: generation.floor,
files,
wal_generation: generation.number,
segments: generation.segments.clone(),
};
manifest.verify_lineage().map_err(BackupError::Manifest)?;
let key = manifest_key(&self.prefix, manifest.commit_seq);
if self.parent_keys.contains(&key) || !self.put_manifest(&key, manifest.to_json()).await? {
return Ok(());
}
self.put_latest(&key).await?;
self.manifest_dirty = false;
self.last_manifest_at = Some(Instant::now());
if self.config.retain_n > 0 {
self.apply_retention(&key).await?;
}
Ok(())
}
async fn apply_retention(&mut self, just_written: &str) -> Result<(), BackupError> {
let dir = ObjectPath::from(object_key(&self.prefix, "manifest"));
let mut keys: Vec<String> = self
.list(&dir)
.await?
.objects
.into_iter()
.map(|o| o.location.to_string())
.filter(|k| k.ends_with(".json"))
.collect();
keys.sort();
let ours = self.lineage();
let mut mine: Vec<(String, Manifest)> = Vec::new();
let mut foreign: HashSet<String> = HashSet::new();
for k in keys {
let Some(bytes) = self.get(&k).await? else {
continue;
};
match Manifest::from_json(&bytes) {
Ok(m) if m.lineage == ours => mine.push((k, m)),
Ok(m) => {
foreign.extend(m.files.into_iter().map(|f| f.key));
foreign.extend(m.segments.into_iter().map(|s| s.key));
}
Err(_) => {}
}
}
if mine.len() <= self.config.retain_n {
return Ok(());
}
let newest_start = mine.len() - self.config.retain_n;
let protected = |k: &str| k == just_written || self.latest_target.as_deref() == Some(k);
let (mut kept, mut dropped) = (Vec::new(), Vec::new());
for (i, (k, m)) in mine.into_iter().enumerate() {
if i >= newest_start || protected(&k) {
kept.push((k, m));
} else {
dropped.push((k, m));
}
}
if dropped.is_empty() {
return Ok(());
}
let mut owned: BTreeSet<u64> = self.allocated.clone();
owned.extend(kept.iter().chain(&dropped).map(|(_, m)| m.wal_generation));
for (k, _) in &dropped {
self.delete(k).await?;
}
let mut referenced: HashSet<String> = HashSet::new();
let mut oldest_kept = u64::MAX;
for (_, m) in &kept {
oldest_kept = oldest_kept.min(m.wal_generation);
referenced.extend(m.files.iter().map(|f| f.key.clone()));
referenced.extend(m.segments.iter().map(|s| s.key.clone()));
}
let mut doomed = Vec::new();
for top in ["snapshots", "archives"] {
for k in self.list_all(&object_key(&self.prefix, top)).await? {
let ours = parse_versioned_generation(&self.prefix, &k)
.is_some_and(|g| owned.contains(&g));
if ours && !referenced.contains(&k) {
doomed.push(k);
}
}
}
let side_prefix = object_key(&self.prefix, "side/");
let side: BTreeSet<String> = dropped
.iter()
.flat_map(|(_, m)| m.files.iter().map(|f| f.key.clone()))
.filter(|k| k.starts_with(&side_prefix) && !referenced.contains(k))
.collect();
doomed.extend(side);
let current = self.current.as_ref().map(|g| g.number);
let retiring: Vec<u64> = owned
.iter()
.copied()
.filter(|g| *g < oldest_kept && Some(*g) != current)
.collect();
for g in &retiring {
let prefix = object_key(&self.prefix, &format!("wal/{g}"));
for k in self.list_all(&prefix).await? {
if parse_segment_key(&self.prefix, &k).is_some_and(|(sg, _, _)| sg == *g)
&& !referenced.contains(&k)
{
doomed.push(k);
}
}
}
for k in doomed {
if !foreign.contains(&k) {
self.delete(&k).await?;
}
}
for g in retiring {
self.generations.remove(&g);
}
let keep: HashSet<&String> = referenced.iter().collect();
self.archives.retain(|_, a| keep.contains(&a.entry.key));
Ok(())
}
async fn list_all(&mut self, prefix: &str) -> Result<Vec<String>, BackupError> {
let mut out = Vec::new();
let mut dirs = vec![ObjectPath::from(prefix)];
while let Some(d) = dirs.pop() {
let r = self.list(&d).await?;
out.extend(r.objects.into_iter().map(|o| o.location.to_string()));
dirs.extend(r.common_prefixes);
}
Ok(out)
}
fn op_timeout(&self) -> Duration {
Duration::from_millis(self.config.op_timeout_ms)
}
async fn deadline<T>(
&self,
what: impl FnOnce() -> String,
fut: impl Future<Output = Result<T, BackupError>>,
) -> Result<T, BackupError> {
match tokio::time::timeout(self.op_timeout(), fut).await {
Ok(r) => r,
Err(_) => Err(BackupError::Timeout(what())),
}
}
fn spend_write(&mut self) -> Result<(), BackupError> {
match self.write_budget.as_mut() {
Some(0) => Err(BackupError::Inconsistent(
"test hook: the process is gone".into(),
)),
Some(n) => {
*n -= 1;
Ok(())
}
None => Ok(()),
}
}
async fn fenced<T>(
&self,
what: impl FnOnce() -> String,
fut: impl Future<Output = Result<T, BackupError>>,
) -> Result<T, BackupError> {
fence(self.lease.as_deref(), self.deadline(what, fut)).await
}
async fn put_mode(
&mut self,
key: &str,
payload: PutPayload,
mode: PutMode,
) -> Result<Result<PutResult, object_store::Error>, BackupError> {
self.spend_write()?;
let path = ObjectPath::from(key);
let store = Arc::clone(&self.store);
self.fenced(|| format!("PUT {key}"), async move {
Ok(store.put_opts(&path, payload, mode.into()).await)
})
.await
}
async fn put_immutable(
&mut self,
key: &str,
bytes: Vec<u8>,
crc32: &str,
) -> Result<(), BackupError> {
let payload = PutPayload::from(bytes);
match self.put_mode(key, payload.clone(), PutMode::Create).await? {
Ok(_) => Ok(()),
Err(object_store::Error::NotImplemented) => {
self.put_mode(key, payload, PutMode::Overwrite).await??;
Ok(())
}
Err(object_store::Error::AlreadyExists { .. }) => match self.get(key).await? {
Some(b) if crc32_hex(&b) == crc32 => Ok(()),
_ => Err(self.conflict(format!("{key} already exists with other content"))),
},
Err(e) => Err(e.into()),
}
}
async fn put_manifest(&mut self, key: &str, bytes: Vec<u8>) -> Result<bool, BackupError> {
if self.parent_lineage.is_some() && !self.manifest_versions.contains_key(key) {
if let Some((b, _)) = self.get_versioned(key).await? {
if Manifest::from_json(&b).ok().map(|m| m.lineage) == self.parent_lineage {
tracing::info!(
key,
"the parent lineage's restore point holds this key; not written"
);
self.parent_keys.insert(key.to_string());
return Ok(false);
}
}
}
let payload = PutPayload::from(bytes);
let mode = match self.manifest_versions.get(key) {
Some(v) => PutMode::Update(v.clone()),
None => PutMode::Create,
};
let written = match self.put_mode(key, payload.clone(), mode).await? {
Err(object_store::Error::AlreadyExists { .. }) => {
let existing = self.get_versioned(key).await?;
let owner = existing
.as_ref()
.and_then(|(b, _)| Manifest::from_json(b).ok())
.map(|m| m.lineage);
let ours = owner.as_deref() == Some(self.lineage().as_str());
if owner.is_some() && owner == self.parent_lineage {
tracing::info!(
key,
"the parent lineage's restore point holds this key; not written"
);
self.parent_keys.insert(key.to_string());
return Ok(false);
}
match existing {
Some((_, v)) if ours => {
self.put_mode(key, payload.clone(), PutMode::Update(v))
.await?
}
_ => return Err(self.conflict(format!("{key} is another lineage's manifest"))),
}
}
other => other,
};
let r = match written {
Ok(r) => r,
Err(object_store::Error::NotImplemented) => {
self.put_mode(key, payload, PutMode::Overwrite).await??
}
Err(object_store::Error::Precondition { .. })
| Err(object_store::Error::AlreadyExists { .. }) => {
return Err(self.conflict(format!("{key} changed since this worker wrote it")))
}
Err(e) => return Err(e.into()),
};
self.manifest_versions
.insert(key.to_string(), version_of(r));
Ok(true)
}
async fn put_latest(&mut self, target: &str) -> Result<(), BackupError> {
let key = latest_key(&self.prefix);
let payload = PutPayload::from(target.as_bytes().to_vec());
let mode = match &self.latest_version {
Some(v) => PutMode::Update(v.clone()),
None => PutMode::Create,
};
let r = match self.put_mode(&key, payload.clone(), mode).await? {
Ok(r) => r,
Err(object_store::Error::NotImplemented) => {
self.put_mode(&key, payload, PutMode::Overwrite).await??
}
Err(object_store::Error::Precondition { .. })
| Err(object_store::Error::AlreadyExists { .. })
| Err(object_store::Error::NotFound { .. }) => {
return Err(self.conflict("LATEST changed since this worker last wrote it".into()))
}
Err(e) => return Err(e.into()),
};
self.latest_version = Some(version_of(r));
self.latest_target = Some(target.to_string());
Ok(())
}
fn conflict(&mut self, what: String) -> BackupError {
let why = format!("another writer is using this backup prefix: {what}");
tracing::warn!(reason = %why, "backup write refused by the bucket");
if let Some(l) = &self.lease {
l.mark_lost();
self.health.lock().lease = Some(why.clone());
}
BackupError::Conflict(why)
}
async fn put_file(
&mut self,
name: &str,
key: &str,
file: std::fs::File,
count_frames: bool,
) -> Result<FileEntry, BackupError> {
if self.head(key).await? {
return Err(self.conflict(format!("{key} already exists")));
}
self.spend_write()?;
let mut file = tokio::fs::File::from_std(file);
let mut writer = BufWriter::new(Arc::clone(&self.store), ObjectPath::from(key));
let mut hasher = crc32fast::Hasher::new();
let mut counter = FrameCounter::default();
let mut size = 0u64;
let mut buf = vec![0u8; UPLOAD_CHUNK];
let timeout = self.op_timeout();
let lease = self.lease.clone();
let written: Result<(), BackupError> = async {
loop {
let n = file.read(&mut buf).await?;
if n == 0 {
return Ok(());
}
hasher.update(&buf[..n]);
if count_frames {
counter.feed(&buf[..n]);
}
size += n as u64;
fence(lease.as_deref(), async {
tokio::time::timeout(timeout, writer.write_all(&buf[..n]))
.await
.map_err(|_| BackupError::Timeout(format!("upload of {key}")))??;
Ok(())
})
.await?;
}
}
.await;
if let Err(e) = written {
let _ = tokio::time::timeout(timeout, writer.abort()).await;
return Err(e);
}
fence(lease.as_deref(), async {
tokio::time::timeout(timeout, writer.shutdown())
.await
.map_err(|_| BackupError::Timeout(format!("upload of {key}")))??;
Ok(())
})
.await?;
Ok(FileEntry {
name: name.to_string(),
key: key.to_string(),
size,
crc32: format_crc32(hasher.finalize()),
frames: counter.frames(),
lineage: self.lineage(),
})
}
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, BackupError> {
let path = ObjectPath::from(key);
let store = Arc::clone(&self.store);
self.deadline(|| format!("GET {key}"), async move {
match store.get(&path).await {
Ok(r) => Ok(Some(r.bytes().await?.to_vec())),
Err(object_store::Error::NotFound { .. }) => Ok(None),
Err(e) => Err(e.into()),
}
})
.await
}
async fn get_versioned(
&self,
key: &str,
) -> Result<Option<(Vec<u8>, UpdateVersion)>, BackupError> {
let path = ObjectPath::from(key);
let store = Arc::clone(&self.store);
self.deadline(|| format!("GET {key}"), async move {
match store.get(&path).await {
Ok(r) => {
let v = UpdateVersion {
e_tag: r.meta.e_tag.clone(),
version: r.meta.version.clone(),
};
Ok(Some((r.bytes().await?.to_vec(), v)))
}
Err(object_store::Error::NotFound { .. }) => Ok(None),
Err(e) => Err(e.into()),
}
})
.await
}
async fn head(&self, key: &str) -> Result<bool, BackupError> {
let path = ObjectPath::from(key);
let store = Arc::clone(&self.store);
self.deadline(|| format!("HEAD {key}"), async move {
match store.head(&path).await {
Ok(_) => Ok(true),
Err(object_store::Error::NotFound { .. }) => Ok(false),
Err(e) => Err(e.into()),
}
})
.await
}
async fn delete(&mut self, key: &str) -> Result<(), BackupError> {
self.spend_write()?;
let path = ObjectPath::from(key);
let store = Arc::clone(&self.store);
self.fenced(|| format!("DELETE {key}"), async move {
match store.delete(&path).await {
Ok(()) | Err(object_store::Error::NotFound { .. }) => Ok(()),
Err(e) => Err(e.into()),
}
})
.await
}
async fn list(&mut self, dir: &ObjectPath) -> Result<object_store::ListResult, BackupError> {
self.lists += 1;
let store = Arc::clone(&self.store);
let d = dir.clone();
self.deadline(|| format!("LIST {dir}"), async move {
Ok(store.list_with_delimiter(Some(&d)).await?)
})
.await
}
}
async fn position_of(source: &Arc<dyn WalSource>) -> WalPosition {
let s = Arc::clone(source);
match tokio::task::spawn_blocking(move || s.durable_wal_position()).await {
Ok(p) => p,
Err(_) => source.durable_wal_position(),
}
}
async fn until_shutdown<F: Future>(
rx: &mut watch::Receiver<BackupTrigger>,
fut: F,
) -> Option<F::Output> {
tokio::pin!(fut);
loop {
tokio::select! {
out = &mut fut => return Some(out),
changed = rx.changed() => {
if is_shutdown(changed, rx) {
return None;
}
}
}
}
}
fn poll_interval(ttl_s: u64) -> Duration {
Duration::from_millis(ttl_s.max(1) * 1000 / 3)
}
async fn fence<T>(
lease: Option<&BucketLease>,
fut: impl Future<Output = Result<T, BackupError>>,
) -> Result<T, BackupError> {
let Some(lease) = lease else {
return fut.await;
};
let fenced = || {
Err(BackupError::Lease(if lease.is_lost() {
"lost to another instance; this one no longer writes to the bucket".into()
} else {
"not renewed in time; writes are stopped until it is".into()
}))
};
tokio::pin!(fut);
let mut wake: Option<std::pin::Pin<Box<tokio::time::Sleep>>> = None;
std::future::poll_fn(|cx| {
let Some(deadline) = lease.write_deadline() else {
return std::task::Poll::Ready(fenced());
};
if let std::task::Poll::Ready(out) = fut.as_mut().poll(cx) {
return std::task::Poll::Ready(out);
}
let at = deadline.min(Instant::now() + Duration::from_secs(1));
let sleep = wake.get_or_insert_with(|| Box::pin(tokio::time::sleep_until(at)));
if sleep.deadline() != at {
sleep.as_mut().reset(at);
}
if sleep.as_mut().poll(cx).is_ready() {
sleep
.as_mut()
.reset(Instant::now() + Duration::from_millis(1));
cx.waker().wake_by_ref();
}
std::task::Poll::Pending
})
.await
}
async fn renew_loop(
lease: Arc<BucketLease>,
first: Duration,
op_timeout: Duration,
health: Health,
errors: Arc<AtomicU64>,
) {
let every = lease.renew_interval().max(Duration::from_millis(1));
let mut wait = first;
loop {
tokio::time::sleep(wait).await;
let why = match tokio::time::timeout(op_timeout, lease.renew()).await {
Ok(Ok(())) => {
health.lock().lease = None;
health.lease_held.store(true, Ordering::Release);
wait = every;
continue;
}
Ok(Err(e @ LeaseError::Lost { .. })) => {
tracing::warn!(error = %e, "lost the backup bucket lease; uploads stop");
health.lease_held.store(false, Ordering::Release);
health.lock().lease = Some(format!(
"{e}; this instance uploads nothing while another holds it"
));
return;
}
Ok(Err(e)) => e.to_string(),
Err(_) => "the renewal timed out".to_string(),
};
errors.fetch_add(1, Ordering::AcqRel);
tracing::warn!(error = %why, "backup bucket lease renewal failed; retrying");
if !lease.may_write() {
health.lease_held.store(false, Ordering::Release);
health.lock().lease = Some(format!(
"the bucket lease could not be renewed ({why}); uploads are stopped until it is"
));
}
wait = RENEW_RETRY.min(every);
}
}
fn version_of(r: PutResult) -> UpdateVersion {
UpdateVersion {
e_tag: r.e_tag,
version: r.version,
}
}
fn is_shutdown(
changed: Result<(), watch::error::RecvError>,
rx: &mut watch::Receiver<BackupTrigger>,
) -> bool {
match changed {
Err(_) => true,
Ok(()) => *rx.borrow_and_update() == BackupTrigger::Shutdown,
}
}
async fn blocking<T: Send + 'static>(
f: impl FnOnce() -> std::io::Result<T> + Send + 'static,
) -> std::io::Result<T> {
tokio::task::spawn_blocking(f)
.await
.unwrap_or_else(|e| Err(std::io::Error::other(format!("blocking read failed: {e}"))))
}
fn unix_now() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
fn now_rfc3339() -> String {
time::OffsetDateTime::now_utc()
.format(&time::format_description::well_known::Rfc3339)
.unwrap_or_default()
}
#[cfg(test)]
mod tests;