Skip to main content

coven_sync/
files.rs

1//! File transfers and cached reads, composed beside the database at the root.
2
3#[path = "file_cache.rs"]
4mod file_cache;
5#[path = "file_cache_retry.rs"]
6mod file_cache_retry;
7#[path = "file_device_only.rs"]
8mod file_device_only;
9#[path = "file_error.rs"]
10mod file_error;
11#[path = "file_failure.rs"]
12mod file_failure;
13#[path = "file_keep.rs"]
14mod file_keep;
15#[path = "file_read.rs"]
16mod file_read;
17#[path = "file_status.rs"]
18mod file_status;
19#[path = "file_upload.rs"]
20mod file_upload;
21#[path = "file_upload_abandoned.rs"]
22mod file_upload_abandoned;
23#[path = "file_upload_connections.rs"]
24mod file_upload_connections;
25#[path = "file_upload_failure.rs"]
26mod file_upload_failure;
27#[path = "file_upload_pool.rs"]
28mod file_upload_pool;
29#[path = "file_upload_queue.rs"]
30mod file_upload_queue;
31#[path = "file_upload_retry.rs"]
32mod file_upload_retry;
33#[path = "file_upload_settings.rs"]
34mod file_upload_settings;
35use crate::SyncError;
36pub use coven_database::PinProgress;
37use coven_database::{DbError, FileDatabase, Mode, ReadOnly, Writer};
38use coven_foundation::locks::{lock_unpoisoned, write_unpoisoned};
39use coven_foundation::{files::StoreDir, id_source::IdSourceRef};
40use coven_storage::Storage;
41pub use file_device_only::{DeviceOnlyError, DeviceOnlyStep};
42pub use file_error::FileReadError;
43pub use file_read::{FileRangeStream, FileStream, ReadProgress};
44pub use file_status::FileStatus;
45pub(crate) use file_upload_connections::SpareConnection;
46pub use file_upload_pool::DrainOutcome;
47pub use file_upload_queue::{QueuedUpload, UploadPhase, UploadProgress, UploadQueue};
48pub use file_upload_settings::UploadSettings;
49use std::{
50    collections::{BTreeMap, BTreeSet},
51    sync::{
52        atomic::{AtomicBool, AtomicUsize, Ordering},
53        Arc, Mutex,
54    },
55};
56use tokio::sync::{watch, Notify};
57
58use coven_storage::TransferBudget;
59pub use coven_storage::TransferLimits;
60
61/// What a handle's mode adds to its file owner: the writer's upload
62/// settings, which the upload runs and the app's setting changes use, and
63/// nothing for a read-only handle, which runs no upload. Sealed with `Mode`.
64pub trait FilesMode: Mode {
65    /// The mode's upload state, beside what every file read uses.
66    type Uploads: Send + Sync + 'static;
67}
68impl FilesMode for Writer {
69    type Uploads = UploadSettings;
70}
71impl FilesMode for ReadOnly {
72    type Uploads = ();
73}
74
75/// Owns storage, database file operations and the device's file area
76/// (Appendix F.3), for the store's writer or a read-only handle
77/// (`Files<ReadOnly>`), which only reads files and keeps its cache. The
78/// writer's starts one commit-driven worker, for uploads and pins; no sync
79/// loop or timer is created. Call `close` before closing the database.
80pub struct Files<M: FilesMode = Writer> {
81    inner: Arc<FilesInner<M>>,
82    worker: Arc<FileWorker>,
83}
84// Written out: `derive(Clone)` would ask the mode to be `Clone`.
85impl<M: FilesMode> Clone for Files<M> {
86    fn clone(&self) -> Self {
87        Self { inner: self.inner.clone(), worker: self.worker.clone() }
88    }
89}
90/// One lock per stored object's cache entries, by namespace and object.
91type FileReadLocks = BTreeMap<
92    (String, coven_foundation::id_source::ObjectId),
93    std::sync::Weak<tokio::sync::Mutex<()>>,
94>;
95struct FilesInner<M: FilesMode = Writer> {
96    database: FileDatabase<M>,
97    directory: StoreDir,
98    storage: std::sync::RwLock<Option<Arc<dyn Storage>>>,
99    send: Arc<crate::SequenceState>,
100    waits: Arc<crate::RetryWaits>,
101    /// Failures observed on this connection that require a new connection.
102    /// Reopening creates a new connection and permits one attempt again.
103    connection_waits: Mutex<BTreeSet<coven_database::RetrySubject>>,
104    ids: IdSourceRef,
105    /// Paces the progress a long call reports.
106    clock: coven_foundation::clock::ClockRef,
107    /// The mode's upload state: the writer's copies of the upload settings,
108    /// and nothing for a read-only handle.
109    uploads: M::Uploads,
110    /// The persisted setting to move files only on unmetered networks, as
111    /// of the last upload run or app change; parts and pins' downloads
112    /// check it, with the network, between requests (§17.9).
113    unmetered_only: AtomicBool,
114    /// The device's network, as the store's watch says it is (§6.7).
115    network: coven_foundation::network_watch::NetworkView,
116    /// Wakes a pin's download waiting for the network as the setting
117    /// changes or the store closes.
118    hold_wake: Notify,
119    /// How many pins' downloads wait for the network now, for a test.
120    #[cfg(test)]
121    pub(crate) held_downloads: tokio::sync::watch::Sender<usize>,
122    /// How many reads have found another read of the same stored bytes
123    /// holding their lock, for a test.
124    #[cfg(test)]
125    pub(crate) shared_reads: tokio::sync::watch::Sender<usize>,
126    /// Wakes the upload worker, or its run in progress.
127    upload_wake: Arc<Notify>,
128    /// Each time the upload worker has taken everything that can wake it
129    /// and waits.
130    upload_parks: crate::Parks,
131    /// One upload run at a time.
132    drain: tokio::sync::Mutex<()>,
133    /// App retries waiting for an upload run to serve them.
134    app_retries: Mutex<Vec<file_upload_pool::AppRetry>>,
135    /// Why the upload worker ended, other than told to stop, set under
136    /// `app_retries`' lock: no run serves a retry once it is set, so each
137    /// fails with it (E15).
138    worker_ended: std::sync::OnceLock<file_upload_pool::WorkerEnd>,
139    /// Connection changes waiting for the uploads in flight to end; while
140    /// any waits, no upload starts.
141    changing: AtomicUsize,
142    cache: tokio::sync::RwLock<()>,
143    reads: Mutex<FileReadLocks>,
144    transfers: Arc<TransferBudget>,
145    pins: tokio::sync::Mutex<()>,
146    closed: AtomicBool,
147    /// Runs where an upload run that found uploads blocked, with none in
148    /// flight, is about to act on that, so a test can change the setting
149    /// between the run's check and what it does.
150    #[cfg(test)]
151    blocked_end_gate: Mutex<Option<Arc<dyn Fn() + Send + Sync>>>,
152    /// Holds a device-only call at the place a test names: where a copy's
153    /// bytes were just appended, or where a copy is renamed to its path and
154    /// not yet recorded placed. It says it got there, then waits to be
155    /// resumed.
156    #[cfg(test)]
157    device_only_gate: Mutex<Option<file_device_only::DeviceOnlyGate>>,
158}
159struct FileWorker {
160    stop: watch::Sender<bool>,
161    /// The writer's upload worker, until the owner closes; a read-only
162    /// handle runs none.
163    task: tokio::sync::Mutex<Option<coven_foundation::task::OwnedTask>>,
164}
165impl Drop for FileWorker {
166    fn drop(&mut self) {
167        self.stop.send_replace(true);
168    }
169}
170impl Files<ReadOnly> {
171    /// Compose a read-only handle's file owner, which reads files and keeps
172    /// its cache; it uploads, pins and records nothing, and runs no worker.
173    pub fn read_only(
174        database: FileDatabase<ReadOnly>,
175        directory: StoreDir,
176        storage: Option<Arc<dyn Storage>>,
177        send: Arc<crate::SequenceState>,
178        waits: Arc<crate::RetryWaits>,
179        ids: IdSourceRef,
180        transfers: Arc<TransferBudget>,
181        clock: coven_foundation::clock::ClockRef,
182        network: coven_foundation::network_watch::NetworkView,
183    ) -> Self {
184        let inner = Arc::new(FilesInner::new(
185            database,
186            directory,
187            storage,
188            send,
189            waits,
190            ids,
191            transfers,
192            clock,
193            network,
194            (),
195        ));
196        let worker =
197            FileWorker { stop: watch::channel(false).0, task: tokio::sync::Mutex::new(None) };
198        Self { inner, worker: Arc::new(worker) }
199    }
200}
201impl<M: FilesMode> FilesInner<M> {
202    fn new(
203        database: FileDatabase<M>,
204        directory: StoreDir,
205        storage: Option<Arc<dyn Storage>>,
206        send: Arc<crate::SequenceState>,
207        waits: Arc<crate::RetryWaits>,
208        ids: IdSourceRef,
209        transfers: Arc<TransferBudget>,
210        clock: coven_foundation::clock::ClockRef,
211        network: coven_foundation::network_watch::NetworkView,
212        uploads: M::Uploads,
213    ) -> Self {
214        Self {
215            database,
216            directory,
217            storage: std::sync::RwLock::new(storage),
218            send,
219            waits,
220            connection_waits: Mutex::new(BTreeSet::new()),
221            ids,
222            clock,
223            uploads,
224            unmetered_only: AtomicBool::new(false),
225            network,
226            hold_wake: Notify::new(),
227            #[cfg(test)]
228            held_downloads: tokio::sync::watch::channel(0).0,
229            #[cfg(test)]
230            shared_reads: tokio::sync::watch::channel(0).0,
231            upload_wake: Arc::new(Notify::new()),
232            upload_parks: crate::Parks::new(),
233            drain: tokio::sync::Mutex::new(()),
234            app_retries: Mutex::new(Vec::new()),
235            worker_ended: std::sync::OnceLock::new(),
236            changing: AtomicUsize::new(0),
237            cache: tokio::sync::RwLock::new(()),
238            reads: Mutex::new(BTreeMap::new()),
239            transfers,
240            pins: tokio::sync::Mutex::new(()),
241            closed: AtomicBool::new(false),
242            #[cfg(test)]
243            blocked_end_gate: Mutex::new(None),
244            #[cfg(test)]
245            device_only_gate: Mutex::new(None),
246        }
247    }
248}
249impl<M: FilesMode> Files<M> {
250    /// Stop background work, wait for in-flight database and transfer work,
251    /// then release the storage client, while clones of this owner live on.
252    /// An app retry still waiting fails as the store closes, as does a read
253    /// of a file stream still open.
254    pub async fn close(&self) {
255        self.inner.closed.store(true, Ordering::Release);
256        // A pin's download waiting for the network fails as the store closes.
257        self.inner.hold_wake.notify_waiters();
258        self.worker.stop.send_replace(true);
259        if let Some(task) = self.worker.task.lock().await.take() {
260            task.ended().await;
261        }
262        let _drain = self.inner.drain.lock().await;
263        let _pins = self.inner.pins.lock().await;
264        let _cache = self.inner.cache.write().await;
265        lock_unpoisoned(&self.inner.app_retries).clear();
266        write_unpoisoned(&self.inner.storage).take();
267    }
268}
269impl Files {
270    /// Each time the upload worker has taken every change, wake and due
271    /// retry that was ready, and waits for the next.
272    #[cfg(any(test, feature = "test-utils"))]
273    pub fn uploads_parked_from_now(
274        &self,
275    ) -> impl std::future::Future<Output = ()> + Send + 'static {
276        self.inner.upload_parks.parked_from_now()
277    }
278
279    /// Compose the writer's file owner and start its upload worker.
280    /// Storage may be absent while uploads wait.
281    pub fn new(
282        database: FileDatabase,
283        directory: StoreDir,
284        storage: Option<Arc<dyn Storage>>,
285        send: Arc<crate::SequenceState>,
286        waits: Arc<crate::RetryWaits>,
287        ids: IdSourceRef,
288        transfers: Arc<TransferBudget>,
289        clock: coven_foundation::clock::ClockRef,
290        network: coven_foundation::network_watch::NetworkView,
291    ) -> Self {
292        let uploads_changes = database.queued_work_changes();
293        let (stop, _) = watch::channel(false);
294        let inner = Arc::new(FilesInner::new(
295            database,
296            directory,
297            storage,
298            send,
299            waits,
300            ids,
301            transfers,
302            clock,
303            network,
304            UploadSettings::new(),
305        ));
306        // A panic in the worker goes to the store's bug sink, halts sync
307        // until the store reopens, as the sync loop's own panic does, and
308        // fails the app retries it would have served (E15).
309        let owner = Arc::downgrade(&inner);
310        let task = coven_foundation::task::spawn_owned(
311            "the upload worker",
312            inner.database.bugs().clone(),
313            upload_worker(Arc::downgrade(&inner), stop.subscribe(), uploads_changes),
314            |bug| async move {
315                if let Some(owner) = owner.upgrade() {
316                    // A failed write has failed the handle, which reports
317                    // it, or its bug goes to the sink.
318                    if let Err(error) = owner.waits.sync_halted(bug.clone()).await {
319                        let error = crate::SyncError::from(error);
320                        if !coven_foundation::task::report_carried(
321                            owner.database.bugs().as_ref(),
322                            &error,
323                        ) {
324                            tracing::debug!(
325                                "halting sync after the upload worker's panic failed: {error}"
326                            );
327                        }
328                    }
329                    owner.worker_ended(file_upload_pool::WorkerEnd::Panicked(bug));
330                }
331            },
332        );
333        let worker = FileWorker { stop, task: tokio::sync::Mutex::new(Some(task)) };
334        Self { inner, worker: Arc::new(worker) }
335    }
336    /// The limits shared by this handle's transfer consumers.
337    pub fn transfer_limits(&self) -> TransferLimits {
338        self.inner.transfers.limits()
339    }
340    /// Change upload and download admission. The next upload to start sees
341    /// the new upload limit; uploads in flight keep running. Active response
342    /// bodies retain their leases until they finish or are dropped.
343    pub fn set_transfer_limits(&self, limits: TransferLimits) {
344        self.inner.transfers.set_limits(limits);
345        self.inner.upload_wake.notify_one();
346    }
347    /// Optionally retry recoverable uploads early: the upload worker attempts
348    /// every upload that can retry, whatever its wait, and this returns once
349    /// each attempt has ended. Provider cooldowns and failures that never
350    /// clear still hold (§17.9), and while uploads are paused only bound ones
351    /// are tried; while sync is stopped none is (§17.4).
352    pub async fn retry_uploads_now(&self) -> Result<DrainOutcome, SyncError> {
353        // The worker stopping with the store drops the retry unanswered; a
354        // worker that ended otherwise answers it with why.
355        self.inner.ask_retry()?.answered().await
356    }
357    /// Persist the pause setting, as a step of `sql`'s write; a running
358    /// trailing upload stops between confirmed parts, preserving its
359    /// provider session (E7). Bound uploads run on (§17.4). Once the write
360    /// commits, and before it returns, the copy parts check takes the new
361    /// setting, after any upload run that read the old one has acted on it:
362    /// so no retry asked after the write returns is answered from the
363    /// setting it replaced.
364    pub fn set_uploads_paused(
365        &self,
366        sql: &coven_database::SqlContext<'_, '_>,
367        paused: bool,
368    ) -> Result<(), DbError> {
369        self.inner.check_open()?;
370        sql.set_uploads_paused(paused)?;
371        let inner = self.inner.clone();
372        sql.after_commit(move || {
373            // The write's effects run on its blocking thread, where waiting
374            // for the run is allowed.
375            let change = inner.hold_change_blocking();
376            inner.set_paused(paused);
377            drop(change);
378            if !paused {
379                inner.upload_wake.notify_one();
380            }
381        });
382        Ok(())
383    }
384    /// Persist the setting to move files only on unmetered networks, as a
385    /// step of `sql`'s write (§17.9): while the network is metered, a running
386    /// trailing upload stops between confirmed parts, as a pause stops it,
387    /// and pins' downloads wait before their next request to storage. Bound
388    /// uploads run on. Once the write commits, and before it returns, the
389    /// uploads and downloads take the new setting, as they take a pause.
390    pub fn set_unmetered_only(
391        &self,
392        sql: &coven_database::SqlContext<'_, '_>,
393        unmetered_only: bool,
394    ) -> Result<(), DbError> {
395        self.inner.check_open()?;
396        sql.set_unmetered_only(unmetered_only)?;
397        let inner = self.inner.clone();
398        sql.after_commit(move || {
399            let change = inner.hold_change_blocking();
400            inner.unmetered_only.store(unmetered_only, Ordering::Release);
401            drop(change);
402            if !unmetered_only {
403                inner.upload_wake.notify_one();
404                inner.hold_wake.notify_waiters();
405            }
406        });
407        Ok(())
408    }
409
410    /// Finish transfers before verifying and committing a connection change.
411    /// No upload starts meanwhile. A failed setup leaves the current
412    /// connection installed.
413    pub async fn set_storage(
414        &self,
415        storage: Option<Arc<dyn Storage>>,
416        ready: impl std::future::Future<Output = Result<(), SyncError>>,
417    ) -> Result<(), SyncError> {
418        let _changing = ConnectionChange::start(&self.inner.changing, &self.inner.upload_wake);
419        let _drain = self.inner.drain.lock().await;
420        let _pins = self.inner.pins.lock().await;
421        let _cache = self.inner.cache.write().await;
422        self.inner.check_open()?;
423        ready.await?;
424        *write_unpoisoned(&self.inner.storage) = storage;
425        lock_unpoisoned(&self.inner.connection_waits).clear();
426        Ok(())
427    }
428}
429
430/// A connection change waiting for, then holding, the upload runs. While it
431/// lasts no upload starts, so the run in progress ends once its attempts do;
432/// as it ends, also when its caller is cancelled, the upload worker runs again.
433struct ConnectionChange<'a> {
434    changing: &'a AtomicUsize,
435    wake: &'a Notify,
436}
437impl<'a> ConnectionChange<'a> {
438    fn start(changing: &'a AtomicUsize, wake: &'a Notify) -> Self {
439        changing.fetch_add(1, Ordering::AcqRel);
440        wake.notify_one();
441        Self { changing, wake }
442    }
443}
444impl Drop for ConnectionChange<'_> {
445    fn drop(&mut self) {
446        self.changing.fetch_sub(1, Ordering::AcqRel);
447        self.wake.notify_one();
448    }
449}
450
451/// The upload worker: runs uploads whenever queued work, a changed
452/// prerequisite or a due retry may let one start, while sync is started
453/// (§17.4, §17.9). A run keeps the
454/// upload limit's number in flight until none can start. After each run it
455/// makes the pins coven keeps that may be made (§17.8).
456async fn upload_worker(
457    owner: std::sync::Weak<FilesInner>,
458    stopping: watch::Receiver<bool>,
459    changes: coven_database::DatabaseChanges,
460) {
461    let ended = serve_uploads(owner.clone(), stopping, changes).await;
462    // No run serves an app retry once the worker has ended, so each waiting
463    // one, and each asked after, hears why.
464    if let (Some(ended), Some(owner)) = (ended, owner.upgrade()) {
465        owner.worker_ended(file_upload_pool::WorkerEnd::of(ended));
466    }
467}
468
469/// The upload worker's runs, until it is told to stop or its owner is gone,
470/// which end it with `None`, or until what it watches fails, which ends it
471/// with that failure.
472async fn serve_uploads(
473    owner: std::sync::Weak<FilesInner>,
474    mut stopping: watch::Receiver<bool>,
475    mut changes: coven_database::DatabaseChanges,
476) -> Option<SyncError> {
477    let first = owner.upgrade()?;
478    let parks = first.upload_parks.clone();
479    let _running = parks.running();
480    let wake = first.upload_wake.clone();
481    // Pins coven keeps resume as the store opens (§17.8).
482    wake.notify_one();
483    let mut sending = first.send.watch();
484    let mut syncing = first.waits.syncing();
485    let mut reports = first.waits.network_reports();
486    // The network turning unmetered lets held uploads and pins go (§17.9).
487    let mut network = first.network.clone();
488    drop(first);
489    // The earliest due upload retry, armed after each run (§6.7).
490    let mut retry: crate::retry_waits::Wake = Box::pin(std::future::pending());
491    loop {
492        tokio::select! {
493            biased;
494            never = parks.polling() => match never {},
495            _ = stopping.changed() => return None,
496            result = changes.next() => if let Err(error) = result {
497                if !matches!(error, DbError::StoreClosed) {
498                    tracing::error!(?error, "file commit observation failed");
499                }
500                return Some(error.into());
501            },
502            () = wake.notified() => {},
503            // The store's sequence state, retry waits and network go only
504            // with the store.
505            result = sending.changed() => if result.is_err() { return Some(DbError::StoreClosed.into()) },
506            // Starting sync lets uploads run (§17.4).
507            result = syncing.changed() => if result.is_err() { return Some(DbError::StoreClosed.into()) },
508            result = reports.changed() => if result.is_err() { return Some(DbError::StoreClosed.into()) },
509            () = network.changed() => {},
510            () = &mut retry => {},
511            never = parks.parked() => match never {},
512        }
513        let owner = owner.upgrade()?;
514        let bugs = owner.database.bugs();
515        let run = async {
516            match owner.drain_uploads().await {
517                Ok(_) => (),
518                Err(SyncError::NoStorage) => {
519                    tracing::debug!("file uploads waiting for storage");
520                }
521                Err(SyncError::Storage(error))
522                    if error.failure() == coven_storage::StorageFailure::CatchUpRequired =>
523                {
524                    tracing::debug!("file uploads waiting for a completed catch-up");
525                }
526                Err(SyncError::Database(DbError::StoreClosed)) => (),
527                Err(error) => {
528                    if !coven_foundation::task::report_carried(bugs.as_ref(), &error) {
529                        tracing::error!(?error, "upload queue failed");
530                    }
531                }
532            }
533            // The switches a run made can ask for pins; a paused queue or no
534            // storage holds no pin made from coven's copy.
535            match owner.keep_pinned().await {
536                Ok(()) | Err(SyncError::Database(DbError::StoreClosed)) => (),
537                Err(error) => {
538                    if !coven_foundation::task::report_carried(bugs.as_ref(), &error) {
539                        tracing::error!(?error, "keeping files here failed");
540                    }
541                }
542            }
543        };
544        tokio::select! { biased; _ = stopping.changed() => return None, () = run => {} }
545        // A paused queue still runs its bound uploads, whose retries the
546        // worker must wake for (§17.4).
547        retry = match owner.database.bound_uploads().await {
548            Ok(bound) => {
549                owner.next_upload_retry(&bound).unwrap_or_else(|| Box::pin(std::future::pending()))
550            }
551            Err(DbError::StoreClosed) => return Some(DbError::StoreClosed.into()),
552            // Commits, network reports and wakes still run the worker; no
553            // retry wait wakes it until a later run arms one.
554            Err(error) => {
555                if !coven_foundation::task::report_carried(owner.database.bugs().as_ref(), &error) {
556                    tracing::error!(?error, "reading the bound uploads failed");
557                }
558                Box::pin(std::future::pending())
559            }
560        };
561    }
562}
563
564impl<M: FilesMode> FilesInner<M> {
565    fn connection_due(&self, subject: coven_database::RetrySubject) -> bool {
566        !lock_unpoisoned(&self.connection_waits).contains(&subject)
567    }
568
569    fn wait_for_connection(&self, subject: coven_database::RetrySubject) {
570        lock_unpoisoned(&self.connection_waits).insert(subject);
571    }
572
573    fn forget_connection_wait(&self, subject: coven_database::RetrySubject) {
574        lock_unpoisoned(&self.connection_waits).remove(&subject);
575    }
576
577    /// Keep only the connection waits `keep` accepts.
578    fn retain_connection_waits(&self, keep: impl Fn(&coven_database::RetrySubject) -> bool) {
579        lock_unpoisoned(&self.connection_waits).retain(|subject| keep(subject));
580    }
581
582    /// Hold the lock on one stored object's cache entries while reading
583    /// them, fetching and caching chunks. Files over the same bytes share it.
584    pub(crate) async fn lock_file(
585        &self,
586        file: &coven_database::FileRef,
587        location: &coven_database::FileLocation,
588    ) -> tokio::sync::OwnedMutexGuard<()> {
589        let lock = {
590            let mut reads = lock_unpoisoned(&self.reads);
591            reads.retain(|_, lock| lock.strong_count() != 0);
592            let key = (file.namespace().to_owned(), location.object());
593            match reads.get(&key).and_then(std::sync::Weak::upgrade) {
594                Some(lock) => {
595                    #[cfg(test)]
596                    self.shared_reads.send_modify(|shared| *shared += 1);
597                    lock
598                }
599                None => {
600                    let lock = Arc::new(tokio::sync::Mutex::new(()));
601                    reads.insert(key, Arc::downgrade(&lock));
602                    lock
603                }
604            }
605        };
606        lock.lock_owned().await
607    }
608
609    pub(crate) fn check_open(&self) -> Result<(), DbError> {
610        if self.closed.load(Ordering::Acquire) {
611            Err(DbError::StoreClosed)
612        } else {
613            Ok(())
614        }
615    }
616    /// Whether the setting to move files only on unmetered networks holds a
617    /// transfer now: it is on, the network is metered, and it holds this
618    /// transfer, which for an upload depends on whether it is bound.
619    pub(crate) fn metered_holds(&self, bound: bool) -> bool {
620        self.unmetered_only.load(Ordering::Acquire)
621            && self.network.current().metered
622            && held_on_metered(bound)
623    }
624    /// Whether a pin's download waits for the network now (§17.8).
625    pub(crate) fn downloads_held(&self) -> bool {
626        self.metered_holds(false)
627    }
628    /// Wait until no metered network holds a pin's download: the network
629    /// turns unmetered, or the setting goes off. Fails once the store
630    /// closes.
631    pub(crate) async fn until_downloads_unheld(&self) -> Result<(), DbError> {
632        let mut network = self.network.clone();
633        loop {
634            let woken = self.hold_wake.notified();
635            tokio::pin!(woken);
636            woken.as_mut().enable();
637            self.check_open()?;
638            network.read();
639            if !self.downloads_held() {
640                return Ok(());
641            }
642            #[cfg(test)]
643            let _held = HeldDownload::new(&self.held_downloads);
644            tokio::select! {
645                () = woken => {}
646                () = network.changed() => {}
647            }
648        }
649    }
650    pub(crate) async fn trim(&self, namespace: &str) {
651        if let Err(error) = self.database.trim_cache(namespace).await {
652            // Space reclamation is ancillary to a successful read (§17.8),
653            // but its failure must remain visible in the structured log.
654            tracing::error!(?error, namespace, "cache eviction failed");
655        }
656    }
657}
658impl FilesInner {
659    /// Take the persisted setting to move files only on unmetered networks
660    /// as this owner's copy, as an upload run does for both settings: a pin
661    /// prepared as the store opens holds as the setting says, before any
662    /// upload run has read it.
663    pub(crate) async fn read_unmetered_only(&self) -> Result<(), DbError> {
664        let _change = self.lock_hold_change().await;
665        let unmetered_only = self.database.unmetered_only().await?;
666        self.unmetered_only.store(unmetered_only, Ordering::Release);
667        Ok(())
668    }
669}
670/// A pin's download counted as waiting for the network while it waits.
671#[cfg(test)]
672struct HeldDownload<'a>(&'a tokio::sync::watch::Sender<usize>);
673
674#[cfg(test)]
675impl<'a> HeldDownload<'a> {
676    fn new(held: &'a tokio::sync::watch::Sender<usize>) -> Self {
677        held.send_modify(|held| *held += 1);
678        Self(held)
679    }
680}
681
682#[cfg(test)]
683impl Drop for HeldDownload<'_> {
684    fn drop(&mut self) {
685        self.0.send_modify(|held| *held -= 1);
686    }
687}
688
689/// What holds trailing uploads (§17.4, §17.9).
690#[derive(Clone, Copy, Debug, PartialEq, Eq)]
691pub(crate) enum TrailingHold {
692    /// The app paused uploads.
693    Paused,
694    /// A self-replacement pauses this device's writes, so a trailing
695    /// upload's completion would have to wait for it (§12.3). A bound
696    /// upload's completion goes through: the replacement waits for it.
697    MemberKeysChanging,
698    /// The network is metered and the app moves files only on unmetered
699    /// networks.
700    Metered,
701}
702
703/// Whether the setting to move files only on unmetered networks holds an
704/// upload on a metered network: the trailing ones, as the pause holds them,
705/// since every later write waits for a bound one (§17.4). The Lean model's
706/// `Upload.heldOnMetered`; the views' `coven_uploads.metered` says the same.
707pub(crate) fn held_on_metered(bound: bool) -> bool {
708    !bound
709}
710
711#[cfg(test)]
712#[path = "files_tests.rs"]
713mod tests;