Skip to main content

#
jellytau_lib/download/
stop.rs

    1//! Stop signalling for in-flight downloads.
    2//!
    3//! TRACES: UR-055 | DR-168
    4//!
    5//! Pausing and cancelling used to be database-only: `pause_download` wrote
    6//! `status = 'paused'` and nothing else. No cancellation existed anywhere in the
    7//! download stack — no token, no flag, no abort — so the streaming task kept
    8//! running, kept writing bytes, and on finishing overwrote the row with
    9//! `completed` or `failed`. The row flicked to "paused" and then undid itself,
    10//! which is precisely the reported "pause does not work".
    11//!
    12//! This is the missing half: a flag per in-flight download that the worker reads
    13//! between chunks. Setting it makes the worker return [`Stopped`] promptly and
    14//! leave the `.part` file **intact**, which is what lets a resume pick up from
    15//! where it stopped via the existing HTTP Range request.
    16//!
    17//! Kept as a module-level registry rather than on `DownloadManager` because the
    18//! two sides never meet: the command handler holds the manager's lock, while the
    19//! worker runs detached inside `tauri::async_runtime::spawn` with no access to
    20//! Tauri state. A registry both can reach is the smallest thing that works.
    21//!
    22//! [`Stopped`]: crate::download::worker::DownloadError::Stopped
    23
    24use std::collections::HashMap;
    25use std::sync::atomic::{AtomicBool, Ordering};
    26use std::sync::{Arc, Mutex, OnceLock};
    27
    28use crate::utils::lock::MutexSafe;
    29
    30/// download id → its stop flag, for downloads currently in flight.
    31fn registry() -> &'static Mutex<HashMap<i64, Arc<AtomicBool>>> {
    32    static REGISTRY: OnceLock<Mutex<HashMap<i64, Arc<AtomicBool>>>> = OnceLock::new();
    33    REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
    34}
    35
    36/// Register `download_id` as in-flight and hand back its stop flag.
    37///
    38/// Called by the worker as it starts. A previous flag for the same id is
    39/// replaced, so a download that is paused and later resumed does not inherit the
    40/// set flag from its last run and stop immediately.
    41pub fn register(download_id: i64) -> Arc<AtomicBool> {
    42    let flag = Arc::new(AtomicBool::new(false));
    43    registry().lock_safe().insert(download_id, flag.clone());
    44    flag
    45}
    46
    47/// Ask an in-flight download to stop.
    48///
    49/// Returns whether one was actually in flight — the caller uses this to tell a
    50/// running download (which will stop shortly) from a merely queued one (which
    51/// the database update alone has already handled).
    52pub fn signal(download_id: i64) -> bool {
    53    match registry().lock_safe().get(&download_id) {
    54        Some(flag) => {
    55            flag.store(true, Ordering::SeqCst);
    56            true
    57        }
    58        None => false,
    59    }
    60}
    61
    62/// Forget a download's flag. Called when its task finishes, however it ended.
    63pub fn clear(download_id: i64) {
    64    registry().lock_safe().remove(&download_id);
    65}
    66
    67/// Whether a stop has been requested for `download_id`.
    68///
    69/// The worker reads its own `Arc<AtomicBool>` directly rather than looking the id
    70/// up, so this exists for the tests that assert the registry's behaviour.
    71#[cfg(test)]
    72pub fn is_stopping(download_id: i64) -> bool {
    73    registry()
    74        .lock_safe()
    75        .get(&download_id)
    76        .map(|f| f.load(Ordering::SeqCst))
    77        .unwrap_or(false)
    78}
    79
    80#[cfg(test)]
    81mod tests {
    82    use super::*;
    83
    84    /// Ids are per-test so the shared registry cannot leak between them.
    85    fn unique_id(seed: i64) -> i64 {
    86        900_000 + seed
    87    }
    88
    89    #[test]
    90    fn test_a_registered_download_starts_unflagged() {
    91        let id = unique_id(1);
    92        let flag = register(id);
    93        assert!(!flag.load(Ordering::SeqCst));
    94        assert!(!is_stopping(id));
    95        clear(id);
    96    }
    97
    98    #[test]
    99    fn test_signal_sets_the_flag_the_worker_reads() {
    100        let id = unique_id(2);
    101        let flag = register(id);
    102
    103        assert!(signal(id), "a registered download reports as in flight");
    104        assert!(
    105            flag.load(Ordering::SeqCst),
    106            "the worker's own handle sees it"
    107        );
    108        assert!(is_stopping(id));
    109
    110        clear(id);
    111    }
    112
    113    /// The pump only needs to abort a task that exists; a queued row is handled
    114    /// by its database status alone.
    115    #[test]
    116    fn test_signalling_an_unregistered_download_reports_not_in_flight() {
    117        assert!(!signal(unique_id(3)));
    118    }
    119
    120    #[test]
    121    fn test_clear_forgets_the_download() {
    122        let id = unique_id(4);
    123        register(id);
    124        signal(id);
    125        clear(id);
    126
    127        assert!(!is_stopping(id));
    128        assert!(!signal(id), "a cleared download is no longer in flight");
    129    }
    130
    131    /// The bug this guards: pause sets the flag, and resume re-runs the same
    132    /// download id. If registering reused the old flag, the resumed run would see
    133    /// a set flag and stop instantly — a download that could never be resumed.
    134    #[test]
    135    fn test_reregistering_clears_a_previous_stop() {
    136        let id = unique_id(5);
    137        register(id);
    138        signal(id);
    139        assert!(is_stopping(id));
    140
    141        let fresh = register(id);
    142        assert!(!fresh.load(Ordering::SeqCst));
    143        assert!(
    144            !is_stopping(id),
    145            "a resumed download must not inherit the pause"
    146        );
    147
    148        clear(id);
    149    }
    150}