Skip to main content

koan_core/remote/
downloads.rs

1//! The download store — what koan is fetching, and what it just fetched.
2//!
3//! One place every front end reads, rather than each deriving its own answer
4//! from the queue. The queue only knows about transfers for tracks that are in
5//! it, and it forgets a download the instant it lands — which is exactly when
6//! somebody wants to see that it did.
7//!
8//! Progress and structure are deliberately separate. The byte counter is an
9//! `Arc<AtomicU64>` the downloader writes without taking any lock, because it
10//! moves hundreds of times a second; `version` moves only when an entry is
11//! added, finishes or fails. A client polls the counter and watches the
12//! version, and neither costs the download anything.
13
14use std::collections::HashMap;
15use std::path::PathBuf;
16use std::sync::Arc;
17use std::sync::atomic::{AtomicU64, Ordering};
18use std::time::{Duration, Instant};
19
20use crate::player::state::QueueItemId;
21
22/// Where a transfer has got to.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum DownloadState {
25    /// Accepted, not yet started. A queue of six shows five of these.
26    Queued,
27    /// Bytes are arriving.
28    Running,
29    /// Every byte landed and the file is at its final path.
30    Done,
31    /// Gave up. The reason is worth keeping — it is the only account of why a
32    /// track will not play.
33    Failed(String),
34}
35
36impl DownloadState {
37    pub fn is_settled(&self) -> bool {
38        matches!(self, Self::Done | Self::Failed(_))
39    }
40}
41
42/// One transfer.
43#[derive(Debug, Clone)]
44pub struct Download {
45    /// The queue item this is being fetched for. Also the identity of the
46    /// transfer, because a track wanted twice is wanted by two queue entries.
47    pub id: QueueItemId,
48    pub track_id: i64,
49    pub title: String,
50    pub artist: String,
51    /// Where the bytes are being written — the `.part` file.
52    pub source: PathBuf,
53    /// Where they end up.
54    pub dest: PathBuf,
55    /// Total expected, or 0 when the server sent no Content-Length.
56    pub total: u64,
57    /// Live byte count, shared with the downloader. Read it, do not store it.
58    pub written: Arc<ByteFeed>,
59    pub state: DownloadState,
60    /// Bytes per second, smoothed. Zero until there are two samples to take a
61    /// rate from — and zero is the honest answer for a transfer that has
62    /// stopped moving, which is the one worth noticing.
63    pub bytes_per_second: u64,
64}
65
66impl Download {
67    /// 0–1, or `None` when the server never said how big this is.
68    pub fn fraction(&self) -> Option<f64> {
69        (self.total > 0)
70            .then(|| self.written.load(Ordering::Relaxed) as f64 / self.total as f64)
71            .map(|f| f.clamp(0.0, 1.0))
72    }
73
74    pub fn bytes_written(&self) -> u64 {
75        self.written.load(Ordering::Relaxed)
76    }
77}
78
79/// What a transfer is doing, in a form cheap enough to ask about per row.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct Phase {
82    pub state: PhaseKind,
83    pub written: u64,
84    pub total: u64,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum PhaseKind {
89    Queued,
90    Running,
91    Done,
92    Failed,
93}
94
95impl Phase {
96    pub fn is_running(&self) -> bool {
97        matches!(self.state, PhaseKind::Queued | PhaseKind::Running)
98    }
99}
100
101/// A byte count that can be waited on.
102///
103/// The downloader publishes it as chunks land, and a decode thread reading a
104/// file that is still arriving used to look at it every ten milliseconds to
105/// find out whether there was more. Same atomic — every read is unchanged and
106/// costs nothing — with somewhere to wait beside it, so the reader sleeps
107/// until there is something to read and the last poll in the audio path goes
108/// with it.
109#[derive(Debug, Default)]
110pub struct ByteFeed {
111    written: AtomicU64,
112    /// Taken by both sides. A store outside it could land between a reader
113    /// deciding to wait and waiting, and be slept through — which for a stream
114    /// is a stall the length of the whole timeout.
115    at: parking_lot::Mutex<()>,
116    more: parking_lot::Condvar,
117}
118
119impl ByteFeed {
120    pub fn new() -> Arc<Self> {
121        Arc::new(Self::default())
122    }
123
124    /// What has been written so far. Reads take nothing.
125    pub fn load(&self, order: Ordering) -> u64 {
126        self.written.load(order)
127    }
128
129    /// Say how much has been written, and wake whoever is waiting for it.
130    pub fn set(&self, bytes: u64) {
131        let _at = self.at.lock();
132        self.written.store(bytes, Ordering::Release);
133        self.more.notify_all();
134    }
135
136    /// Add to the count, for a writer that knows only how much it just wrote.
137    pub fn advance(&self, bytes: u64) {
138        let _at = self.at.lock();
139        self.written.fetch_add(bytes, Ordering::Release);
140        self.more.notify_all();
141    }
142
143    /// The transfer is over, however it ended. Whoever is waiting wants to
144    /// look at the status now rather than at the byte count.
145    pub fn done(&self) {
146        let _at = self.at.lock();
147        self.more.notify_all();
148    }
149
150    /// Wait for something to happen past `seen` bytes, or until `deadline`.
151    /// Returns what has been written either way.
152    ///
153    /// Returns on any wake, not only on a byte: a transfer that failed has no
154    /// more bytes to offer and the caller has a status to re-read, which is
155    /// the answer it is really waiting for. Deciding that here would be
156    /// deciding it twice.
157    pub fn wait_past(&self, seen: u64, deadline: Instant) -> u64 {
158        let mut at = self.at.lock();
159        let written = self.written.load(Ordering::Acquire);
160        if written > seen {
161            return written;
162        }
163        let Some(left) = deadline.checked_duration_since(Instant::now()) else {
164            return written;
165        };
166        self.more.wait_for(&mut at, left);
167        self.written.load(Ordering::Acquire)
168    }
169}
170
171/// Every transfer koan knows about.
172#[derive(Debug, Default)]
173pub struct DownloadStore {
174    entries: parking_lot::RwLock<Vec<Download>>,
175    version: AtomicU64,
176    /// The last reading taken of each transfer, for working out a rate.
177    /// Separate from the entries so taking a sample does not touch the list
178    /// every client is reading.
179    samples: parking_lot::Mutex<HashMap<QueueItemId, Sample>>,
180    /// When the last reading was taken, and the count that goes with it.
181    ///
182    /// Readings are taken as bytes land rather than on a timer — the thing
183    /// that knows a transfer moved is the code moving it — and a chunk lands
184    /// far more often than a figure needs redrawing, so this is what holds
185    /// them to `MIN_SAMPLE_GAP`.
186    last_sample: parking_lot::Mutex<Option<Instant>>,
187    figures: AtomicU64,
188    /// How many settled entries to keep. This is a view of now, not an archive,
189    /// and old rows would push the live ones off the end of it.
190    settled_limit: usize,
191}
192
193impl DownloadStore {
194    pub fn new() -> Arc<Self> {
195        Arc::new(Self {
196            entries: parking_lot::RwLock::new(Vec::new()),
197            version: AtomicU64::new(0),
198            samples: parking_lot::Mutex::new(HashMap::new()),
199            last_sample: parking_lot::Mutex::new(None),
200            figures: AtomicU64::new(0),
201            settled_limit: 50,
202        })
203    }
204
205    /// Bumped when an entry appears, settles or is forgotten — not when its
206    /// byte count moves. A client redraws its list on this and reads the
207    /// counters every frame regardless.
208    pub fn version(&self) -> u64 {
209        self.version.load(Ordering::Acquire)
210    }
211
212    /// Bumped whenever a byte count or a rate here moved. What a client
213    /// redraws a figure on, as against `version`, which is the list itself
214    /// changing shape.
215    pub fn figures(&self) -> u64 {
216        self.figures.load(Ordering::Acquire)
217    }
218
219    /// Everything, running first, then whatever settled most recently.
220    pub fn all(&self) -> Vec<Download> {
221        self.entries.read().clone()
222    }
223
224    /// The transfer for one queue item, if there is one.
225    ///
226    /// A scan, deliberately: entries are bounded by the number of download
227    /// workers plus the settled tail, because a transfer only appears here
228    /// when a worker picks it up. Indexing tens of rows would cost more to
229    /// maintain than it saves.
230    pub fn get(&self, id: QueueItemId) -> Option<Download> {
231        self.entries.read().iter().find(|d| d.id == id).cloned()
232    }
233
234    /// Whether a transfer exists for this item and what it is doing, without
235    /// cloning its paths and titles — what deriving a queue row needs, per row
236    /// per frame.
237    pub fn phase_of(&self, id: QueueItemId) -> Option<Phase> {
238        self.entries
239            .read()
240            .iter()
241            .find(|d| d.id == id)
242            .map(|d| Phase {
243                state: match &d.state {
244                    DownloadState::Queued => PhaseKind::Queued,
245                    DownloadState::Running => PhaseKind::Running,
246                    DownloadState::Done => PhaseKind::Done,
247                    DownloadState::Failed(_) => PhaseKind::Failed,
248                },
249                written: d.bytes_written(),
250                total: d.total,
251            })
252    }
253
254    /// How many transfers are actually moving.
255    pub fn active(&self) -> usize {
256        self.entries
257            .read()
258            .iter()
259            .filter(|d| !d.state.is_settled())
260            .count()
261    }
262
263    /// Note that a transfer is wanted. Replaces any earlier entry for the same
264    /// queue item — a track cleared and fetched again is the same row starting
265    /// over, not a second one.
266    pub fn queued(&self, download: Download) {
267        let mut entries = self.entries.write();
268        entries.retain(|d| d.id != download.id);
269        entries.insert(0, download);
270        drop(entries);
271        self.settle();
272    }
273
274    /// Bytes have started arriving, and this is how many there are in total.
275    pub fn started(&self, id: QueueItemId, total: u64, written: Arc<ByteFeed>) {
276        let mut entries = self.entries.write();
277        if let Some(entry) = entries.iter_mut().find(|d| d.id == id) {
278            entry.total = total;
279            entry.written = written;
280            entry.state = DownloadState::Running;
281        }
282        drop(entries);
283        self.bump();
284    }
285
286    /// It landed.
287    pub fn finished(&self, id: QueueItemId) {
288        self.settle_one(id, DownloadState::Done);
289    }
290
291    /// It did not.
292    pub fn failed(&self, id: QueueItemId, reason: String) {
293        self.settle_one(id, DownloadState::Failed(reason));
294    }
295
296    /// Drop everything that has already settled. The running ones are not this
297    /// call's business — stopping a transfer is a different verb.
298    pub fn clear_settled(&self) {
299        let mut entries = self.entries.write();
300        let before = entries.len();
301        entries.retain(|d| !d.state.is_settled());
302        let changed = entries.len() != before;
303        drop(entries);
304        if changed {
305            self.bump();
306        }
307    }
308
309    fn settle_one(&self, id: QueueItemId, state: DownloadState) {
310        let mut entries = self.entries.write();
311        if let Some(entry) = entries.iter_mut().find(|d| d.id == id) {
312            entry.state = state;
313            // Said here rather than at the next reading: a transfer that has
314            // finished takes no more readings, and a row left showing the rate
315            // it managed on its last chunk is a row that never stops.
316            entry.bytes_per_second = 0;
317        }
318        drop(entries);
319        self.samples.lock().remove(&id);
320        self.figures.fetch_add(1, Ordering::Release);
321        // `settle` bumps the version, which says so for both.
322        self.settle();
323    }
324
325    /// Keep running transfers at the top and the settled tail bounded.
326    fn settle(&self) {
327        let mut entries = self.entries.write();
328        // Stable, so a list being watched does not shuffle under the pointer.
329        let (mut running, settled): (Vec<_>, Vec<_>) =
330            entries.drain(..).partition(|d| !d.state.is_settled());
331        running.extend(settled.into_iter().take(self.settled_limit));
332        *entries = running;
333        drop(entries);
334        self.bump();
335    }
336
337    fn bump(&self) {
338        self.version.fetch_add(1, Ordering::Release);
339        crate::signal::engine_changed().bump();
340    }
341}
342
343/// The last reading of one transfer.
344#[derive(Debug)]
345struct Sample {
346    at: Instant,
347    bytes: u64,
348    /// Smoothed rate, so a figure on screen does not jump about between frames.
349    bps: f64,
350}
351
352/// How much of a new reading to believe against the running average. Low
353/// enough to be steady, high enough that a transfer stopping shows within a
354/// second or so.
355const RATE_SMOOTHING: f64 = 0.3;
356
357/// Ignore samples closer together than this — over a short enough interval the
358/// arithmetic is mostly noise.
359const MIN_SAMPLE_GAP: Duration = Duration::from_millis(250);
360
361impl DownloadStore {
362    /// Take a rate reading, if one is due.
363    ///
364    /// Called by the downloader as bytes land, not by a timer: what knows a
365    /// transfer moved is the code moving it, and what knows it stopped is the
366    /// absence of the next call. Chunks arrive far faster than a figure needs
367    /// redrawing, so this is gated to `MIN_SAMPLE_GAP` before it touches the
368    /// list every client is reading.
369    ///
370    /// Every running transfer is sampled, not just the one that moved: a
371    /// transfer that has stalled has nothing to report by definition, and its
372    /// figure decaying to zero is the one worth noticing.
373    ///
374    /// Rates live here rather than in each front end because every one of them
375    /// would otherwise keep its own last-reading map and get a different
376    /// answer.
377    pub fn progressed(&self) {
378        let now = Instant::now();
379        {
380            let mut last = self.last_sample.lock();
381            if last.is_some_and(|at| now.saturating_duration_since(at) < MIN_SAMPLE_GAP) {
382                return;
383            }
384            *last = Some(now);
385        }
386        self.sample_rates_at(now);
387    }
388
389    fn sample_rates_at(&self, now: Instant) {
390        let mut entries = self.entries.write();
391        let mut samples = self.samples.lock();
392
393        for entry in entries.iter_mut() {
394            if entry.state.is_settled() {
395                entry.bytes_per_second = 0;
396                samples.remove(&entry.id);
397                continue;
398            }
399            let bytes = entry.written.load(Ordering::Relaxed);
400            match samples.get_mut(&entry.id) {
401                Some(previous) => {
402                    let elapsed = now.saturating_duration_since(previous.at);
403                    if elapsed < MIN_SAMPLE_GAP {
404                        entry.bytes_per_second = previous.bps as u64;
405                        continue;
406                    }
407                    let moved = bytes.saturating_sub(previous.bytes) as f64;
408                    let instant = moved / elapsed.as_secs_f64();
409                    previous.bps = previous.bps * (1.0 - RATE_SMOOTHING) + instant * RATE_SMOOTHING;
410                    previous.at = now;
411                    previous.bytes = bytes;
412                    entry.bytes_per_second = previous.bps as u64;
413                }
414                None => {
415                    samples.insert(
416                        entry.id,
417                        Sample {
418                            at: now,
419                            bytes,
420                            bps: 0.0,
421                        },
422                    );
423                    entry.bytes_per_second = 0;
424                }
425            }
426        }
427
428        // A transfer that left the list leaves its reading behind with it.
429        let live: std::collections::HashSet<QueueItemId> = entries.iter().map(|e| e.id).collect();
430        samples.retain(|id, _| live.contains(id));
431
432        // Said once for the whole reading, so a client redraws every figure
433        // from one moment rather than a row at a time.
434        self.figures.fetch_add(1, Ordering::Release);
435        crate::signal::engine_changed().bump();
436    }
437}
438
439/// The process's download store.
440///
441/// A singleton for the same reason the download queue is one: there is one set
442/// of transfers happening, and everything that reports on them — the queue, the
443/// front ends, the downloader itself — has to be looking at the same set.
444pub fn store() -> &'static Arc<DownloadStore> {
445    static STORE: std::sync::OnceLock<Arc<DownloadStore>> = std::sync::OnceLock::new();
446    STORE.get_or_init(DownloadStore::new)
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452
453    fn download(title: &str) -> Download {
454        Download {
455            id: QueueItemId::new(),
456            track_id: 1,
457            title: title.into(),
458            artist: "Artist".into(),
459            source: PathBuf::from(format!("/cache/{title}.opus.part")),
460            dest: PathBuf::from(format!("/cache/{title}.opus")),
461            total: 0,
462            written: ByteFeed::new(),
463            state: DownloadState::Queued,
464            bytes_per_second: 0,
465        }
466    }
467
468    #[test]
469    fn a_transfer_runs_then_settles() {
470        let store = DownloadStore::new();
471        let entry = download("train");
472        let id = entry.id;
473        store.queued(entry);
474        assert_eq!(store.active(), 1);
475
476        let written = ByteFeed::new();
477        store.started(id, 400, written.clone());
478        written.set(100);
479        assert_eq!(store.all()[0].fraction(), Some(0.25));
480
481        store.finished(id);
482        assert_eq!(store.active(), 0);
483        assert_eq!(store.all()[0].state, DownloadState::Done);
484    }
485
486    #[test]
487    fn progress_does_not_move_the_version() {
488        // The counter is read every frame and the list is rebuilt on the
489        // version; if bytes bumped it, every client would rebuild at the rate
490        // the download writes.
491        let store = DownloadStore::new();
492        let entry = download("train");
493        let id = entry.id;
494        store.queued(entry);
495        let written = ByteFeed::new();
496        store.started(id, 1000, written.clone());
497
498        let before = store.version();
499        written.set(500);
500        assert_eq!(store.version(), before);
501        assert_eq!(store.all()[0].bytes_written(), 500);
502    }
503
504    #[test]
505    fn no_content_length_means_no_fraction() {
506        // A bar drawn at zero for a transfer that is going fine reads as stuck.
507        let store = DownloadStore::new();
508        let entry = download("chunked");
509        let id = entry.id;
510        store.queued(entry);
511        store.started(id, 0, {
512            let feed = ByteFeed::new();
513            feed.set(9000);
514            feed
515        });
516        assert_eq!(store.all()[0].fraction(), None);
517        assert_eq!(store.all()[0].bytes_written(), 9000);
518    }
519
520    #[test]
521    fn fetching_the_same_item_again_restarts_its_row() {
522        // Clearing a download and playing the track again is the same transfer
523        // starting over, not a second one to scroll past.
524        let store = DownloadStore::new();
525        let first = download("train");
526        let id = first.id;
527        store.queued(first);
528        store.finished(id);
529
530        let mut again = download("train");
531        again.id = id;
532        store.queued(again);
533
534        assert_eq!(store.all().len(), 1);
535        assert_eq!(store.all()[0].state, DownloadState::Queued);
536    }
537
538    #[test]
539    fn running_transfers_sort_above_settled_ones() {
540        let store = DownloadStore::new();
541        let done = download("done");
542        let done_id = done.id;
543        store.queued(done);
544        let running = download("running");
545        store.queued(running);
546        store.finished(done_id);
547
548        let all = store.all();
549        assert_eq!(all[0].title, "running");
550        assert_eq!(all[1].title, "done");
551    }
552
553    #[test]
554    fn a_failure_keeps_its_reason() {
555        let store = DownloadStore::new();
556        let entry = download("gone");
557        let id = entry.id;
558        store.queued(entry);
559        store.failed(id, "server returned 404".into());
560        assert_eq!(
561            store.all()[0].state,
562            DownloadState::Failed("server returned 404".into())
563        );
564    }
565
566    #[test]
567    fn a_rate_needs_two_readings_and_a_gap_between_them() {
568        let store = DownloadStore::new();
569        let entry = download("train");
570        let (id, written) = (entry.id, entry.written.clone());
571        store.queued(entry);
572        store.started(id, 1_000_000, written.clone());
573
574        let start = Instant::now();
575        store.sample_rates_at(start);
576        assert_eq!(
577            store.all()[0].bytes_per_second,
578            0,
579            "one reading is not a rate"
580        );
581
582        // A second too close to the first says nothing.
583        written.set(100_000);
584        store.sample_rates_at(start + Duration::from_millis(50));
585        assert_eq!(store.all()[0].bytes_per_second, 0);
586
587        // A second far enough away does. Smoothed, so it reads low at first.
588        store.sample_rates_at(start + Duration::from_secs(1));
589        let bps = store.all()[0].bytes_per_second;
590        assert!(bps > 0, "a rate should have been worked out, got {bps}");
591        assert!(bps < 100_000, "and smoothed rather than taken whole: {bps}");
592    }
593
594    #[test]
595    fn a_settled_transfer_has_no_rate() {
596        // Zero, not the speed it happened to be going when it stopped.
597        let store = DownloadStore::new();
598        let entry = download("train");
599        let (id, written) = (entry.id, entry.written.clone());
600        store.queued(entry);
601        store.started(id, 1000, written.clone());
602        let start = Instant::now();
603        store.sample_rates_at(start);
604        written.set(500);
605        store.sample_rates_at(start + Duration::from_secs(1));
606        assert!(store.all()[0].bytes_per_second > 0);
607
608        store.finished(id);
609        store.sample_rates_at(start + Duration::from_secs(2));
610        assert_eq!(store.all()[0].bytes_per_second, 0);
611    }
612
613    #[test]
614    fn a_transfer_can_be_found_by_its_queue_item() {
615        let store = DownloadStore::new();
616        let entry = download("train");
617        let (id, written) = (entry.id, entry.written.clone());
618        store.queued(entry);
619        store.started(id, 400, written.clone());
620        written.set(100);
621
622        let phase = store.phase_of(id).expect("the transfer is there");
623        assert_eq!(phase.state, PhaseKind::Running);
624        assert_eq!(phase.written, 100);
625        assert_eq!(phase.total, 400);
626        assert!(phase.is_running());
627
628        assert!(
629            store.phase_of(QueueItemId::new()).is_none(),
630            "and only that one"
631        );
632    }
633
634    #[test]
635    fn clearing_settled_leaves_the_running_alone() {
636        let store = DownloadStore::new();
637        let done = download("done");
638        let done_id = done.id;
639        store.queued(done);
640        store.queued(download("running"));
641        store.finished(done_id);
642
643        store.clear_settled();
644        let all = store.all();
645        assert_eq!(all.len(), 1);
646        assert_eq!(all[0].title, "running");
647    }
648}