Skip to main content

backup/
worker.rs

1//! The backup task: ships WAL segments, snapshots, archives and side files, and
2//! writes the manifests that name consistent restore points.
3//!
4//! # What it reads, and what it never touches
5//!
6//! It reads the store's files through ordinary OS reads on Tokio's blocking
7//! pool, and one value from the engine: [`WalSource::durable_wal_position`],
8//! which says how much of `wal.bin` is fsynced, in which generation, and which
9//! `snapshot.bin` and `wal.bin` files the engine's state is on. Reading it
10//! takes no engine lock, so it cannot slow a write. The one exception: when
11//! the files on disk show a snapshot or commits the engine has not seen
12//! (another process wrote them), the worker asks the handle to catch up — the
13//! same metadata check and refresh every read makes. It writes one file into
14//! the store directory, once: `backup.lineage`, on the first backup of a store
15//! (see *Lineage*).
16//!
17//! # Consistency
18//!
19//! A restore point is a snapshot plus the WAL that starts beside it, and which
20//! ones those are comes from the files on disk:
21//!
22//! - Nothing is read while `snapshot.intent` holds anything — a snapshot, in
23//!   this process or another, is between writing `snapshot.bin` and replacing
24//!   `wal.bin`. The step is deferred, not failed.
25//! - Every read is bracketed by two looks at the directory, and kept only if
26//!   neither the snapshot, the WAL file, nor the intent changed and the WAL
27//!   did not shrink between them ([`DiskState::unchanged_until`]).
28//! - A restore point is built only when the engine's position names the
29//!   snapshot and WAL file on disk; a new generation starts whenever either
30//!   changes. A snapshot that kept its WAL carries a mark of the prefix it
31//!   holds, and the open of a restored store skips that prefix by itself, so
32//!   shipping the whole WAL from offset 0 beside it is always correct.
33//! - A manifest's `commit_seq` is computed from the objects it lists — the
34//!   `wal.floor` it carries plus the frames of each archive and segment — so
35//!   it is what the restored store reports, by construction.
36//!
37//! # Objects and ordering
38//!
39//! Every object a manifest names is immutable (see [`crate::manifest`]); a
40//! manifest is written only after every object it names exists, and `LATEST`
41//! only after its manifest. Retention deletes only what no retained manifest
42//! names. So at any point — a crash between any two requests included — every
43//! manifest in the bucket verifies, and `LATEST` names one of them.
44//!
45//! # Lineage
46//!
47//! A prefix holds the backups of one store lineage. The first backup of a
48//! store gives it a random lineage id (`backup.lineage`, a side file, so a
49//! store restored from the prefix carries it on). Every manifest and every
50//! object entry names its lineage, and resume checks it before using anything
51//! in the prefix. The worker refuses the prefix — uploads nothing, deletes
52//! nothing, reports unhealthy with the reason — when its newest restore point
53//! is another lineage's, when this store has no lineage but the prefix has
54//! restore points, or when this store holds fewer commits than that restore
55//! point (it was restored from an older backup, or rewound). Archives found
56//! in the prefix are reused only when their number, length and CRC all match
57//! the local file. Retention reads only this lineage's manifests and deletes
58//! only objects of this lineage's generations, never the manifest it just
59//! wrote or the one `LATEST` names, and never an object another lineage's
60//! manifest names.
61//!
62//! One exception lets a store restored on boot ([`crate::boot`]) back up to
63//! the prefix it came from. It has a lineage of its own and a parent record
64//! ([`disk::PARENT_FILE`]) naming the lineage and manifest it was restored
65//! from. While `LATEST` still names exactly that manifest, the worker adopts
66//! the prefix: it writes its own lineage's objects beside the parent's, under
67//! generation numbers past every one in the prefix, moves `LATEST` to its own
68//! manifests, and never overwrites or deletes anything of the parent's. A
69//! manifest key the parent already holds (the same commit count) is not
70//! written; the next restore point, one commit later, is. If `LATEST` has
71//! moved on since the restore, another instance wrote to the prefix: refused
72//! like any other lineage.
73//!
74//! # Writes the bucket can refuse
75//!
76//! Every immutable object (segment, side file) is written with
77//! `If-None-Match: *`; an object already there is accepted only with the same
78//! content. Snapshots and archives (multipart) are checked absent first.
79//! Manifests and `LATEST` are written with `If-Match` against the version
80//! this worker last wrote or read. A refusal means another writer is active:
81//! the worker drops its lease and starts over through the lease. A client
82//! without conditional PUT (`MUSHROOMDB_BACKUP_LEASE_CONDITIONAL=off`) falls
83//! back to plain writes.
84//!
85//! # The bucket lease
86//!
87//! A spawned worker uploads nothing until it holds the prefix's
88//! [`BucketLease`]. While another instance holds a fresh one the worker
89//! uploads nothing, reports itself unhealthy naming the holder, and tries
90//! again every third of a TTL — so a successor waiting on a holder that died,
91//! or on one shutting down in a rolling deploy, takes over within a TTL. A
92//! separate task renews the lease every third of a TTL; every request that
93//! changes the bucket runs only while [`BucketLease::may_write`] holds and is
94//! cancelled when it stops holding. A lease found lost (taken over) is
95//! dropped, and a lease taken again later starts from the bucket's state
96//! afresh: its previous run's view of the bucket is stale. On shutdown, after
97//! the flush, the lease is deleted. The lease is advisory; see
98//! [`crate::lease`] for what it does and does not guarantee.
99//!
100//! [`BackupWorker::run_once`] and [`BackupWorker::step`] take no lease: they
101//! are for tests and callers that hold one themselves.
102//!
103//! # Health
104//!
105//! A supervisor beside the worker re-evaluates health every `wal_interval_ms`,
106//! whatever the worker is doing, and reports a worker that exits or panics as
107//! unhealthy with the reason. A dead backup never looks healthy, and neither
108//! does one kept out of the bucket by its lease.
109
110use crate::disk::{self, DiskState, Ident, LINEAGE_FILE};
111use crate::lease::{new_instance_id, BucketLease, Exclusion, LeaseError, LeaseWatch};
112use crate::manifest::{
113    archive_key, crc32_hex, format_crc32, latest_key, manifest_key, object_key, parse_archive_name,
114    parse_segment_key, parse_versioned_generation, segment_key, side_key, snapshot_key, FileEntry,
115    Manifest, SegmentEntry, MANIFEST_VERSION,
116};
117use crate::segment::{complete_frames, read_new_segment, FrameCounter};
118use crate::store::BackupConfig;
119use crate::BackupError;
120use mushroomdb::{SharedDb, WalPosition};
121use object_store::buffered::BufWriter;
122use object_store::path::Path as ObjectPath;
123use object_store::{ObjectStore, PutMode, PutPayload, PutResult, UpdateVersion};
124use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
125use std::future::Future;
126use std::path::PathBuf;
127use std::pin::Pin;
128use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
129use std::sync::{Arc, Mutex};
130use std::time::{Duration, SystemTime, UNIX_EPOCH};
131use tokio::io::{AsyncReadExt, AsyncWriteExt};
132use tokio::sync::watch;
133use tokio::task::JoinHandle;
134use tokio::time::Instant;
135
136/// The store's small files that travel with every restore point.
137///
138/// `snapshot.intent` is deliberately absent: it describes one machine's
139/// half-finished snapshot and must never be restored onto another (W4 §4).
140pub const SIDE_FILES: [&str; 7] = [
141    LINEAGE_FILE,
142    "commit_times.bin",
143    "roles.json",
144    "keys.json",
145    "wal.genesis",
146    // A V11 store's genesis marker, a name 0.9.x does not read (TY-12).
147    "wal.genesis.v11",
148    "wal.floor",
149];
150
151/// The side files that describe the archive chain. They change only at a
152/// snapshot, and they belong to the generation they were read with: captured
153/// once, when the generation starts, never refreshed under it.
154const CHAIN_FILES: [&str; 3] = ["wal.genesis", "wal.genesis.v11", "wal.floor"];
155
156/// Consecutive failed steps after which the worker reports itself unhealthy.
157pub const UNHEALTHY_AFTER_FAILURES: u32 = 3;
158/// First retry delay after a failure; doubles per failure up to [`MAX_BACKOFF`].
159pub const FIRST_BACKOFF: Duration = Duration::from_secs(1);
160/// Longest retry delay.
161pub const MAX_BACKOFF: Duration = Duration::from_secs(60);
162/// The shortest lag that makes the worker unhealthy: durable data unshipped
163/// for longer than `max(MIN_UNHEALTHY_LAG, 10 × wal_interval)`.
164pub const MIN_UNHEALTHY_LAG: Duration = Duration::from_secs(30);
165/// Retry delay after a failed lease renewal (capped at the renewal interval).
166pub const RENEW_RETRY: Duration = Duration::from_secs(1);
167/// Read size when streaming a snapshot or archive.
168const UPLOAD_CHUNK: usize = 8 * 1024 * 1024;
169
170/// Where the worker learns how much of the WAL is durable.
171///
172/// [`SharedDb`] is the real one. The trait exists so the worker does not care
173/// how the handle is held.
174pub trait WalSource: Send + Sync + 'static {
175    fn durable_wal_position(&self) -> WalPosition;
176
177    /// Absorb whatever another process committed or snapshotted. Called, on a
178    /// blocking thread, only when the files on disk differ from the position
179    /// and no snapshot is in progress.
180    fn catch_up(&self) {}
181}
182
183impl WalSource for SharedDb {
184    fn durable_wal_position(&self) -> WalPosition {
185        SharedDb::durable_wal_position(self)
186    }
187
188    fn catch_up(&self) {
189        self.refresh_if_stale_now();
190    }
191}
192
193/// What the serve loop tells the worker, over a `watch` channel.
194///
195/// A `watch` keeps only the latest value, so a `Snapshot` followed quickly by
196/// a `Shutdown` is seen as `Shutdown` alone. That is safe: the worker finds a
197/// new snapshot by itself (the files on disk change), and its shutdown flush
198/// uploads it. `Snapshot` only makes that happen now instead of at the next
199/// tick.
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
201pub enum BackupTrigger {
202    /// The channel's initial value; means nothing.
203    #[default]
204    Idle,
205    /// A snapshot finished: upload it now.
206    Snapshot,
207    /// Flush what is pending, write a final manifest, and stop — within
208    /// `shutdown_grace_ms`, abandoning an upload still in flight then.
209    Shutdown,
210}
211
212/// The running worker, as the serve loop holds it.
213///
214/// There is one way to stop it: send [`BackupTrigger::Shutdown`] (or drop the
215/// sender), then await `join`. It finishes within `shutdown_grace_ms`, however
216/// unresponsive the bucket is.
217#[derive(Debug)]
218pub struct BackupHandle {
219    /// Failed steps since start (`mushroomdb_backup_upload_errors_total`).
220    pub error_count: Arc<AtomicU64>,
221    /// Unix seconds of the last moment everything durable was shipped; 0
222    /// before the first.
223    pub last_success_ts: Arc<AtomicU64>,
224    /// False after [`UNHEALTHY_AFTER_FAILURES`] consecutive failures, while
225    /// durable data has stayed unshipped for longer than
226    /// `max(30 s, 10 × wal_interval)`, or for good once the worker has
227    /// stopped; true otherwise.
228    pub healthy: Arc<AtomicBool>,
229    /// `backup_lag_s`: seconds the oldest durable, unshipped WAL byte has
230    /// waited; 0 when everything durable is shipped.
231    pub lag_s: Arc<AtomicU64>,
232    /// Whether this worker holds the bucket lease right now: false while
233    /// another instance holds it, after the lease is lost or cannot be
234    /// renewed, and once the worker has released it.
235    pub lease_held: Arc<AtomicBool>,
236    /// Why the worker is unhealthy, while it is; once it has stopped, why it
237    /// stopped (`"stopped"` after a shutdown, the panic message after a panic).
238    pub reason: Arc<Mutex<Option<String>>>,
239    /// Finishes when the worker has flushed and stopped.
240    pub join: JoinHandle<()>,
241}
242
243/// The worker's counters, shared with whoever reports them.
244#[derive(Debug, Clone)]
245pub struct BackupCounters {
246    pub error_count: Arc<AtomicU64>,
247    pub last_success_ts: Arc<AtomicU64>,
248    pub healthy: Arc<AtomicBool>,
249    pub lag_s: Arc<AtomicU64>,
250    pub lease_held: Arc<AtomicBool>,
251    pub reason: Arc<Mutex<Option<String>>>,
252}
253
254/// Which WAL file a generation ships, as the engine's position names it.
255#[derive(Debug, Clone, Copy, PartialEq, Eq)]
256struct WalKey {
257    engine: u64,
258    snapshot: Option<Ident>,
259    wal: Option<u64>,
260}
261
262impl WalKey {
263    fn of(pos: &WalPosition) -> Self {
264        Self {
265            engine: pos.generation,
266            snapshot: pos.snapshot,
267            wal: pos.wal,
268        }
269    }
270}
271
272/// The WAL generation being shipped.
273#[derive(Debug, Clone)]
274struct Generation {
275    /// Its number in the bucket (`wal_generation`).
276    number: u64,
277    key: WalKey,
278    /// Bytes of `wal.bin` shipped so far.
279    cursor: u64,
280    /// The store's horizon floor when the generation started.
281    floor: u64,
282    /// The chain side files, as they were when the generation started.
283    chain: Vec<FileEntry>,
284    segments: Vec<SegmentEntry>,
285}
286
287/// What the health evaluation reads, shared by the worker and its supervisor.
288#[derive(Debug, Default)]
289struct HealthState {
290    /// What has been shipped: which WAL, and up to where.
291    shipped: Option<(WalKey, u64)>,
292    consecutive_failures: u32,
293    /// Why the last step did nothing, when it deferred.
294    deferral: Option<String>,
295    /// Durable lengths seen and when first seen, for the WAL being shipped.
296    seen: VecDeque<(u64, Instant)>,
297    seen_key: Option<WalKey>,
298    /// Since when a new restore point has been due and not made.
299    base_due_since: Option<Instant>,
300    /// Why the bucket lease keeps this worker from uploading, while it does.
301    lease: Option<String>,
302    /// Why this worker refused the prefix (another lineage, or a rewound
303    /// store). Permanent for the life of the worker.
304    lineage: Option<String>,
305    stopped: Option<String>,
306}
307
308/// Lag and health, computed from the engine's position and what has shipped.
309#[derive(Clone)]
310struct Health {
311    state: Arc<Mutex<HealthState>>,
312    healthy: Arc<AtomicBool>,
313    lag_s: Arc<AtomicU64>,
314    last_success_ts: Arc<AtomicU64>,
315    lease_held: Arc<AtomicBool>,
316    reason: Arc<Mutex<Option<String>>>,
317    limit: Duration,
318}
319
320impl Health {
321    fn lock(&self) -> std::sync::MutexGuard<'_, HealthState> {
322        self.state.lock().unwrap_or_else(|e| e.into_inner())
323    }
324
325    /// Re-evaluate at `now` against the durable position `pos`.
326    ///
327    /// The lag is measured from the first moment a durable length beyond what
328    /// has shipped was seen — evaluations run every cadence, so that is at
329    /// most one cadence after the fsync — or, when a new restore point is
330    /// due, from when that was first seen.
331    fn observe(&self, pos: &WalPosition, now: Instant) {
332        let mut s = self.lock();
333        let key = WalKey::of(pos);
334        let lag = match s.shipped {
335            Some((k, cursor)) if k == key => {
336                s.base_due_since = None;
337                if s.seen_key != Some(key) {
338                    s.seen.clear();
339                    s.seen_key = Some(key);
340                }
341                if pos.len > s.seen.back().map_or(cursor, |(l, _)| *l) {
342                    s.seen.push_back((pos.len, now));
343                }
344                while s.seen.front().is_some_and(|(l, _)| *l <= cursor) {
345                    s.seen.pop_front();
346                }
347                s.seen
348                    .front()
349                    .map(|(_, t)| now.saturating_duration_since(*t))
350            }
351            _ => {
352                let since = *s.base_due_since.get_or_insert(now);
353                Some(now.saturating_duration_since(since))
354            }
355        };
356        let lag = lag.unwrap_or(Duration::ZERO);
357        if lag.is_zero() {
358            self.last_success_ts.store(unix_now(), Ordering::Release);
359        }
360        self.lag_s.store(lag.as_secs(), Ordering::Release);
361        let reason = if let Some(r) = &s.stopped {
362            Some(r.clone())
363        } else if let Some(r) = &s.lineage {
364            Some(r.clone())
365        } else if let Some(r) = &s.lease {
366            Some(r.clone())
367        } else if s.consecutive_failures >= UNHEALTHY_AFTER_FAILURES {
368            Some(format!(
369                "{} consecutive failed backup steps",
370                s.consecutive_failures
371            ))
372        } else if lag > self.limit {
373            Some(match &s.deferral {
374                Some(d) => format!("durable data unshipped for {}s: {d}", lag.as_secs()),
375                None => format!("durable data unshipped for {}s", lag.as_secs()),
376            })
377        } else {
378            None
379        };
380        self.healthy.store(reason.is_none(), Ordering::Release);
381        *self.reason.lock().unwrap_or_else(|e| e.into_inner()) = reason;
382    }
383
384    /// The worker is gone, for `why`. Unhealthy from now on.
385    fn stop(&self, why: String) {
386        self.lock().stopped = Some(why.clone());
387        self.healthy.store(false, Ordering::Release);
388        self.lease_held.store(false, Ordering::Release);
389        *self.reason.lock().unwrap_or_else(|e| e.into_inner()) = Some(why);
390    }
391}
392
393/// The backup worker. [`BackupWorker::spawn`] runs it as a task; tests drive
394/// it a step at a time with [`run_once`](BackupWorker::run_once).
395pub struct BackupWorker {
396    config: BackupConfig,
397    store: Arc<dyn ObjectStore>,
398    source: Arc<dyn WalSource>,
399    dir: PathBuf,
400    prefix: String,
401    error_count: Arc<AtomicU64>,
402    health: Health,
403    backoff: Duration,
404    /// Whether the bucket's previous state has been read.
405    resumed: bool,
406    next_generation: u64,
407    /// Every generation number with objects in the bucket that retention has
408    /// not retired yet.
409    generations: BTreeSet<u64>,
410    current: Option<Generation>,
411    /// The snapshot the current generation starts beside.
412    snapshot: Option<FileEntry>,
413    /// Archives in the bucket, by number, with the local length they were
414    /// uploaded from. An archive is immutable: never uploaded twice. Entries
415    /// seeded from the bucket at resume are reused only once the local
416    /// file's CRC matches.
417    archives: BTreeMap<u64, Archive>,
418    /// Archives the current restore point names.
419    base_archives: Vec<u64>,
420    /// Side files refreshed at every manifest, by name, with the local
421    /// identity they came from.
422    side: BTreeMap<&'static str, (Ident, FileEntry)>,
423    manifest_dirty: bool,
424    last_manifest_at: Option<Instant>,
425    /// Test hook: requests that change the bucket still allowed before the
426    /// worker behaves as if killed. See [`BackupWorker::fail_after_writes`].
427    write_budget: Option<u64>,
428    /// Test hook: panic at the start of the next step.
429    panic_next: bool,
430    /// LIST requests made, for the retention-cost test.
431    lists: u64,
432    /// The bucket lease, while this worker holds it (spawned workers only).
433    lease: Option<Arc<BucketLease>>,
434    /// The task renewing `lease`.
435    renewer: Option<JoinHandle<()>>,
436    /// Leases taken so far; a second one starts from the bucket afresh.
437    tenures: u64,
438    /// The foreign holder last logged, so a wait is logged once.
439    foreign: Option<String>,
440    /// This worker's instance id, the same across every lease attempt.
441    instance_id: String,
442    /// What lease attempts have seen of a suspect foreign lease.
443    watch: LeaseWatch,
444    /// Owned across cancellation of a waiter: a remote acquisition PUT may
445    /// already have landed while its capability probes are still pending.
446    pending_lease: Option<PendingLease>,
447    /// The store's lineage, once resume has established it.
448    lineage: Option<String>,
449    /// The lineage this store was restored on boot from, when it was: its
450    /// manifests are never overwritten, and a key one holds is passed over.
451    parent_lineage: Option<String>,
452    /// Manifest keys found holding the parent lineage's manifest, not
453    /// written again.
454    parent_keys: HashSet<String>,
455    /// Set when resume refused the prefix: the worker uploads nothing more.
456    refused: bool,
457    /// The version of `LATEST` last written or read, for `If-Match`; `None`
458    /// when it does not exist.
459    latest_version: Option<UpdateVersion>,
460    /// The manifest `LATEST` names.
461    latest_target: Option<String>,
462    /// Versions of the manifests this tenure wrote, for `If-Match`.
463    manifest_versions: HashMap<String, UpdateVersion>,
464    /// Generation numbers this tenure allocated.
465    allocated: BTreeSet<u64>,
466}
467
468type LeaseAttempt = Result<Result<BucketLease, LeaseError>, tokio::time::error::Elapsed>;
469// The worker's other async helpers borrow &self, so the worker must remain
470// Sync. Exclusive get_mut accesses the future; no mutex guard crosses await.
471type PendingLease = Mutex<Pin<Box<dyn Future<Output = (LeaseWatch, LeaseAttempt)> + Send>>>;
472
473/// An archive known to be in the bucket.
474#[derive(Debug, Clone)]
475struct Archive {
476    /// Length of the local file it was uploaded from.
477    len: u64,
478    entry: FileEntry,
479    /// Whether the local file is known to have these bytes: uploaded by this
480    /// tenure, or CRC-checked against the entry.
481    verified: bool,
482}
483
484impl BackupWorker {
485    /// A worker for the store in `store_dir`, uploading to `store` under the
486    /// prefix `config.s3_url` names.
487    pub fn new(
488        config: BackupConfig,
489        store: Arc<dyn ObjectStore>,
490        source: Arc<dyn WalSource>,
491        store_dir: PathBuf,
492    ) -> Result<Self, BackupError> {
493        let prefix = config.location()?.prefix;
494        let limit = MIN_UNHEALTHY_LAG.max(Duration::from_millis(config.wal_interval_ms) * 10);
495        Ok(Self {
496            config,
497            store,
498            source,
499            dir: store_dir,
500            prefix,
501            error_count: Arc::new(AtomicU64::new(0)),
502            health: Health {
503                state: Arc::default(),
504                healthy: Arc::new(AtomicBool::new(true)),
505                lag_s: Arc::new(AtomicU64::new(0)),
506                last_success_ts: Arc::new(AtomicU64::new(0)),
507                lease_held: Arc::new(AtomicBool::new(false)),
508                reason: Arc::default(),
509                limit,
510            },
511            backoff: FIRST_BACKOFF,
512            resumed: false,
513            next_generation: 0,
514            generations: BTreeSet::new(),
515            current: None,
516            snapshot: None,
517            archives: BTreeMap::new(),
518            base_archives: Vec::new(),
519            side: BTreeMap::new(),
520            manifest_dirty: false,
521            last_manifest_at: None,
522            write_budget: None,
523            panic_next: false,
524            lists: 0,
525            lease: None,
526            renewer: None,
527            tenures: 0,
528            foreign: None,
529            instance_id: new_instance_id(),
530            watch: LeaseWatch::default(),
531            pending_lease: None,
532            lineage: None,
533            parent_lineage: None,
534            parent_keys: HashSet::new(),
535            refused: false,
536            latest_version: None,
537            latest_target: None,
538            manifest_versions: HashMap::new(),
539            allocated: BTreeSet::new(),
540        })
541    }
542
543    /// Start the worker as a Tokio task. It takes the bucket lease, then runs
544    /// one step at once — reading the bucket's previous state and uploading a
545    /// base restore point — then one every `wal_interval_ms`, and one
546    /// immediately on each trigger. While another instance holds the lease it
547    /// uploads nothing and is unhealthy (see [the module docs](self)).
548    ///
549    /// # Errors
550    ///
551    /// Only a malformed `config.s3_url`.
552    pub fn spawn(
553        config: BackupConfig,
554        store: Arc<dyn ObjectStore>,
555        source: Arc<dyn WalSource>,
556        store_dir: PathBuf,
557        snapshot_rx: watch::Receiver<BackupTrigger>,
558    ) -> Result<BackupHandle, BackupError> {
559        Ok(Self::new(config, store, source, store_dir)?.start(snapshot_rx))
560    }
561
562    /// Run this worker as a Tokio task, beside a supervisor that evaluates
563    /// health every `wal_interval_ms` and reports the worker's end — a
564    /// shutdown, or a panic — as unhealthy with the reason.
565    pub fn start(self, snapshot_rx: watch::Receiver<BackupTrigger>) -> BackupHandle {
566        let BackupCounters {
567            error_count,
568            last_success_ts,
569            healthy,
570            lag_s,
571            lease_held,
572            reason,
573        } = self.counters();
574        let health = self.health.clone();
575        let source = Arc::clone(&self.source);
576        let cadence = Duration::from_millis(self.config.wal_interval_ms);
577        let worker = tokio::spawn(self.run(snapshot_rx));
578        let join = tokio::spawn(async move {
579            let mut tick = tokio::time::interval(cadence);
580            tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
581            tokio::pin!(worker);
582            let ended = loop {
583                tokio::select! {
584                    ended = &mut worker => break ended,
585                    _ = tick.tick() => {
586                        let pos = position_of(&source).await;
587                        health.observe(&pos, Instant::now());
588                    }
589                }
590            };
591            let why = match ended {
592                Ok(()) => "stopped".to_string(),
593                Err(e) if e.is_panic() => {
594                    let p = e.into_panic();
595                    let msg = p
596                        .downcast_ref::<&str>()
597                        .map(|s| s.to_string())
598                        .or_else(|| p.downcast_ref::<String>().cloned())
599                        .unwrap_or_else(|| "a non-string panic".into());
600                    tracing::error!(panic = %msg, "backup worker panicked; backup has stopped");
601                    format!("backup worker panicked: {msg}")
602                }
603                Err(e) => format!("backup worker ended: {e}"),
604            };
605            health.stop(why);
606        });
607        BackupHandle {
608            error_count,
609            last_success_ts,
610            healthy,
611            lag_s,
612            lease_held,
613            reason,
614            join,
615        }
616    }
617
618    /// The counters this worker updates (the same ones its
619    /// [`BackupHandle`] carries).
620    pub fn counters(&self) -> BackupCounters {
621        BackupCounters {
622            error_count: Arc::clone(&self.error_count),
623            last_success_ts: Arc::clone(&self.health.last_success_ts),
624            healthy: Arc::clone(&self.health.healthy),
625            lag_s: Arc::clone(&self.health.lag_s),
626            lease_held: Arc::clone(&self.health.lease_held),
627            reason: Arc::clone(&self.health.reason),
628        }
629    }
630
631    /// The instance id this worker takes the bucket lease under, the same
632    /// for every attempt.
633    pub fn instance_id(&self) -> &str {
634        &self.instance_id
635    }
636
637    /// Hold `lease`, taken for this prefix by someone else in this process (a
638    /// restore on boot), instead of taking one: the worker renews it at once
639    /// and from then on, and its instance id becomes the lease's. Call before
640    /// [`start`](Self::start), inside a Tokio runtime.
641    pub fn adopt_lease(&mut self, lease: Arc<BucketLease>) {
642        self.instance_id = lease.instance_id().to_string();
643        self.hold(lease, Duration::ZERO);
644    }
645
646    /// Test hook: allow `n` more requests that change the bucket (PUTs and
647    /// DELETEs), then fail every one as if the process had been killed just
648    /// before it. Drop the worker after the first failure to model a kill
649    /// between two requests.
650    #[doc(hidden)]
651    pub fn fail_after_writes(&mut self, n: u64) {
652        self.write_budget = Some(n);
653    }
654
655    /// Test hook: panic at the start of the next step.
656    #[doc(hidden)]
657    pub fn panic_on_next_step(&mut self) {
658        self.panic_next = true;
659    }
660
661    /// Test hook: LIST requests made so far.
662    #[doc(hidden)]
663    pub fn list_requests(&self) -> u64 {
664        self.lists
665    }
666
667    async fn run(mut self, mut rx: watch::Receiver<BackupTrigger>) {
668        #[cfg(test)]
669        let probe_control = self.watch.probe_control.clone();
670        let mut delay = Duration::ZERO;
671        loop {
672            let shutdown = tokio::select! {
673                _ = tokio::time::sleep(delay) => false,
674                changed = rx.changed() => is_shutdown(changed, &mut rx),
675            };
676            // A ready timer can win the select while Shutdown (or sender
677            // closure) is already observable. Do not start another attempt.
678            if shutdown || *rx.borrow() == BackupTrigger::Shutdown || rx.has_changed().is_err() {
679                break;
680            }
681            // Nothing is uploaded without the bucket lease.
682            if !self.holds_lease() {
683                match until_shutdown(&mut rx, self.take_lease()).await {
684                    None => break,
685                    Some(Some(wait)) => {
686                        delay = wait;
687                        continue;
688                    }
689                    Some(None) => {}
690                }
691            }
692            // A step blocked on an unresponsive bucket must not hold up a
693            // shutdown: the step is dropped — its in-flight request abandoned
694            // — the moment one arrives.
695            match until_shutdown(&mut rx, self.step()).await {
696                Some(d) => delay = d,
697                None => break,
698            }
699            if self.refused {
700                break;
701            }
702        }
703        #[cfg(test)]
704        if let Some(control) = &probe_control {
705            control.shutdown.notify_one();
706        }
707        if self.refused {
708            // The prefix is not ours: give the lease back so its owner is not
709            // kept out, and idle, unhealthy, until shutdown.
710            self.release_lease().await;
711            until_shutdown(&mut rx, std::future::pending::<()>()).await;
712            return;
713        }
714        // Completing an already-started acquire and flushing share one grace
715        // window. Shutdown never starts another acquisition or retries one.
716        let grace = Duration::from_millis(self.config.shutdown_grace_ms);
717        let flush_deadline = Instant::now() + grace;
718        if self.pending_lease.is_some()
719            && tokio::time::timeout_at(flush_deadline, self.finish_lease_attempt(true))
720                .await
721                .is_err()
722        {
723            self.error_count.fetch_add(1, Ordering::AcqRel);
724            tracing::warn!("backup lease acquisition exceeded the shutdown grace; canceled");
725        }
726        // Ordinary drop cancels the owned future before owner-ID cleanup;
727        // there is no detached task left that can send another local request.
728        self.pending_lease = None;
729        if self.holds_lease()
730            && Instant::now() < flush_deadline
731            && tokio::time::timeout_at(flush_deadline, self.flush())
732                .await
733                .is_err()
734        {
735            self.error_count.fetch_add(1, Ordering::AcqRel);
736            tracing::warn!(
737                grace_ms = self.config.shutdown_grace_ms,
738                "backup flush did not finish within the shutdown grace; abandoned"
739            );
740        }
741        self.release_lease().await;
742    }
743
744    // ── The bucket lease ────────────────────────────────────────────────────
745
746    fn holds_lease(&self) -> bool {
747        self.lease.as_ref().is_some_and(|l| !l.is_lost())
748    }
749
750    /// Try to take the bucket lease. `None` once it is held; otherwise how
751    /// long to wait before trying again.
752    async fn take_lease(&mut self) -> Option<Duration> {
753        if self.lease.as_ref().is_some_and(|l| l.is_lost()) {
754            if let Some(r) = self.renewer.take() {
755                r.abort();
756            }
757            self.lease = None;
758            self.health.lease_held.store(false, Ordering::Release);
759        }
760        if self.pending_lease.is_none() {
761            let store = Arc::clone(&self.store);
762            let prefix = self.prefix.clone();
763            let instance_id = self.instance_id.clone();
764            let ttl_s = self.config.lease_ttl_s;
765            let mut watch = std::mem::take(&mut self.watch);
766            // The original limit begins on the attempt's first poll and is
767            // retained unchanged if shutdown takes over its waiter.
768            let limit = self.op_timeout() * 4;
769            self.pending_lease = Some(Mutex::new(Box::pin(async move {
770                let attempt =
771                    BucketLease::acquire_as(store, &prefix, ttl_s, &instance_id, &mut watch);
772                let outcome = tokio::time::timeout(limit, attempt).await;
773                (watch, outcome)
774            })));
775        }
776        let wait = self.finish_lease_attempt(false).await;
777        let pos = self.position().await;
778        self.health.observe(&pos, Instant::now());
779        wait
780    }
781
782    /// Await the existing attempt by borrow so canceling this waiter retains
783    /// its requests, timeout and suspect-record observation state.
784    async fn finish_lease_attempt(&mut self, shutting_down: bool) -> Option<Duration> {
785        let (watch, outcome) = self
786            .pending_lease
787            .as_mut()?
788            .get_mut()
789            .unwrap_or_else(|error| error.into_inner())
790            .await;
791        self.pending_lease = None;
792        self.watch = watch;
793        match outcome {
794            Ok(Ok(lease)) => {
795                let first = if shutting_down {
796                    Duration::ZERO
797                } else {
798                    lease.renew_interval()
799                };
800                self.hold(Arc::new(lease), first);
801                None
802            }
803            Ok(Err(LeaseError::ForeignActive { instance_id })) => {
804                if self.foreign.as_deref() != Some(instance_id.as_str()) {
805                    tracing::warn!(
806                        holder = %instance_id,
807                        "the backup bucket's lease is held by another instance; \
808                         this one uploads nothing while that lease is fresh"
809                    );
810                    self.foreign = Some(instance_id.clone());
811                }
812                self.health.lease_held.store(false, Ordering::Release);
813                self.health.lock().lease = Some(format!(
814                    "the backup bucket's lease is held by another instance ({instance_id}); \
815                     this instance uploads nothing while it is"
816                ));
817                Some(poll_interval(self.config.lease_ttl_s))
818            }
819            Ok(Err(e @ LeaseError::PreconditionsIgnored { .. })) => {
820                tracing::error!(error = %e, "backup is not started");
821                self.health.lease_held.store(false, Ordering::Release);
822                self.health.lock().lease = Some(e.to_string());
823                Some(MAX_BACKOFF)
824            }
825            Ok(Err(LeaseError::Store(e))) if self.config.lease_conditional => {
826                let why = format!(
827                    "taking the bucket lease: {} (if this service rejects conditional PUT \
828                     headers, set MUSHROOMDB_BACKUP_LEASE_CONDITIONAL=off)",
829                    crate::redact(&e.to_string())
830                );
831                Some(self.failed(&BackupError::Lease(why)))
832            }
833            Ok(Err(e)) => Some(self.failed(&BackupError::Lease(e.to_string()))),
834            Err(_) => Some(self.failed(&BackupError::Timeout("taking the bucket lease".into()))),
835        }
836    }
837
838    /// Hold `lease`: start renewing it, and forget a previous tenure's view
839    /// of the bucket.
840    /// The first renewal comes after `first`; then every third of a TTL.
841    fn hold(&mut self, lease: Arc<BucketLease>, first: Duration) {
842        if self.tenures > 0 {
843            self.reset_tenure();
844        }
845        self.tenures += 1;
846        self.foreign = None;
847        self.health.lock().lease = None;
848        self.health.lease_held.store(true, Ordering::Release);
849        match lease.exclusion() {
850            Exclusion::Conditional => tracing::info!(
851                instance_id = lease.instance_id(),
852                "took the backup bucket lease (conditional writes: one writer at a time)"
853            ),
854            Exclusion::BestEffort => tracing::warn!(
855                instance_id = lease.instance_id(),
856                "took the backup bucket lease BEST-EFFORT: conditional writes are off, so two \
857                 instances started together may both upload and overwrite each other's objects"
858            ),
859        }
860        self.renewer = Some(tokio::spawn(renew_loop(
861            Arc::clone(&lease),
862            first,
863            self.op_timeout(),
864            self.health.clone(),
865            Arc::clone(&self.error_count),
866        )));
867        self.lease = Some(lease);
868    }
869
870    /// Forget everything known about the bucket: another writer may have
871    /// changed it since this worker last held the lease.
872    fn reset_tenure(&mut self) {
873        self.resumed = false;
874        self.lineage = None;
875        self.parent_lineage = None;
876        self.parent_keys.clear();
877        self.latest_version = None;
878        self.latest_target = None;
879        self.manifest_versions.clear();
880        self.allocated.clear();
881        self.next_generation = 0;
882        self.generations.clear();
883        self.current = None;
884        self.snapshot = None;
885        self.archives.clear();
886        self.base_archives.clear();
887        self.side.clear();
888        self.manifest_dirty = false;
889        self.last_manifest_at = None;
890        self.health.lock().shipped = None;
891    }
892
893    /// Stop renewing and delete the lease, if this worker still holds it.
894    async fn release_lease(&mut self) {
895        self.pending_lease = None;
896        self.health.lease_held.store(false, Ordering::Release);
897        let deadline = Instant::now() + self.op_timeout();
898        let release = async {
899            if let Some(renewer) = self.renewer.take() {
900                renewer.abort();
901                let _ = renewer.await;
902            }
903            if let Some(lease) = self.lease.take() {
904                // A known lost lease keeps the existing no-release rule.
905                if lease.is_lost() {
906                    return Ok(());
907                }
908                if let Ok(lease) = Arc::try_unwrap(lease) {
909                    return lease.release().await;
910                }
911            }
912            // An acquisition that failed, timed out or was canceled can have
913            // left an own record without a handle. Never acquire it again.
914            crate::lease::release_own_record(
915                &self.store,
916                &ObjectPath::from(crate::lease::lease_key(&self.prefix)),
917                &self.instance_id,
918            )
919            .await
920        };
921        match tokio::time::timeout_at(deadline, release).await {
922            Ok(Ok(())) => tracing::info!("released the backup bucket lease"),
923            Ok(Err(e)) => {
924                tracing::warn!(error = %e, "could not release the backup bucket lease; it expires by itself")
925            }
926            Err(_) => {
927                tracing::warn!("releasing the backup bucket lease timed out; it expires by itself")
928            }
929        }
930    }
931
932    /// One step, with its outcome recorded in the counters. Returns how long
933    /// to wait before the next one: the WAL interval after a success or a
934    /// deferral, the back-off after a failure.
935    pub async fn step(&mut self) -> Duration {
936        let result = self.run_once().await;
937        let delay = match result {
938            Ok(()) => {
939                self.health.lock().consecutive_failures = 0;
940                self.backoff = FIRST_BACKOFF;
941                Duration::from_millis(self.config.wal_interval_ms)
942            }
943            Err(e) => self.failed(&e),
944        };
945        let pos = self.position().await;
946        self.health.observe(&pos, Instant::now());
947        delay
948    }
949
950    /// Count a failed step or lease attempt; return the back-off to wait.
951    fn failed(&mut self, e: &BackupError) -> Duration {
952        #[cfg(test)]
953        if let Some(control) = &self.watch.probe_control {
954            control.failed.notify_one();
955        }
956        self.error_count.fetch_add(1, Ordering::AcqRel);
957        let failures = {
958            let mut s = self.health.lock();
959            s.consecutive_failures += 1;
960            s.consecutive_failures
961        };
962        let delay = self.backoff;
963        self.backoff = (self.backoff * 2).min(MAX_BACKOFF);
964        tracing::warn!(
965            error = %e,
966            consecutive_failures = failures,
967            retry_in_s = delay.as_secs(),
968            "backup step failed"
969        );
970        delay
971    }
972
973    /// Ship pending WAL bytes and write a final manifest, retrying a failure
974    /// twice. Used on shutdown, under the shutdown grace.
975    pub async fn flush(&mut self) {
976        for attempt in 0..3u32 {
977            let done = match self.run_once().await {
978                Ok(()) => self.write_manifest().await,
979                other => other,
980            };
981            match done {
982                Ok(()) => {
983                    let pos = self.position().await;
984                    self.health.observe(&pos, Instant::now());
985                    return;
986                }
987                Err(e) => {
988                    self.error_count.fetch_add(1, Ordering::AcqRel);
989                    tracing::warn!(error = %e, attempt, "backup flush failed");
990                    if attempt < 2 {
991                        tokio::time::sleep(FIRST_BACKOFF * (1 << attempt)).await;
992                    }
993                }
994            }
995        }
996    }
997
998    /// Record why this step did nothing, and do nothing. Not a failure.
999    fn defer(&mut self, why: &str) -> Result<(), BackupError> {
1000        self.health.lock().deferral = Some(why.to_string());
1001        Ok(())
1002    }
1003
1004    /// One step: read the bucket's previous state if not done yet; upload a
1005    /// new restore point when the snapshot or WAL file on disk, or the
1006    /// engine's WAL generation, changed; ship the WAL bytes fsynced since the
1007    /// last step; write a manifest when one is due.
1008    pub async fn run_once(&mut self) -> Result<(), BackupError> {
1009        if std::mem::take(&mut self.panic_next) {
1010            panic!("test hook: backup worker panics");
1011        }
1012        if !self.resumed && !self.resume().await? {
1013            return self.defer("the store's commit count is not known yet");
1014        }
1015        let mut disk = self.observe().await?;
1016        if disk.intent_nonempty {
1017            return self.defer("a snapshot is in progress (snapshot.intent is not empty)");
1018        }
1019        let mut pos = self.position().await;
1020        // The disk differs from the handle: another process snapshotted,
1021        // replaced or extended the WAL. Let the handle catch up — the intent
1022        // was empty just now — and keep the result only if the disk held
1023        // still across it.
1024        if pos.snapshot != disk.snapshot || pos.wal != disk.wal || disk.wal_len != pos.len {
1025            self.catch_up().await?;
1026            let after = self.observe().await?;
1027            if !disk.unchanged_until(&after) {
1028                return self.defer("the store's files changed while catching up");
1029            }
1030            disk = after;
1031            pos = self.position().await;
1032        }
1033        if pos.snapshot_in_progress {
1034            return self.defer("this handle is taking a snapshot");
1035        }
1036        if pos.snapshot != disk.snapshot || pos.wal != disk.wal || disk.wal_len < pos.len {
1037            return self.defer("the handle has not caught up with the files on disk");
1038        }
1039        self.health.lock().deferral = None;
1040        if self.current.as_ref().map(|g| g.key) != Some(WalKey::of(&pos)) {
1041            return self.upload_base(pos, disk).await;
1042        }
1043        self.ship_segments().await?;
1044        if self.manifest_due() {
1045            self.write_manifest().await?;
1046        }
1047        Ok(())
1048    }
1049
1050    fn manifest_due(&self) -> bool {
1051        let interval = Duration::from_secs(self.config.manifest_interval_s);
1052        self.manifest_dirty
1053            && self
1054                .last_manifest_at
1055                .is_none_or(|t| t.elapsed() >= interval)
1056    }
1057
1058    async fn observe(&self) -> Result<DiskState, BackupError> {
1059        let dir = self.dir.clone();
1060        Ok(blocking(move || disk::observe(&dir)).await?)
1061    }
1062
1063    async fn position(&self) -> WalPosition {
1064        position_of(&self.source).await
1065    }
1066
1067    async fn catch_up(&self) -> Result<(), BackupError> {
1068        let source = Arc::clone(&self.source);
1069        blocking(move || {
1070            source.catch_up();
1071            Ok(())
1072        })
1073        .await?;
1074        Ok(())
1075    }
1076
1077    // ── Resume ──────────────────────────────────────────────────────────────
1078
1079    /// Read what a previous run left in the bucket, so this run never reuses a
1080    /// generation number and never re-uploads an archive the latest manifest
1081    /// names; and check that the prefix is this store's lineage. `Ok(false)`:
1082    /// not yet decidable, try again.
1083    ///
1084    /// # Errors
1085    ///
1086    /// [`BackupError::Lineage`] when the prefix belongs to another lineage or
1087    /// this store was rewound: the worker then refuses the prefix for good.
1088    async fn resume(&mut self) -> Result<bool, BackupError> {
1089        let latest = self.latest_manifest().await?;
1090        let dir = self.dir.clone();
1091        let (local, parent) =
1092            blocking(move || Ok((disk::read_lineage(&dir)?, disk::read_parent(&dir)?))).await?;
1093        // A store restored on boot from exactly the restore point `LATEST`
1094        // names continues the prefix (see the module docs).
1095        let adopt = match (&local, &latest, &parent) {
1096            (Some(ours), Some(m), Some(p)) if *ours != m.lineage && p.lineage == m.lineage => {
1097                if self.latest_target.as_deref() != Some(p.manifest.as_str()) {
1098                    return Err(self.refuse(format!(
1099                        "this store was restored from {} of lineage {}, but the backup prefix \
1100                         has moved on since (LATEST names {}): another instance has written \
1101                         to it. Nothing is uploaded or deleted. Point MUSHROOMDB_BACKUP_S3_URL \
1102                         at a new prefix",
1103                        p.manifest,
1104                        p.lineage,
1105                        self.latest_target.as_deref().unwrap_or("nothing")
1106                    )));
1107                }
1108                true
1109            }
1110            _ => false,
1111        };
1112        let lineage = match (local, &latest) {
1113            (Some(ours), Some(m)) if ours != m.lineage && !adopt => {
1114                return Err(self.refuse(format!(
1115                    "this backup prefix holds the backups of another store (lineage {}), \
1116                     not this one (lineage {ours}); nothing is uploaded or deleted. Point \
1117                     MUSHROOMDB_BACKUP_S3_URL at a new prefix, or restore this store from \
1118                     that prefix first",
1119                    m.lineage
1120                )))
1121            }
1122            (None, Some(m)) => {
1123                return Err(self.refuse(format!(
1124                    "this backup prefix holds the backups of another store (lineage {}), and \
1125                     this store has never been backed up; nothing is uploaded or deleted. \
1126                     Point MUSHROOMDB_BACKUP_S3_URL at a new prefix, or restore this store \
1127                     from that prefix first",
1128                    m.lineage
1129                )))
1130            }
1131            (Some(ours), Some(m)) => match self.position().await.frames {
1132                None => return Ok(false),
1133                Some(have) if have < m.commit_seq => {
1134                    return Err(self.refuse(format!(
1135                        "this store holds {have} commits, fewer than the {} of the newest \
1136                         restore point in the backup prefix: it was restored from an older \
1137                         backup, or rewound. Uploading would bury the newer history, so \
1138                         nothing is uploaded or deleted. Point MUSHROOMDB_BACKUP_S3_URL at \
1139                         a new prefix",
1140                        m.commit_seq
1141                    )))
1142                }
1143                Some(_) => ours,
1144            },
1145            (Some(ours), None) => ours,
1146            (None, None) => {
1147                let id = new_instance_id();
1148                let (dir, new) = (self.dir.clone(), id.clone());
1149                blocking(move || disk::create_lineage(&dir, &new)).await?;
1150                tracing::info!(lineage = %id, "this store's first backup: a new backup lineage");
1151                id
1152            }
1153        };
1154        if adopt {
1155            tracing::info!(
1156                lineage = %lineage,
1157                parent = parent.as_ref().map_or("", |p| p.lineage.as_str()),
1158                "continuing the backup prefix this store was restored from, as a new lineage"
1159            );
1160        }
1161        self.parent_lineage = parent.map(|p| p.lineage);
1162        let wal_dir = ObjectPath::from(object_key(&self.prefix, "wal"));
1163        for p in self.list(&wal_dir).await?.common_prefixes {
1164            if let Some(g) = p.filename().and_then(|s| s.parse::<u64>().ok()) {
1165                self.generations.insert(g);
1166            }
1167        }
1168        for dir in ["snapshots", "archives"] {
1169            let path = ObjectPath::from(object_key(&self.prefix, dir));
1170            for o in self.list(&path).await?.objects {
1171                if let Some(g) = parse_versioned_generation(&self.prefix, o.location.as_ref()) {
1172                    self.generations.insert(g);
1173                }
1174            }
1175        }
1176        let mut highest = self.generations.last().copied();
1177        if let Some(m) = latest {
1178            highest = highest.max(Some(m.wal_generation));
1179            // Another lineage's archives are never reused: a restore point
1180            // names only its own lineage's objects.
1181            let files = if m.lineage == lineage {
1182                m.files
1183            } else {
1184                Vec::new()
1185            };
1186            for f in &files {
1187                if let Some(n) = parse_archive_name(&f.name) {
1188                    self.archives.insert(
1189                        n,
1190                        Archive {
1191                            len: f.size,
1192                            entry: f.clone(),
1193                            verified: false,
1194                        },
1195                    );
1196                }
1197            }
1198        }
1199        self.next_generation = highest.map_or(0, |g| g + 1);
1200        self.lineage = Some(lineage);
1201        self.resumed = true;
1202        Ok(true)
1203    }
1204
1205    /// Refuse the prefix for the life of this worker, for `why`.
1206    fn refuse(&mut self, why: String) -> BackupError {
1207        tracing::error!(reason = %why, "backup refuses this prefix");
1208        self.health.lock().lineage = Some(why.clone());
1209        self.refused = true;
1210        BackupError::Lineage(why)
1211    }
1212
1213    /// The lineage resume established; empty before it.
1214    fn lineage(&self) -> String {
1215        self.lineage.clone().unwrap_or_default()
1216    }
1217
1218    /// The manifest `LATEST` names, remembering `LATEST`'s version and
1219    /// target for the next conditional write of it.
1220    async fn latest_manifest(&mut self) -> Result<Option<Manifest>, BackupError> {
1221        let key = match self.get_versioned(&latest_key(&self.prefix)).await? {
1222            Some((b, v)) => {
1223                self.latest_version = Some(v);
1224                String::from_utf8(b)
1225                    .map_err(|_| BackupError::Manifest("LATEST is not UTF-8".into()))?
1226            }
1227            None => {
1228                self.latest_version = None;
1229                return Ok(None);
1230            }
1231        };
1232        self.latest_target = Some(key.trim().to_string());
1233        let bytes = self.get(key.trim()).await?.ok_or_else(|| {
1234            BackupError::Manifest(format!("LATEST names {key}, which does not exist"))
1235        })?;
1236        Manifest::from_json(&bytes)
1237            .map(Some)
1238            .map_err(BackupError::Manifest)
1239    }
1240
1241    // ── Restore points ──────────────────────────────────────────────────────
1242
1243    /// Start a generation beside the snapshot on disk: open the snapshot and
1244    /// the archives and read the chain files while the disk holds still, then
1245    /// upload them under keys of their own, ship the generation's durable WAL
1246    /// bytes from offset 0, and write the manifest.
1247    async fn upload_base(&mut self, pos: WalPosition, disk: DiskState) -> Result<(), BackupError> {
1248        let dir = self.dir.clone();
1249        let read = blocking(move || {
1250            let snapshot = disk::open_snapshot(&dir)?;
1251            let mut archives = Vec::new();
1252            for (n, len) in disk::local_archives(&dir)? {
1253                if let Ok(f) = std::fs::File::open(dir.join(format!("wal.{n}.archive"))) {
1254                    archives.push((n, len, f));
1255                }
1256            }
1257            let mut chain = Vec::new();
1258            for name in CHAIN_FILES {
1259                if let Some((_, Some(bytes))) = disk::read_side(&dir, name, None)? {
1260                    chain.push((name, bytes));
1261                }
1262            }
1263            Ok((snapshot, archives, chain))
1264        })
1265        .await?;
1266        let (snapshot, archives, chain) = read;
1267        let after = self.observe().await?;
1268        let pos_after = self.position().await;
1269        let opened_ident = snapshot.as_ref().map(|(_, i)| *i);
1270        if !disk.unchanged_until(&after)
1271            || opened_ident != disk.snapshot
1272            || WalKey::of(&pos_after) != WalKey::of(&pos)
1273            || pos_after.snapshot_in_progress
1274        {
1275            return self.defer("the store's files changed while a restore point was read");
1276        }
1277        // Burn the number now: objects a failed attempt leaves behind belong
1278        // to a generation no manifest will ever name.
1279        let number = self.next_generation;
1280        self.next_generation += 1;
1281        self.generations.insert(number);
1282        self.allocated.insert(number);
1283
1284        let mut present = Vec::new();
1285        for (n, len, mut file) in archives {
1286            let reusable = match self.archives.get(&n) {
1287                Some(a) if a.len == len && a.verified => true,
1288                Some(a) if a.len == len => {
1289                    // Seeded from the bucket: the same number and length are
1290                    // not the same bytes until the CRC says so.
1291                    let want = a.entry.crc32.clone();
1292                    let (crc, f) =
1293                        blocking(move || disk::crc32_of(&mut file).map(|c| (c, file))).await?;
1294                    file = f;
1295                    crc == want
1296                }
1297                _ => false,
1298            };
1299            if reusable {
1300                if let Some(a) = self.archives.get_mut(&n) {
1301                    a.verified = true;
1302                }
1303            } else {
1304                let name = format!("wal.{n}.archive");
1305                let key = archive_key(&self.prefix, number, n);
1306                let entry = self.put_file(&name, &key, file, true).await?;
1307                self.archives.insert(
1308                    n,
1309                    Archive {
1310                        len: entry.size,
1311                        entry,
1312                        verified: true,
1313                    },
1314                );
1315            }
1316            present.push(n);
1317        }
1318        let mut floor = 0;
1319        let mut chain_entries = Vec::new();
1320        for (name, bytes) in chain {
1321            if name == "wal.floor" {
1322                floor = bytes
1323                    .get(..8)
1324                    .map_or(0, |b| u64::from_le_bytes(b.try_into().unwrap()));
1325            }
1326            chain_entries.push(self.put_side(name, bytes).await?);
1327        }
1328        let archive_frames: u64 = present
1329            .iter()
1330            .filter_map(|n| self.archives.get(n))
1331            .map(|a| a.entry.frames)
1332            .sum();
1333        let snapshot = match snapshot {
1334            Some((file, _)) => {
1335                let key = snapshot_key(&self.prefix, number, floor + archive_frames);
1336                Some(self.put_file("snapshot.bin", &key, file, false).await?)
1337            }
1338            None => None,
1339        };
1340        self.sync_side_files().await?;
1341        self.snapshot = snapshot;
1342        self.base_archives = present;
1343        self.current = Some(Generation {
1344            number,
1345            key: WalKey::of(&pos),
1346            cursor: 0,
1347            floor,
1348            chain: chain_entries,
1349            segments: Vec::new(),
1350        });
1351        self.record_shipped();
1352        self.manifest_dirty = true;
1353        self.ship_segments().await?;
1354        self.write_manifest().await
1355    }
1356
1357    /// Upload a side file under its content-addressed key.
1358    async fn put_side(&mut self, name: &str, bytes: Vec<u8>) -> Result<FileEntry, BackupError> {
1359        let crc32 = crc32_hex(&bytes);
1360        let size = bytes.len() as u64;
1361        let key = side_key(&self.prefix, name, &crc32, size);
1362        self.put_immutable(&key, bytes, &crc32).await?;
1363        Ok(FileEntry {
1364            name: name.to_string(),
1365            key,
1366            size,
1367            crc32,
1368            frames: 0,
1369            lineage: self.lineage(),
1370        })
1371    }
1372
1373    /// Upload each per-manifest side file whose local copy changed since it
1374    /// was last uploaded; forget one that no longer exists locally. The chain
1375    /// files are the generation's, not refreshed here.
1376    async fn sync_side_files(&mut self) -> Result<(), BackupError> {
1377        for name in SIDE_FILES {
1378            if CHAIN_FILES.contains(&name) {
1379                continue;
1380            }
1381            let known = self.side.get(name).map(|(i, _)| *i);
1382            let dir = self.dir.clone();
1383            let read = blocking(move || disk::read_side(&dir, name, known)).await?;
1384            let Some((ident, bytes)) = read else {
1385                if self.side.remove(name).is_some() {
1386                    self.manifest_dirty = true;
1387                }
1388                continue;
1389            };
1390            let Some(bytes) = bytes else {
1391                continue;
1392            };
1393            let crc32 = crc32_hex(&bytes);
1394            let size = bytes.len() as u64;
1395            let unchanged = self
1396                .side
1397                .get(name)
1398                .is_some_and(|(_, e)| e.key == side_key(&self.prefix, name, &crc32, size));
1399            let entry = if unchanged {
1400                self.side[name].1.clone()
1401            } else {
1402                self.manifest_dirty = true;
1403                self.put_side(name, bytes).await?
1404            };
1405            self.side.insert(name, (ident, entry));
1406        }
1407        Ok(())
1408    }
1409
1410    // ── WAL segments ────────────────────────────────────────────────────────
1411
1412    /// Ship every fsynced WAL byte of the current generation not shipped yet.
1413    /// Each read is bracketed by two looks at the directory and dropped if
1414    /// anything that pairs it with the snapshot moved.
1415    async fn ship_segments(&mut self) -> Result<(), BackupError> {
1416        loop {
1417            let Some(g) = self.current.as_ref() else {
1418                return Ok(());
1419            };
1420            let (key, cursor, number) = (g.key, g.cursor, g.number);
1421            let before = self.observe().await?;
1422            let pos = self.position().await;
1423            if before.intent_nonempty
1424                || before.snapshot != key.snapshot
1425                || before.wal != key.wal
1426                || WalKey::of(&pos) != key
1427            {
1428                return Ok(());
1429            }
1430            if pos.len <= cursor {
1431                return Ok(());
1432            }
1433            let wal = self.dir.join("wal.bin");
1434            let len = pos.len;
1435            let read = blocking(move || read_new_segment(&wal, cursor, len)).await?;
1436            let after = self.observe().await?;
1437            if !before.unchanged_until(&after) || WalKey::of(&self.position().await) != key {
1438                return Ok(());
1439            }
1440            let Some((start, end, bytes)) = read else {
1441                return Ok(());
1442            };
1443            let (_, frames) = complete_frames(&bytes);
1444            let seg_key = segment_key(&self.prefix, number, start, end);
1445            let crc32 = crc32_hex(&bytes);
1446            self.put_immutable(&seg_key, bytes, &crc32).await?;
1447            let lineage = self.lineage();
1448            if let Some(g) = self.current.as_mut() {
1449                g.cursor = end;
1450                g.segments.push(SegmentEntry {
1451                    key: seg_key,
1452                    crc32,
1453                    frames,
1454                    lineage,
1455                });
1456            }
1457            self.record_shipped();
1458            self.manifest_dirty = true;
1459        }
1460    }
1461
1462    fn record_shipped(&self) {
1463        if let Some(g) = &self.current {
1464            self.health.lock().shipped = Some((g.key, g.cursor));
1465        }
1466    }
1467
1468    // ── Manifests and retention ─────────────────────────────────────────────
1469
1470    /// Write a manifest for the current restore point and point `LATEST` at
1471    /// it, then apply retention.
1472    async fn write_manifest(&mut self) -> Result<(), BackupError> {
1473        self.sync_side_files().await?;
1474        let Some(generation) = self.current.as_ref() else {
1475            return Ok(());
1476        };
1477        let mut files = Vec::new();
1478        files.extend(self.snapshot.clone());
1479        for n in &self.base_archives {
1480            if let Some(a) = self.archives.get(n) {
1481                files.push(a.entry.clone());
1482            }
1483        }
1484        files.extend(generation.chain.iter().cloned());
1485        files.extend(self.side.values().map(|(_, e)| e.clone()));
1486        // What the restored store will count: its floor, plus every frame of
1487        // every archive and segment listed here.
1488        let commit_seq = generation.floor
1489            + files.iter().map(|f| f.frames).sum::<u64>()
1490            + generation.segments.iter().map(|s| s.frames).sum::<u64>();
1491        let manifest = Manifest {
1492            version: MANIFEST_VERSION,
1493            lineage: self.lineage(),
1494            created_at: now_rfc3339(),
1495            commit_seq,
1496            oldest_commit_seq: generation.floor,
1497            files,
1498            wal_generation: generation.number,
1499            segments: generation.segments.clone(),
1500        };
1501        // Refuse rather than write a restore point that would not verify.
1502        manifest.verify_lineage().map_err(BackupError::Manifest)?;
1503        let key = manifest_key(&self.prefix, manifest.commit_seq);
1504        if self.parent_keys.contains(&key) || !self.put_manifest(&key, manifest.to_json()).await? {
1505            // The parent's restore point at this commit count: the same
1506            // state, which `LATEST` may still name. The next commit's
1507            // restore point is this lineage's first.
1508            return Ok(());
1509        }
1510        self.put_latest(&key).await?;
1511        self.manifest_dirty = false;
1512        self.last_manifest_at = Some(Instant::now());
1513        if self.config.retain_n > 0 {
1514            self.apply_retention(&key).await?;
1515        }
1516        Ok(())
1517    }
1518
1519    /// Keep the newest `retain_n` manifests of this lineage, then delete
1520    /// what only the dropped ones named.
1521    ///
1522    /// - Only manifests that parse and name this lineage are counted or
1523    ///   deleted; any other manifest under the prefix is left alone.
1524    ///   `just_written` and `LATEST`'s target are always kept.
1525    /// - Snapshots and archives are deleted when no kept manifest names them
1526    ///   and their generation is this lineage's (named by one of its
1527    ///   manifests, or allocated by this tenure). Side files are deleted when
1528    ///   a dropped manifest named them and no kept one does.
1529    /// - Segments: a generation of this lineage older than every kept
1530    ///   manifest's (other than the current one) has its prefix listed, once,
1531    ///   and its unnamed segments deleted. `wal/` is never listed whole.
1532    ///
1533    /// `LATEST` and anything outside those directories is never touched.
1534    /// Conservative on doubt: when a kept manifest cannot be read, nothing but
1535    /// the surplus manifests is deleted.
1536    async fn apply_retention(&mut self, just_written: &str) -> Result<(), BackupError> {
1537        let dir = ObjectPath::from(object_key(&self.prefix, "manifest"));
1538        let mut keys: Vec<String> = self
1539            .list(&dir)
1540            .await?
1541            .objects
1542            .into_iter()
1543            .map(|o| o.location.to_string())
1544            .filter(|k| k.ends_with(".json"))
1545            .collect();
1546        keys.sort();
1547        let ours = self.lineage();
1548        let mut mine: Vec<(String, Manifest)> = Vec::new();
1549        // What other lineages' manifests name: side files are keyed by their
1550        // content, so a parent and a child lineage can name the same one.
1551        let mut foreign: HashSet<String> = HashSet::new();
1552        for k in keys {
1553            let Some(bytes) = self.get(&k).await? else {
1554                continue;
1555            };
1556            match Manifest::from_json(&bytes) {
1557                Ok(m) if m.lineage == ours => mine.push((k, m)),
1558                Ok(m) => {
1559                    foreign.extend(m.files.into_iter().map(|f| f.key));
1560                    foreign.extend(m.segments.into_iter().map(|s| s.key));
1561                }
1562                Err(_) => {}
1563            }
1564        }
1565        if mine.len() <= self.config.retain_n {
1566            return Ok(());
1567        }
1568        let newest_start = mine.len() - self.config.retain_n;
1569        let protected = |k: &str| k == just_written || self.latest_target.as_deref() == Some(k);
1570        let (mut kept, mut dropped) = (Vec::new(), Vec::new());
1571        for (i, (k, m)) in mine.into_iter().enumerate() {
1572            if i >= newest_start || protected(&k) {
1573                kept.push((k, m));
1574            } else {
1575                dropped.push((k, m));
1576            }
1577        }
1578        if dropped.is_empty() {
1579            return Ok(());
1580        }
1581        let mut owned: BTreeSet<u64> = self.allocated.clone();
1582        owned.extend(kept.iter().chain(&dropped).map(|(_, m)| m.wal_generation));
1583        for (k, _) in &dropped {
1584            self.delete(k).await?;
1585        }
1586        let mut referenced: HashSet<String> = HashSet::new();
1587        let mut oldest_kept = u64::MAX;
1588        for (_, m) in &kept {
1589            oldest_kept = oldest_kept.min(m.wal_generation);
1590            referenced.extend(m.files.iter().map(|f| f.key.clone()));
1591            referenced.extend(m.segments.iter().map(|s| s.key.clone()));
1592        }
1593        let mut doomed = Vec::new();
1594        for top in ["snapshots", "archives"] {
1595            for k in self.list_all(&object_key(&self.prefix, top)).await? {
1596                let ours = parse_versioned_generation(&self.prefix, &k)
1597                    .is_some_and(|g| owned.contains(&g));
1598                if ours && !referenced.contains(&k) {
1599                    doomed.push(k);
1600                }
1601            }
1602        }
1603        let side_prefix = object_key(&self.prefix, "side/");
1604        let side: BTreeSet<String> = dropped
1605            .iter()
1606            .flat_map(|(_, m)| m.files.iter().map(|f| f.key.clone()))
1607            .filter(|k| k.starts_with(&side_prefix) && !referenced.contains(k))
1608            .collect();
1609        doomed.extend(side);
1610        let current = self.current.as_ref().map(|g| g.number);
1611        let retiring: Vec<u64> = owned
1612            .iter()
1613            .copied()
1614            .filter(|g| *g < oldest_kept && Some(*g) != current)
1615            .collect();
1616        for g in &retiring {
1617            let prefix = object_key(&self.prefix, &format!("wal/{g}"));
1618            for k in self.list_all(&prefix).await? {
1619                if parse_segment_key(&self.prefix, &k).is_some_and(|(sg, _, _)| sg == *g)
1620                    && !referenced.contains(&k)
1621                {
1622                    doomed.push(k);
1623                }
1624            }
1625        }
1626        for k in doomed {
1627            if !foreign.contains(&k) {
1628                self.delete(&k).await?;
1629            }
1630        }
1631        for g in retiring {
1632            self.generations.remove(&g);
1633        }
1634        let keep: HashSet<&String> = referenced.iter().collect();
1635        self.archives.retain(|_, a| keep.contains(&a.entry.key));
1636        Ok(())
1637    }
1638
1639    /// Every object key under `prefix`, recursively.
1640    async fn list_all(&mut self, prefix: &str) -> Result<Vec<String>, BackupError> {
1641        let mut out = Vec::new();
1642        let mut dirs = vec![ObjectPath::from(prefix)];
1643        while let Some(d) = dirs.pop() {
1644            let r = self.list(&d).await?;
1645            out.extend(r.objects.into_iter().map(|o| o.location.to_string()));
1646            dirs.extend(r.common_prefixes);
1647        }
1648        Ok(out)
1649    }
1650
1651    // ── Object store calls, each under the operation deadline ───────────────
1652
1653    fn op_timeout(&self) -> Duration {
1654        Duration::from_millis(self.config.op_timeout_ms)
1655    }
1656
1657    async fn deadline<T>(
1658        &self,
1659        what: impl FnOnce() -> String,
1660        fut: impl Future<Output = Result<T, BackupError>>,
1661    ) -> Result<T, BackupError> {
1662        match tokio::time::timeout(self.op_timeout(), fut).await {
1663            Ok(r) => r,
1664            Err(_) => Err(BackupError::Timeout(what())),
1665        }
1666    }
1667
1668    /// Spend one request of the test hook's budget, if one is set.
1669    fn spend_write(&mut self) -> Result<(), BackupError> {
1670        match self.write_budget.as_mut() {
1671            Some(0) => Err(BackupError::Inconsistent(
1672                "test hook: the process is gone".into(),
1673            )),
1674            Some(n) => {
1675                *n -= 1;
1676                Ok(())
1677            }
1678            None => Ok(()),
1679        }
1680    }
1681
1682    /// A request that changes the bucket: under the operation deadline and,
1683    /// when the worker holds a lease, the lease's fence.
1684    async fn fenced<T>(
1685        &self,
1686        what: impl FnOnce() -> String,
1687        fut: impl Future<Output = Result<T, BackupError>>,
1688    ) -> Result<T, BackupError> {
1689        fence(self.lease.as_deref(), self.deadline(what, fut)).await
1690    }
1691
1692    /// One PUT in `mode`, under the deadline and the fence. The outer error
1693    /// is the worker's (deadline, fence, test hook); the inner one is the
1694    /// service's answer, for the caller to interpret.
1695    async fn put_mode(
1696        &mut self,
1697        key: &str,
1698        payload: PutPayload,
1699        mode: PutMode,
1700    ) -> Result<Result<PutResult, object_store::Error>, BackupError> {
1701        self.spend_write()?;
1702        let path = ObjectPath::from(key);
1703        let store = Arc::clone(&self.store);
1704        self.fenced(|| format!("PUT {key}"), async move {
1705            Ok(store.put_opts(&path, payload, mode.into()).await)
1706        })
1707        .await
1708    }
1709
1710    /// Write an immutable object with `If-None-Match: *`. One already there
1711    /// is accepted only when its CRC is `crc32` (a retried upload that had
1712    /// landed); any other content means another writer.
1713    async fn put_immutable(
1714        &mut self,
1715        key: &str,
1716        bytes: Vec<u8>,
1717        crc32: &str,
1718    ) -> Result<(), BackupError> {
1719        let payload = PutPayload::from(bytes);
1720        match self.put_mode(key, payload.clone(), PutMode::Create).await? {
1721            Ok(_) => Ok(()),
1722            Err(object_store::Error::NotImplemented) => {
1723                self.put_mode(key, payload, PutMode::Overwrite).await??;
1724                Ok(())
1725            }
1726            Err(object_store::Error::AlreadyExists { .. }) => match self.get(key).await? {
1727                Some(b) if crc32_hex(&b) == crc32 => Ok(()),
1728                _ => Err(self.conflict(format!("{key} already exists with other content"))),
1729            },
1730            Err(e) => Err(e.into()),
1731        }
1732    }
1733
1734    /// Write a manifest: `If-Match` against the version this tenure last
1735    /// wrote at `key`; otherwise `If-None-Match: *`, and over an existing one
1736    /// only when it is this lineage's (a previous run's restore point at the
1737    /// same commit). `Ok(false)`, nothing written, when `key` holds the
1738    /// manifest of the lineage this store was restored on boot from.
1739    async fn put_manifest(&mut self, key: &str, bytes: Vec<u8>) -> Result<bool, BackupError> {
1740        // A client without conditional PUT would overwrite: look first.
1741        if self.parent_lineage.is_some() && !self.manifest_versions.contains_key(key) {
1742            if let Some((b, _)) = self.get_versioned(key).await? {
1743                if Manifest::from_json(&b).ok().map(|m| m.lineage) == self.parent_lineage {
1744                    tracing::info!(
1745                        key,
1746                        "the parent lineage's restore point holds this key; not written"
1747                    );
1748                    self.parent_keys.insert(key.to_string());
1749                    return Ok(false);
1750                }
1751            }
1752        }
1753        let payload = PutPayload::from(bytes);
1754        let mode = match self.manifest_versions.get(key) {
1755            Some(v) => PutMode::Update(v.clone()),
1756            None => PutMode::Create,
1757        };
1758        let written = match self.put_mode(key, payload.clone(), mode).await? {
1759            Err(object_store::Error::AlreadyExists { .. }) => {
1760                let existing = self.get_versioned(key).await?;
1761                let owner = existing
1762                    .as_ref()
1763                    .and_then(|(b, _)| Manifest::from_json(b).ok())
1764                    .map(|m| m.lineage);
1765                let ours = owner.as_deref() == Some(self.lineage().as_str());
1766                if owner.is_some() && owner == self.parent_lineage {
1767                    tracing::info!(
1768                        key,
1769                        "the parent lineage's restore point holds this key; not written"
1770                    );
1771                    self.parent_keys.insert(key.to_string());
1772                    return Ok(false);
1773                }
1774                match existing {
1775                    Some((_, v)) if ours => {
1776                        self.put_mode(key, payload.clone(), PutMode::Update(v))
1777                            .await?
1778                    }
1779                    _ => return Err(self.conflict(format!("{key} is another lineage's manifest"))),
1780                }
1781            }
1782            other => other,
1783        };
1784        let r = match written {
1785            Ok(r) => r,
1786            Err(object_store::Error::NotImplemented) => {
1787                self.put_mode(key, payload, PutMode::Overwrite).await??
1788            }
1789            Err(object_store::Error::Precondition { .. })
1790            | Err(object_store::Error::AlreadyExists { .. }) => {
1791                return Err(self.conflict(format!("{key} changed since this worker wrote it")))
1792            }
1793            Err(e) => return Err(e.into()),
1794        };
1795        self.manifest_versions
1796            .insert(key.to_string(), version_of(r));
1797        Ok(true)
1798    }
1799
1800    /// Point `LATEST` at `target`, with `If-Match` against the version last
1801    /// written or read (`If-None-Match: *` when there was none). A refusal
1802    /// means another writer moved it.
1803    async fn put_latest(&mut self, target: &str) -> Result<(), BackupError> {
1804        let key = latest_key(&self.prefix);
1805        let payload = PutPayload::from(target.as_bytes().to_vec());
1806        let mode = match &self.latest_version {
1807            Some(v) => PutMode::Update(v.clone()),
1808            None => PutMode::Create,
1809        };
1810        let r = match self.put_mode(&key, payload.clone(), mode).await? {
1811            Ok(r) => r,
1812            Err(object_store::Error::NotImplemented) => {
1813                self.put_mode(&key, payload, PutMode::Overwrite).await??
1814            }
1815            Err(object_store::Error::Precondition { .. })
1816            | Err(object_store::Error::AlreadyExists { .. })
1817            | Err(object_store::Error::NotFound { .. }) => {
1818                return Err(self.conflict("LATEST changed since this worker last wrote it".into()))
1819            }
1820            Err(e) => return Err(e.into()),
1821        };
1822        self.latest_version = Some(version_of(r));
1823        self.latest_target = Some(target.to_string());
1824        Ok(())
1825    }
1826
1827    /// The bucket refused a write because another writer got there first.
1828    /// Drop the lease: the worker starts over through it, and resume reads
1829    /// the bucket afresh.
1830    fn conflict(&mut self, what: String) -> BackupError {
1831        let why = format!("another writer is using this backup prefix: {what}");
1832        tracing::warn!(reason = %why, "backup write refused by the bucket");
1833        if let Some(l) = &self.lease {
1834            l.mark_lost();
1835            self.health.lock().lease = Some(why.clone());
1836        }
1837        BackupError::Conflict(why)
1838    }
1839
1840    /// Stream `file` to `key`, multipart when large; return its entry, with
1841    /// its frame count when `count_frames`. Each chunk, and the final commit,
1842    /// is under the operation deadline and the lease's fence.
1843    ///
1844    /// A failure while chunks are being written aborts the upload once. A
1845    /// failure of the final commit drops it instead: the writer has already
1846    /// been shut down, and an abort then is not allowed (it panics). An
1847    /// unfinished multipart upload left that way is the bucket's to expire.
1848    async fn put_file(
1849        &mut self,
1850        name: &str,
1851        key: &str,
1852        file: std::fs::File,
1853        count_frames: bool,
1854    ) -> Result<FileEntry, BackupError> {
1855        // A multipart upload cannot be conditional here: check the key is
1856        // free first. Every snapshot and archive key carries a generation
1857        // this worker allocated, so one already there is another writer's.
1858        if self.head(key).await? {
1859            return Err(self.conflict(format!("{key} already exists")));
1860        }
1861        self.spend_write()?;
1862        let mut file = tokio::fs::File::from_std(file);
1863        let mut writer = BufWriter::new(Arc::clone(&self.store), ObjectPath::from(key));
1864        let mut hasher = crc32fast::Hasher::new();
1865        let mut counter = FrameCounter::default();
1866        let mut size = 0u64;
1867        let mut buf = vec![0u8; UPLOAD_CHUNK];
1868        let timeout = self.op_timeout();
1869        let lease = self.lease.clone();
1870        let written: Result<(), BackupError> = async {
1871            loop {
1872                let n = file.read(&mut buf).await?;
1873                if n == 0 {
1874                    return Ok(());
1875                }
1876                hasher.update(&buf[..n]);
1877                if count_frames {
1878                    counter.feed(&buf[..n]);
1879                }
1880                size += n as u64;
1881                fence(lease.as_deref(), async {
1882                    tokio::time::timeout(timeout, writer.write_all(&buf[..n]))
1883                        .await
1884                        .map_err(|_| BackupError::Timeout(format!("upload of {key}")))??;
1885                    Ok(())
1886                })
1887                .await?;
1888            }
1889        }
1890        .await;
1891        if let Err(e) = written {
1892            let _ = tokio::time::timeout(timeout, writer.abort()).await;
1893            return Err(e);
1894        }
1895        fence(lease.as_deref(), async {
1896            tokio::time::timeout(timeout, writer.shutdown())
1897                .await
1898                .map_err(|_| BackupError::Timeout(format!("upload of {key}")))??;
1899            Ok(())
1900        })
1901        .await?;
1902        Ok(FileEntry {
1903            name: name.to_string(),
1904            key: key.to_string(),
1905            size,
1906            crc32: format_crc32(hasher.finalize()),
1907            frames: counter.frames(),
1908            lineage: self.lineage(),
1909        })
1910    }
1911
1912    async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, BackupError> {
1913        let path = ObjectPath::from(key);
1914        let store = Arc::clone(&self.store);
1915        self.deadline(|| format!("GET {key}"), async move {
1916            match store.get(&path).await {
1917                Ok(r) => Ok(Some(r.bytes().await?.to_vec())),
1918                Err(object_store::Error::NotFound { .. }) => Ok(None),
1919                Err(e) => Err(e.into()),
1920            }
1921        })
1922        .await
1923    }
1924
1925    async fn get_versioned(
1926        &self,
1927        key: &str,
1928    ) -> Result<Option<(Vec<u8>, UpdateVersion)>, BackupError> {
1929        let path = ObjectPath::from(key);
1930        let store = Arc::clone(&self.store);
1931        self.deadline(|| format!("GET {key}"), async move {
1932            match store.get(&path).await {
1933                Ok(r) => {
1934                    let v = UpdateVersion {
1935                        e_tag: r.meta.e_tag.clone(),
1936                        version: r.meta.version.clone(),
1937                    };
1938                    Ok(Some((r.bytes().await?.to_vec(), v)))
1939                }
1940                Err(object_store::Error::NotFound { .. }) => Ok(None),
1941                Err(e) => Err(e.into()),
1942            }
1943        })
1944        .await
1945    }
1946
1947    async fn head(&self, key: &str) -> Result<bool, BackupError> {
1948        let path = ObjectPath::from(key);
1949        let store = Arc::clone(&self.store);
1950        self.deadline(|| format!("HEAD {key}"), async move {
1951            match store.head(&path).await {
1952                Ok(_) => Ok(true),
1953                Err(object_store::Error::NotFound { .. }) => Ok(false),
1954                Err(e) => Err(e.into()),
1955            }
1956        })
1957        .await
1958    }
1959
1960    async fn delete(&mut self, key: &str) -> Result<(), BackupError> {
1961        self.spend_write()?;
1962        let path = ObjectPath::from(key);
1963        let store = Arc::clone(&self.store);
1964        self.fenced(|| format!("DELETE {key}"), async move {
1965            match store.delete(&path).await {
1966                Ok(()) | Err(object_store::Error::NotFound { .. }) => Ok(()),
1967                Err(e) => Err(e.into()),
1968            }
1969        })
1970        .await
1971    }
1972
1973    async fn list(&mut self, dir: &ObjectPath) -> Result<object_store::ListResult, BackupError> {
1974        self.lists += 1;
1975        let store = Arc::clone(&self.store);
1976        let d = dir.clone();
1977        self.deadline(|| format!("LIST {dir}"), async move {
1978            Ok(store.list_with_delimiter(Some(&d)).await?)
1979        })
1980        .await
1981    }
1982}
1983
1984/// The engine's durable position, read on the blocking pool: the first read
1985/// after the handle applied WAL bytes it did not write fsyncs them.
1986async fn position_of(source: &Arc<dyn WalSource>) -> WalPosition {
1987    let s = Arc::clone(source);
1988    match tokio::task::spawn_blocking(move || s.durable_wal_position()).await {
1989        Ok(p) => p,
1990        // The read itself cannot fail; a panicked blocking task falls back to
1991        // reading here.
1992        Err(_) => source.durable_wal_position(),
1993    }
1994}
1995
1996/// Run `fut` unless a shutdown arrives first; `None` if it did.
1997async fn until_shutdown<F: Future>(
1998    rx: &mut watch::Receiver<BackupTrigger>,
1999    fut: F,
2000) -> Option<F::Output> {
2001    tokio::pin!(fut);
2002    loop {
2003        tokio::select! {
2004            out = &mut fut => return Some(out),
2005            changed = rx.changed() => {
2006                if is_shutdown(changed, rx) {
2007                    return None;
2008                }
2009            }
2010        }
2011    }
2012}
2013
2014/// How often a worker kept out by a foreign lease looks again.
2015fn poll_interval(ttl_s: u64) -> Duration {
2016    Duration::from_millis(ttl_s.max(1) * 1000 / 3)
2017}
2018
2019/// Run a write while `lease` allows writes, and cancel it the moment it
2020/// stops: lost, or not renewed in time. The lease is checked before every
2021/// poll of the write, so a process resumed after a freeze is fenced before
2022/// the write makes progress, not after. No lease: run it as it is.
2023async fn fence<T>(
2024    lease: Option<&BucketLease>,
2025    fut: impl Future<Output = Result<T, BackupError>>,
2026) -> Result<T, BackupError> {
2027    let Some(lease) = lease else {
2028        return fut.await;
2029    };
2030    let fenced = || {
2031        Err(BackupError::Lease(if lease.is_lost() {
2032            "lost to another instance; this one no longer writes to the bucket".into()
2033        } else {
2034            "not renewed in time; writes are stopped until it is".into()
2035        }))
2036    };
2037    tokio::pin!(fut);
2038    let mut wake: Option<std::pin::Pin<Box<tokio::time::Sleep>>> = None;
2039    std::future::poll_fn(|cx| {
2040        let Some(deadline) = lease.write_deadline() else {
2041            return std::task::Poll::Ready(fenced());
2042        };
2043        if let std::task::Poll::Ready(out) = fut.as_mut().poll(cx) {
2044            return std::task::Poll::Ready(out);
2045        }
2046        // Look again at the deadline, and at least every second: the wall
2047        // clock can jump.
2048        let at = deadline.min(Instant::now() + Duration::from_secs(1));
2049        let sleep = wake.get_or_insert_with(|| Box::pin(tokio::time::sleep_until(at)));
2050        if sleep.deadline() != at {
2051            sleep.as_mut().reset(at);
2052        }
2053        if sleep.as_mut().poll(cx).is_ready() {
2054            sleep
2055                .as_mut()
2056                .reset(Instant::now() + Duration::from_millis(1));
2057            cx.waker().wake_by_ref();
2058        }
2059        std::task::Poll::Pending
2060    })
2061    .await
2062}
2063
2064/// Renew `lease` every third of its TTL, and a second after a failure. Stops
2065/// when the lease is found lost; health says why the worker is kept out.
2066async fn renew_loop(
2067    lease: Arc<BucketLease>,
2068    first: Duration,
2069    op_timeout: Duration,
2070    health: Health,
2071    errors: Arc<AtomicU64>,
2072) {
2073    let every = lease.renew_interval().max(Duration::from_millis(1));
2074    let mut wait = first;
2075    loop {
2076        tokio::time::sleep(wait).await;
2077        let why = match tokio::time::timeout(op_timeout, lease.renew()).await {
2078            Ok(Ok(())) => {
2079                health.lock().lease = None;
2080                health.lease_held.store(true, Ordering::Release);
2081                wait = every;
2082                continue;
2083            }
2084            Ok(Err(e @ LeaseError::Lost { .. })) => {
2085                tracing::warn!(error = %e, "lost the backup bucket lease; uploads stop");
2086                health.lease_held.store(false, Ordering::Release);
2087                health.lock().lease = Some(format!(
2088                    "{e}; this instance uploads nothing while another holds it"
2089                ));
2090                return;
2091            }
2092            Ok(Err(e)) => e.to_string(),
2093            Err(_) => "the renewal timed out".to_string(),
2094        };
2095        errors.fetch_add(1, Ordering::AcqRel);
2096        tracing::warn!(error = %why, "backup bucket lease renewal failed; retrying");
2097        if !lease.may_write() {
2098            health.lease_held.store(false, Ordering::Release);
2099            health.lock().lease = Some(format!(
2100                "the bucket lease could not be renewed ({why}); uploads are stopped until it is"
2101            ));
2102        }
2103        wait = RENEW_RETRY.min(every);
2104    }
2105}
2106
2107fn version_of(r: PutResult) -> UpdateVersion {
2108    UpdateVersion {
2109        e_tag: r.e_tag,
2110        version: r.version,
2111    }
2112}
2113
2114fn is_shutdown(
2115    changed: Result<(), watch::error::RecvError>,
2116    rx: &mut watch::Receiver<BackupTrigger>,
2117) -> bool {
2118    match changed {
2119        // The sender is gone: nobody can ask for a shutdown any more, so this
2120        // is it.
2121        Err(_) => true,
2122        Ok(()) => *rx.borrow_and_update() == BackupTrigger::Shutdown,
2123    }
2124}
2125
2126/// Run a file-system call on Tokio's blocking pool.
2127async fn blocking<T: Send + 'static>(
2128    f: impl FnOnce() -> std::io::Result<T> + Send + 'static,
2129) -> std::io::Result<T> {
2130    tokio::task::spawn_blocking(f)
2131        .await
2132        .unwrap_or_else(|e| Err(std::io::Error::other(format!("blocking read failed: {e}"))))
2133}
2134
2135fn unix_now() -> u64 {
2136    SystemTime::now()
2137        .duration_since(UNIX_EPOCH)
2138        .map(|d| d.as_secs())
2139        .unwrap_or(0)
2140}
2141
2142fn now_rfc3339() -> String {
2143    time::OffsetDateTime::now_utc()
2144        .format(&time::format_description::well_known::Rfc3339)
2145        .unwrap_or_default()
2146}
2147
2148#[cfg(test)]
2149mod tests;