Skip to main content

runnerd_core/
store.rs

1//! The job history: a SQLite database of jobs, their runners, steps, samples and logs, the events clients resume
2//! from, cache sizes, the paused repositories and where the notifier got to. Owns the schema and its migrations, the one writer and the event
3//! broadcast. Writes are messages to a writer thread, so the engine never waits on the disk; reads and pruning run
4//! on Tokio's blocking pool, pruning on its own connection, a batch at a time. It decides nothing about scheduling
5//! and stores no secret: no token, no JIT config, no runner `_diag` log.
6
7use std::collections::BTreeMap;
8use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt};
9use std::path::{Path, PathBuf};
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::sync::{Arc, Mutex, TryLockError};
12use std::time::Duration;
13
14use rusqlite::{Connection, OpenFlags};
15use tokio::sync::{OwnedSemaphorePermit, Semaphore, broadcast, mpsc, oneshot};
16use tracing::warn;
17
18use crate::api::{self, Event, EventEnvelope};
19use crate::config::History;
20use crate::github::{JobDetails, QueuedJob};
21use crate::observe::metrics::Events;
22use crate::scheduler::warm;
23use crate::sizing;
24use crate::sync::lock;
25
26mod prune;
27mod read;
28mod write;
29
30/// The schema's migrations, in order; the database's `user_version` is how many ran.
31const MIGRATIONS: &[&str] = &[
32    include_str!("store/migrations/0001_history.sql"),
33    include_str!("store/migrations/0002_runner_caps.sql"),
34    include_str!("store/migrations/0003_paused.sql"),
35    include_str!("store/migrations/0004_prune.sql"),
36    include_str!("store/migrations/0005_announced_profiles.sql"),
37    include_str!("store/migrations/0006_records.sql"),
38    include_str!("store/migrations/0007_admission.sql"),
39    include_str!("store/migrations/0008_memory_without_page_cache.sql"),
40    include_str!("store/migrations/0009_auto_sizing.sql"),
41    include_str!("store/migrations/0010_auto_sizing_cpus.sql"),
42    include_str!("store/migrations/0011_notify.sql"),
43    include_str!("store/migrations/0012_build_cache.sql"),
44    include_str!("store/migrations/0013_host_memory.sql"),
45];
46
47/// Bytes of log lines sent and not yet stored, at most: a job's capture waits beyond it, so a noisy job or a slow
48/// disk holds back that job's log, not the daemon's memory.
49pub(crate) const LOG_BACKLOG: u64 = 8 << 20;
50
51/// Host samples the writer holds before it stores them in one transaction: a minute's worth. One transaction per
52/// sample wrote about 300 MB a day to an idle daemon's WAL. A crash loses the samples held.
53const HOST_SAMPLE_BATCH: usize = 30;
54
55/// How many events a slow client may fall behind before its stream ends; it reconnects with `Last-Event-ID`.
56const EVENT_BUFFER: usize = 4096;
57
58/// Why the database cannot be used.
59#[derive(Debug, thiserror::Error)]
60pub enum StoreError {
61    /// SQLite failed.
62    #[error("{0}")]
63    Sqlite(#[from] rusqlite::Error),
64    /// A newer runnerd wrote the database.
65    #[error(
66        "the job history database is version {found}, newer than the {supported} this runnerd reads; \
67         run a newer runnerd or move the database aside"
68    )]
69    Newer {
70        /// The database's version.
71        found: u32,
72        /// The newest version this build knows.
73        supported: u32,
74    },
75    /// The directory or file cannot be created.
76    #[error("{0}")]
77    Io(#[from] std::io::Error),
78    /// A blocking read did not finish.
79    #[error("a database read stopped: {0}")]
80    Join(#[from] tokio::task::JoinError),
81    /// The database's `-wal` file holds changes not yet in it, and this user cannot open the database the way that
82    /// reads them.
83    #[error(
84        "{} holds changes not yet in the job history, which this user cannot read; run runnerd doctor as the user \
85         the daemon runs as",
86        .0.display()
87    )]
88    Unread(PathBuf),
89}
90
91/// A runner container as the history records it.
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct RunnerRecord {
94    /// The runner's name, also its container's.
95    pub name: String,
96    /// `owner/name`.
97    pub repository: String,
98    /// The profile it runs in.
99    pub profile: String,
100    /// 1-based.
101    pub slot: usize,
102    /// The Docker container ID.
103    pub container: String,
104    /// When the container started, in milliseconds since the epoch.
105    pub started_at: u64,
106    /// The container's memory cap in bytes.
107    pub memory_cap: Option<u64>,
108    /// The container's CPU limit in millicores.
109    pub cpu_cap: Option<u64>,
110    /// How it was admitted; `None` for one a restart adopted without a record of it.
111    pub admission: Option<api::Admission>,
112}
113
114/// One sample of one runner's container.
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct SampleRecord {
117    /// The runner.
118    pub runner: String,
119    /// When, in milliseconds since the epoch.
120    pub at: u64,
121    /// Millicores over the interval before it.
122    pub cpu: u64,
123    /// CPU time so far, in microseconds.
124    pub cpu_usage: u64,
125    /// Bytes without page cache (`observe::metrics::Usage::memory`).
126    pub memory: u64,
127    /// Bytes received since the container started.
128    pub network_received: u64,
129    /// Bytes sent since the container started.
130    pub network_sent: u64,
131    /// The container's `memory.events` so far.
132    pub events: Events,
133}
134
135/// The last measurement of the cache volumes.
136#[derive(Debug, Clone, Default, PartialEq, Eq)]
137pub struct Measured {
138    /// When it was taken (seconds); none before the first measurement.
139    pub at: Option<u64>,
140    /// Each volume's size in bytes, by name.
141    pub bytes: BTreeMap<String, u64>,
142}
143
144/// The profiles and repositories as the stored events last announced them: what clients resuming after those
145/// events have.
146#[derive(Debug, Clone, Default, PartialEq, Eq)]
147pub struct Announced {
148    /// The last `profile.changed` of each profile not removed since, by name.
149    pub profiles: Vec<api::ProfileStatus>,
150    /// The last `repository.changed` of each repository not removed since, by name.
151    pub repositories: Vec<api::RepositoryStatus>,
152}
153
154/// A job the history has not closed, as the engine needs it after a restart.
155#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct OpenJob {
157    /// GitHub's job ID.
158    pub id: u64,
159    /// `owner/name`.
160    pub repository: String,
161    /// `Queued` or `Running`.
162    pub state: api::JobState,
163    /// Its runner, once one took it.
164    pub runner: Option<String>,
165    /// When its runner was removed, in milliseconds since the epoch.
166    pub runner_removed_at: Option<u64>,
167    /// The workflow run it belongs to.
168    pub run_id: u64,
169    /// 1 for the run's first attempt.
170    pub run_attempt: u32,
171    /// The workflow's name, when known.
172    pub workflow: Option<String>,
173    /// When GitHub queued it, in milliseconds since the epoch.
174    pub queued_at: u64,
175    /// The labels it asked for, which tell its repository's pool.
176    pub labels: Vec<String>,
177}
178
179/// Where a job's log capture continues: the runner's page file and the byte offset in it.
180#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181pub struct LogPosition {
182    /// The page number, from 1.
183    pub page: u64,
184    /// Bytes of that page already stored as lines.
185    pub offset: u64,
186}
187
188/// How far the mirror's log was counted: the time of the last response counted, in nanoseconds since the epoch as
189/// Docker logs it, and how many responses logged at exactly that time were counted, so the next read counts the
190/// ones it was too early to see.
191#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
192pub struct MirrorPosition {
193    /// Nanoseconds since the epoch.
194    pub at: u64,
195    /// Responses logged at `at` already counted.
196    pub seen: u64,
197}
198
199/// What the writer thread is asked to do, in order.
200#[derive(Debug)]
201enum Write {
202    Queued {
203        repository: String,
204        job: QueuedJob,
205    },
206    RunnerStarted(RunnerRecord),
207    RunnerAdmission {
208        runner: String,
209        admission: api::Admission,
210    },
211    RunnerOomKilled(String),
212    HostWaited {
213        jobs: Vec<u64>,
214        ms: u64,
215    },
216    BuildCache {
217        runner: String,
218        counts: api::BuildCacheCounts,
219    },
220    RunnerRemoved {
221        runner: String,
222        at: u64,
223    },
224    RunnersGone {
225        running: Vec<String>,
226        at: u64,
227    },
228    Claimed {
229        job: u64,
230        run_id: u64,
231        runner: String,
232        at: u64,
233    },
234    Details {
235        details: JobDetails,
236    },
237    Elsewhere {
238        details: JobDetails,
239    },
240    Lost {
241        job: u64,
242        at: u64,
243    },
244    Log {
245        job: u64,
246        lines: Vec<String>,
247        /// Where the capture resumes once these lines are stored; `None` for all but the last part of a batch.
248        position: Option<LogPosition>,
249        max_bytes: u64,
250        /// The lines' share of `LOG_BACKLOG`, given back once they are stored.
251        _backlog: OwnedSemaphorePermit,
252    },
253    Samples(Vec<SampleRecord>),
254    HostSample(api::HostSample),
255    MirrorBytes {
256        bytes: Vec<(String, u64)>,
257        until: MirrorPosition,
258        /// Whether the count committed.
259        done: oneshot::Sender<bool>,
260    },
261    CacheSizes {
262        at: u64,
263        sizes: BTreeMap<String, u64>,
264    },
265    /// Boxed: an event with a job's summary is many times the other writes' size.
266    Event(Box<Event>),
267    RecordMade(api::Record),
268    RecordSkipped {
269        job: u64,
270        why: String,
271    },
272    RecordRemoved {
273        job: u64,
274        at: u64,
275        by: api::RecordRemoval,
276    },
277    Paused {
278        repositories: Vec<String>,
279        /// Whether it committed.
280        done: oneshot::Sender<bool>,
281    },
282    SizingIntent {
283        change: api::SizingChange,
284        /// Its row, once it committed.
285        done: oneshot::Sender<Option<i64>>,
286    },
287    SizingResolved {
288        id: i64,
289        resolution: Resolution,
290        /// Whether it committed.
291        done: oneshot::Sender<bool>,
292    },
293    Notify {
294        change: NotifyChange,
295        /// Whether it committed.
296        done: oneshot::Sender<bool>,
297    },
298    Flush(oneshot::Sender<()>),
299}
300
301/// What the notifier records.
302#[derive(Debug)]
303enum NotifyChange {
304    /// Sets a target's keys and removes others.
305    Record {
306        target: String,
307        set: Vec<(String, u64)>,
308        remove: Vec<String>,
309    },
310    /// Forgets every target but these.
311    Keep(Vec<String>),
312}
313
314/// What became of a pending auto sizing change.
315#[derive(Debug, Clone, Copy, PartialEq, Eq)]
316pub enum Resolution {
317    /// The config file has it: announced as `sizing.changed`.
318    Done,
319    /// The file was not written.
320    Failed,
321    /// Left pending, and the config no longer says whether it was written: it keeps its interval and stays listed,
322    /// with what it replaced.
323    Uncertain,
324}
325
326/// Auto sizing as the history keeps it: when each profile last changed, and changes not yet known to be written.
327#[derive(Debug, Clone, Default, PartialEq, Eq)]
328pub struct AutoSizing {
329    /// The latest pending or done change of each profile, in seconds, however old.
330    pub last: BTreeMap<String, u64>,
331    /// Changes recorded before their file was written and not resolved since, by row.
332    pub pending: Vec<(i64, api::SizingChange)>,
333    /// Each profile's `cpus` as the daemon ran them, from its `profile.changed` events, oldest first: those `[history]
334    /// events` keeps, and each profile's latest however old.
335    pub cpus: Vec<sizing::CpusSet>,
336}
337
338/// The job history. Cloning shares it.
339#[derive(Debug, Clone)]
340pub struct Store {
341    writes: mpsc::UnboundedSender<Write>,
342    reader: Arc<Mutex<Connection>>,
343    /// Taken with `try_lock`, so one prune runs at a time; one that panicked midway left rows the next one deletes.
344    pruner: Arc<Mutex<Connection>>,
345    /// Set by `close`: a running prune stops after its batch, and none starts.
346    closed: Arc<AtomicBool>,
347    /// Host samples sent and not yet stored, oldest first; reads include them. The writer removes them once stored.
348    // Locked with `sync::lock`: every critical section is a push, a copy or a drain.
349    held: Arc<Mutex<Vec<api::HostSample>>>,
350    /// The last host sample sent, for admission to read without a query.
351    latest_host: Arc<Mutex<Option<api::HostSample>>>,
352    /// The last reading of the host's memory, which is not stored.
353    latest_host_memory: Arc<Mutex<Option<api::HostMemory>>>,
354    /// Each running runner's last sample, for admission to read without a query.
355    latest_samples: Arc<Mutex<BTreeMap<String, SampleRecord>>>,
356    events: broadcast::Sender<Arc<EventEnvelope>>,
357    backlog: Arc<Semaphore>,
358}
359
360impl Store {
361    /// Opens the database at `path`, creating it and its directory (mode 0700) if missing, and migrates it to the
362    /// latest version. Refuses a database a newer runnerd wrote.
363    pub fn open(path: &Path) -> Result<Store, StoreError> {
364        if let Some(dir) = path.parent().filter(|dir| !dir.as_os_str().is_empty()) {
365            std::fs::DirBuilder::new()
366                .recursive(true)
367                .mode(0o700)
368                .create(dir)?;
369        }
370        // Owner-only, as SQLite then makes its `-wal` and `-shm` files: logs may say more than the owner wants shared.
371        std::fs::OpenOptions::new()
372            .create(true)
373            .append(true)
374            .mode(0o600)
375            .open(path)?;
376        let mut db = Connection::open(path)?;
377        configure(&db)?;
378        migrate(&mut db)?;
379        let reader = Connection::open_with_flags(
380            path,
381            OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
382        )?;
383        configure(&reader)?;
384        let pruner = Connection::open(path)?;
385        configure(&pruner)?;
386        let (events, _) = broadcast::channel(EVENT_BUFFER);
387        let (writes, receiver) = mpsc::unbounded_channel();
388        let thread_events = events.clone();
389        let held = Arc::new(Mutex::new(Vec::new()));
390        let thread_held = held.clone();
391        std::thread::Builder::new()
392            .name("runnerd-store".to_owned())
393            .spawn(move || write::run(db, receiver, &thread_events, &thread_held))?;
394        Ok(Store {
395            writes,
396            reader: Arc::new(Mutex::new(reader)),
397            pruner: Arc::new(Mutex::new(pruner)),
398            closed: Arc::new(AtomicBool::new(false)),
399            held,
400            latest_host: Arc::new(Mutex::new(None)),
401            latest_host_memory: Arc::new(Mutex::new(None)),
402            latest_samples: Arc::new(Mutex::new(BTreeMap::new())),
403            events,
404            backlog: Arc::new(Semaphore::new(
405                usize::try_from(LOG_BACKLOG).unwrap_or(usize::MAX),
406            )),
407        })
408    }
409
410    fn send(&self, write: Write) {
411        // Fails only once the writer thread is gone, after a SQLite error it logged.
412        let _ = self.writes.send(write);
413    }
414
415    /// Records a queued job the repository's runners may run; announces it the first time.
416    pub fn queued(&self, repository: &str, job: &QueuedJob) {
417        self.send(Write::Queued {
418            repository: repository.to_owned(),
419            job: job.clone(),
420        });
421    }
422
423    /// Records a runner container that started, or one an earlier daemon started and this one adopted.
424    pub fn runner_started(&self, runner: RunnerRecord) {
425        self.send(Write::RunnerStarted(runner));
426    }
427
428    /// Records that a runner's container and registration were removed at `at` (ms).
429    pub fn runner_removed(&self, runner: &str, at: u64) {
430        lock(&self.latest_samples).remove(runner);
431        self.send(Write::RunnerRemoved {
432            runner: runner.to_owned(),
433            at,
434        });
435    }
436
437    /// Records every runner the history holds open, except the `running` ones, removed at `at` (ms): what recovery
438    /// found gone, so it stops counting as holding a slot.
439    pub fn runners_gone(&self, running: Vec<String>, at: u64) {
440        lock(&self.latest_samples).retain(|runner, _| running.contains(runner));
441        self.send(Write::RunnersGone { running, at });
442    }
443
444    /// Records that a runner took a job at `at` (ms).
445    pub fn claimed(&self, job: u64, run_id: u64, runner: &str, at: u64) {
446        self.send(Write::Claimed {
447            job,
448            run_id,
449            runner: runner.to_owned(),
450            at,
451        });
452    }
453
454    /// Records a running job's progress and steps as GitHub reports them; announces changed steps.
455    pub fn details(&self, details: JobDetails) {
456        self.send(Write::Details { details });
457    }
458
459    /// Closes a queued job that left the queue without a runner of this daemon: another machine's runner took it, or
460    /// none did and it has no start.
461    pub fn elsewhere(&self, details: JobDetails) {
462        self.send(Write::Elsewhere { details });
463    }
464
465    /// Closes a job whose container is gone and that GitHub did not report completed in time.
466    pub fn lost(&self, job: u64, at: u64) {
467        self.send(Write::Lost { job, at });
468    }
469
470    /// Appends log lines to a job, up to `max_bytes` per job, and records where the capture continues. Waits while
471    /// `LOG_BACKLOG` bytes of lines are waiting to be stored; a larger batch goes in parts, each waiting its turn.
472    pub async fn log(&self, job: u64, lines: Vec<String>, position: LogPosition, max_bytes: u64) {
473        let size = |line: &String| line.len() as u64 + 1;
474        let mut lines = lines.into_iter().map(|mut line| {
475            // No line takes the whole backlog; the capture already cuts lines far shorter.
476            if size(&line) > LOG_BACKLOG {
477                let mut end = usize::try_from(LOG_BACKLOG / 2).unwrap_or(usize::MAX);
478                while !line.is_char_boundary(end) {
479                    end -= 1;
480                }
481                line.truncate(end);
482            }
483            line
484        });
485        let mut next = lines.next();
486        loop {
487            let (mut part, mut bytes) = (Vec::new(), 0);
488            while let Some(line) = next.take_if(|line| bytes + size(line) <= LOG_BACKLOG) {
489                bytes += size(&line);
490                part.push(line);
491                next = lines.next();
492            }
493            let last = next.is_none();
494            let share = u32::try_from(bytes.max(1)).unwrap_or(u32::MAX);
495            // The semaphore is never closed.
496            let Ok(backlog) = self.backlog.clone().acquire_many_owned(share).await else {
497                return;
498            };
499            self.send(Write::Log {
500                job,
501                lines: part,
502                position: last.then_some(position),
503                max_bytes,
504                _backlog: backlog,
505            });
506            if last {
507                return;
508            }
509        }
510    }
511
512    /// Records samples of runner containers.
513    pub fn samples(&self, samples: Vec<SampleRecord>) {
514        let mut latest = lock(&self.latest_samples);
515        for sample in &samples {
516            latest.insert(sample.runner.clone(), sample.clone());
517        }
518        drop(latest);
519        self.send(Write::Samples(samples));
520    }
521
522    /// Records a sample of the slice and sends it to every subscriber at once; it is stored with the next ones, a
523    /// minute's worth in one transaction, or at `flush`.
524    pub fn host_sample(&self, sample: api::HostSample) {
525        *lock(&self.latest_host) = Some(sample.clone());
526        lock(&self.held).push(sample.clone());
527        self.send(Write::HostSample(sample));
528    }
529
530    /// A runner's last sample, stored or not; `None` before its first sample or once it is removed.
531    pub fn latest_sample(&self, runner: &str) -> Option<SampleRecord> {
532        lock(&self.latest_samples).get(runner).cloned()
533    }
534
535    /// Records what a runner reserves now, when measured admission changed it after the start.
536    pub fn runner_admission(&self, runner: &str, admission: api::Admission) {
537        self.send(Write::RunnerAdmission {
538            runner: runner.to_owned(),
539            admission,
540        });
541    }
542
543    /// Records that Docker saw the kernel kill a process in a runner's container for memory, which its samples may
544    /// have missed: one in the last interval before the container stopped.
545    pub fn runner_oom_killed(&self, runner: &str) {
546        self.send(Write::RunnerOomKilled(runner.to_owned()));
547    }
548
549    /// Adds what the shared build cache served a runner and took from it, and records whether its job could write.
550    pub fn build_cache(&self, runner: &str, counts: api::BuildCacheCounts) {
551        self.send(Write::BuildCache {
552            runner: runner.to_owned(),
553            counts,
554        });
555    }
556
557    /// Keeps the latest reading of the host's memory for admission and the status; it is not stored.
558    pub fn host_memory(&self, memory: api::HostMemory) {
559        *lock(&self.latest_host_memory) = Some(memory);
560    }
561
562    /// The latest reading of the host's memory; `None` before the first.
563    pub fn latest_host_memory(&self) -> Option<api::HostMemory> {
564        lock(&self.latest_host_memory).clone()
565    }
566
567    /// Adds `ms` to how long each of `jobs` waited because the host's memory could not take its runner, which
568    /// sizing does not read as a wait for a slot.
569    pub fn host_waited(&self, jobs: Vec<u64>, ms: u64) {
570        self.send(Write::HostWaited { jobs, ms });
571    }
572
573    /// The last host sample sent, stored or not; `None` before the first.
574    pub fn latest_host_sample(&self) -> Option<api::HostSample> {
575        lock(&self.latest_host).clone()
576    }
577
578    /// Adds bytes the mirror served to runners, counted from its log up to `until`, and says whether that was
579    /// stored: if not, the caller counts the same lines again.
580    pub async fn mirror_bytes(&self, bytes: Vec<(String, u64)>, until: MirrorPosition) -> bool {
581        let (done, committed) = oneshot::channel();
582        self.send(Write::MirrorBytes { bytes, until, done });
583        committed.await.unwrap_or(false)
584    }
585
586    /// Records volume sizes in bytes, by name, measured at `at` (ms).
587    pub fn cache_sizes(&self, at: u64, sizes: BTreeMap<String, u64>) {
588        self.send(Write::CacheSizes { at, sizes });
589    }
590
591    /// Stores a durable event, or sends a live one, to every subscriber.
592    pub fn event(&self, event: Event) {
593        self.send(Write::Event(Box::new(event)));
594    }
595
596    /// Records that a failed job's runner was kept as `record`, and announces it.
597    pub fn record_made(&self, record: api::Record) {
598        self.send(Write::RecordMade(record));
599    }
600
601    /// Records why a failed job's runner was not kept.
602    pub fn record_skipped(&self, job: u64, why: String) {
603        self.send(Write::RecordSkipped { job, why });
604    }
605
606    /// Records that a job's record was removed at `at` (ms), and why, and announces it.
607    pub fn record_removed(&self, job: u64, at: u64, by: api::RecordRemoval) {
608        self.send(Write::RecordRemoved { job, at, by });
609    }
610
611    /// Records the paused repositories, all of them, in place of the ones recorded before. The write is sent at
612    /// once, in order with every other; the future says whether it committed.
613    pub fn paused(&self, repositories: Vec<String>) -> impl Future<Output = bool> + use<> {
614        let (done, committed) = oneshot::channel();
615        self.send(Write::Paused { repositories, done });
616        async move { committed.await.unwrap_or(false) }
617    }
618
619    /// Records a change auto sizing is about to write, as pending, before the file is, synced to disk. The future
620    /// has its row once it committed, `None` if it did not: then nothing may be written.
621    pub fn sizing_intent(
622        &self,
623        change: api::SizingChange,
624    ) -> impl Future<Output = Option<i64>> + use<> {
625        let (done, committed) = oneshot::channel();
626        self.send(Write::SizingIntent { change, done });
627        async move { committed.await.ok().flatten() }
628    }
629
630    /// Records what became of a pending change, synced to disk; a done one is announced as `sizing.changed` in the
631    /// same transaction. The future says whether it committed.
632    pub fn sizing_resolved(
633        &self,
634        id: i64,
635        resolution: Resolution,
636    ) -> impl Future<Output = bool> + use<> {
637        let (done, committed) = oneshot::channel();
638        self.send(Write::SizingResolved {
639            id,
640            resolution,
641            done,
642        });
643        async move { committed.await.unwrap_or(false) }
644    }
645
646    /// Records what the notifier delivered to a target: sets `set`'s keys and removes `remove`'s, in one
647    /// transaction. The future says whether it committed.
648    pub fn notify_record(
649        &self,
650        target: &str,
651        set: Vec<(String, u64)>,
652        remove: Vec<String>,
653    ) -> impl Future<Output = bool> + use<> {
654        let (done, committed) = oneshot::channel();
655        self.send(Write::Notify {
656            change: NotifyChange::Record {
657                target: target.to_owned(),
658                set,
659                remove,
660            },
661            done,
662        });
663        async move { committed.await.unwrap_or(false) }
664    }
665
666    /// Forgets what the notifier kept for every target but `targets`. The future says whether it committed.
667    pub fn notify_keep(&self, targets: Vec<String>) -> impl Future<Output = bool> + use<> {
668        let (done, committed) = oneshot::channel();
669        self.send(Write::Notify {
670            change: NotifyChange::Keep(targets),
671            done,
672        });
673        async move { committed.await.unwrap_or(false) }
674    }
675
676    /// Deletes what is older than `history` keeps, at `now` (ms), off the writer, in batches the writer's
677    /// transactions fit between. A prune while the last one runs does nothing. The handle finishes with it; dropping
678    /// it lets it run.
679    pub fn prune(&self, now: u64, history: &History) -> tokio::task::JoinHandle<()> {
680        let pruner = self.pruner.clone();
681        let closed = self.closed.clone();
682        let history = history.clone();
683        tokio::task::spawn_blocking(move || {
684            let db = match pruner.try_lock() {
685                Ok(db) => db,
686                Err(TryLockError::Poisoned(poisoned)) => poisoned.into_inner(),
687                Err(TryLockError::WouldBlock) => return,
688            };
689            let db = prune::Db {
690                connection: &db,
691                stop: &closed,
692            };
693            if !db.stop.load(Ordering::Relaxed) {
694                prune::run(&db, now, &history);
695            }
696        })
697    }
698
699    /// Stops a running prune after its batch, and any later one before it starts, so a shutdown does not wait for
700    /// one. What a stopped prune left, the next daemon's deletes. Writes go on as before.
701    pub fn close(&self) {
702        self.closed.store(true, Ordering::Relaxed);
703    }
704
705    /// Waits until every write sent before is done.
706    pub async fn flush(&self) {
707        let (done, wait) = oneshot::channel();
708        self.send(Write::Flush(done));
709        let _ = wait.await;
710    }
711
712    /// Every event from now on, durable or live. Subscribe before reading what was stored, then skip what the read
713    /// returned, and nothing is missed or sent twice.
714    pub fn subscribe(&self) -> broadcast::Receiver<Arc<EventEnvelope>> {
715        self.events.subscribe()
716    }
717
718    async fn read<T: Send + 'static>(
719        &self,
720        query: impl FnOnce(&Connection) -> rusqlite::Result<T> + Send + 'static,
721    ) -> Result<T, StoreError> {
722        let reader = self.reader.clone();
723        // A panic during a read leaves the connection usable: reads change nothing.
724        Ok(tokio::task::spawn_blocking(move || query(&lock(&reader))).await??)
725    }
726
727    /// One page of jobs, newest first.
728    pub async fn jobs(&self, query: api::JobQuery) -> Result<api::JobPage, StoreError> {
729        self.read(move |db| read::jobs(db, &query)).await
730    }
731
732    /// A job with its steps.
733    pub async fn job(&self, id: u64) -> Result<Option<api::Job>, StoreError> {
734        self.read(move |db| read::job(db, id)).await
735    }
736
737    /// A job's samples, oldest first; `None` if there is no such job.
738    pub async fn job_samples(
739        &self,
740        id: u64,
741        interval: Duration,
742    ) -> Result<Option<api::SampleSeries>, StoreError> {
743        self.read(move |db| read::job_samples(db, id, interval))
744            .await
745    }
746
747    /// Up to `limit` log lines of a job from line `from`, with their numbers.
748    pub async fn log_lines(
749        &self,
750        job: u64,
751        from: u64,
752        limit: u64,
753    ) -> Result<Vec<(u64, String)>, StoreError> {
754        self.read(move |db| read::log_lines(db, job, from, limit))
755            .await
756    }
757
758    /// Up to `limit` durable events after `after`, oldest first.
759    pub async fn events_after(
760        &self,
761        after: u64,
762        limit: u64,
763    ) -> Result<Vec<EventEnvelope>, StoreError> {
764        self.read(move |db| read::events_after(db, after, limit))
765            .await
766    }
767
768    /// The changes auto sizing wrote since `since` (ms), oldest first.
769    pub async fn sizing_changes(&self, since: u64) -> Result<Vec<api::SizingChange>, StoreError> {
770        self.read(move |db| read::sizing_changes(db, since)).await
771    }
772
773    /// When each profile last changed, and the changes still pending.
774    pub async fn auto_sizing(&self) -> Result<AutoSizing, StoreError> {
775        self.read(read::auto_sizing).await
776    }
777
778    /// The profiles and repositories as the stored events last announced them, so a restarted daemon announces
779    /// only what changed since, including what its config no longer has. Blocks on the database: for startup.
780    pub fn announced(&self) -> Result<Announced, StoreError> {
781        Ok(read::announced(&lock(&self.reader))?)
782    }
783
784    /// The repositories recorded paused, for the engine to start with. Blocks on the database: for startup.
785    pub fn paused_repositories(&self) -> Result<Vec<String>, StoreError> {
786        Ok(read::paused(&lock(&self.reader))?)
787    }
788
789    /// The slice's samples since `since` (seconds), oldest first, the ones not yet stored included.
790    pub async fn host_samples(&self, since: u64) -> Result<Vec<api::HostSample>, StoreError> {
791        // Copied before the read: the writer removes a sample only once it is stored, so each one is in the copy,
792        // the read, or both.
793        let held = lock(&self.held).clone();
794        let mut samples = self.read(move |db| read::host_samples(db, since)).await?;
795        let stored = samples.last().map_or(0, |sample| sample.at);
796        samples.extend(
797            held.into_iter()
798                .filter(|sample| sample.at >= since && sample.at > stored),
799        );
800        Ok(samples)
801    }
802
803    /// The jobs runnerd's runners started and finished from `since` until `now` (seconds).
804    pub async fn activity(&self, since: u64, now: u64) -> Result<api::Activity, StoreError> {
805        self.read(move |db| read::activity(db, since, now)).await
806    }
807
808    /// Finished jobs per repository and bucket.
809    pub async fn stats(&self, query: api::StatsQuery, now: u64) -> Result<api::Stats, StoreError> {
810        self.read(move |db| read::stats(db, &query, now)).await
811    }
812
813    /// What sizing reads: the finished jobs queued at or after `since` (ms) and the runners since.
814    pub async fn sizing(&self, since: u64) -> Result<sizing::History, StoreError> {
815        self.read(move |db| read::sizing(db, since)).await
816    }
817
818    /// The jobs completed or lost that were queued at or after `since` (ms), as insights read them.
819    pub async fn insights(&self, since: u64) -> Result<Vec<crate::insights::Run>, StoreError> {
820        self.read(move |db| read::insights(db, since)).await
821    }
822
823    /// The newest durable event's ID; 0 before the first.
824    pub async fn last_event(&self) -> Result<u64, StoreError> {
825        self.read(read::last_event).await
826    }
827
828    /// What the notifier kept for each target, by target, then key.
829    pub async fn notify_state(
830        &self,
831    ) -> Result<BTreeMap<String, BTreeMap<String, u64>>, StoreError> {
832        self.read(read::notify_state).await
833    }
834
835    /// The jobs queued at or after `since` (ms), whatever became of them, as warm runners learn from them.
836    pub async fn warm(&self, since: u64) -> Result<Vec<warm::Seen>, StoreError> {
837        self.read(move |db| read::warm(db, since)).await
838    }
839
840    /// The last measurement of every volume, with its time (seconds), by name.
841    pub async fn cache_sizes_now(&self) -> Result<Measured, StoreError> {
842        self.read(read::latest_cache_sizes).await
843    }
844
845    /// What the build cache served and took from the runners started since `since` (ms), summed.
846    pub async fn build_cache_since(&self, since: u64) -> Result<api::BuildCacheCounts, StoreError> {
847        self.read(move |db| read::build_cache_since(db, since))
848            .await
849    }
850
851    /// Every volume measurement since `since` (seconds).
852    pub async fn cache_history(&self, since: u64) -> Result<Vec<api::CacheSize>, StoreError> {
853        self.read(move |db| read::cache_history(db, since)).await
854    }
855
856    /// The jobs not yet closed, for the engine after a restart.
857    pub async fn open_jobs(&self) -> Result<Vec<OpenJob>, StoreError> {
858        self.read(read::open_jobs).await
859    }
860
861    /// The job a runner took, if the history knows it.
862    pub async fn runner_job(&self, runner: &str) -> Result<Option<u64>, StoreError> {
863        let runner = runner.to_owned();
864        self.read(move |db| read::runner_job(db, &runner)).await
865    }
866
867    /// How a runner was admitted, with what it reserves since, if the history knows it.
868    pub async fn runner_admitted(
869        &self,
870        runner: &str,
871    ) -> Result<Option<api::Admission>, StoreError> {
872        let runner = runner.to_owned();
873        self.read(move |db| read::runner_admission(db, &runner))
874            .await
875    }
876
877    /// Where a job's log capture continues.
878    pub async fn log_position(&self, job: u64) -> Result<LogPosition, StoreError> {
879        self.read(move |db| read::log_position(db, job)).await
880    }
881
882    /// How far the mirror's log was counted.
883    pub async fn mirror_log_until(&self) -> Result<MirrorPosition, StoreError> {
884        self.read(read::mirror_log_until).await
885    }
886}
887
888/// What sizing reads from the database at `path`, opened read-only and not migrated, as `runnerd doctor` reads it
889/// while no daemon runs; `None` when there is no database yet. In a directory it may write, SQLite keeps the
890/// `-wal` and `-shm` files a read of a WAL database needs next to it; in one it may not, it reads the file as it is,
891/// or refuses when the `-wal` holds changes the file does not have yet.
892pub fn sizing_at(path: &Path, since: u64) -> Result<Option<sizing::History>, StoreError> {
893    if !path.exists() {
894        return Ok(None);
895    }
896    let read = |path: &str, flags| -> Result<sizing::History, StoreError> {
897        let db = Connection::open_with_flags(path, flags)?;
898        db.busy_timeout(Duration::from_secs(5))?;
899        sizing_of(&db, since)
900    };
901    let name = path.to_string_lossy();
902    match read(&name, OpenFlags::SQLITE_OPEN_READ_ONLY) {
903        Err(StoreError::Sqlite(error))
904            if matches!(
905                error.sqlite_error_code(),
906                Some(rusqlite::ErrorCode::ReadOnly | rusqlite::ErrorCode::CannotOpen)
907            ) =>
908        {
909            // No `-shm` can be made: read the file alone, unless the `-wal` holds what it lacks.
910            let mut wal = path.as_os_str().to_owned();
911            wal.push("-wal");
912            let wal = PathBuf::from(wal);
913            if std::fs::metadata(&wal).is_ok_and(|wal| wal.len() > 0) {
914                return Err(StoreError::Unread(wal));
915            }
916            let uri = format!(
917                "file:{}?mode=ro&immutable=1",
918                name.replace('%', "%25")
919                    .replace('?', "%3f")
920                    .replace('#', "%23")
921            );
922            read(
923                &uri,
924                OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_URI,
925            )
926        }
927        history => history,
928    }
929    .map(Some)
930}
931
932/// What sizing reads from a database of any version this build knows, without migrating it.
933fn sizing_of(db: &Connection, since: u64) -> Result<sizing::History, StoreError> {
934    let found: u32 = db.pragma_query_value(None, "user_version", |row| row.get(0))?;
935    let supported = u32::try_from(MIGRATIONS.len()).unwrap_or(u32::MAX);
936    if found > supported {
937        return Err(StoreError::Newer { found, supported });
938    }
939    if found == 0 {
940        return Ok(sizing::History::default());
941    }
942    // Columns sizing reads that later versions added: before them, a temporary view gives each runner none.
943    let added = match found {
944        1 => {
945            "NULL AS memory_cap, NULL AS cpu_cap, NULL AS admission, NULL AS oom_kills, NULL AS ooms, \
946             NULL AS memory_measure"
947        }
948        2..=6 => "NULL AS admission, NULL AS oom_kills, NULL AS ooms, NULL AS memory_measure",
949        7 => "NULL AS ooms, NULL AS memory_measure",
950        _ => "",
951    };
952    if found < 13 {
953        db.execute_batch("CREATE TEMP VIEW jobs AS SELECT *, 0 AS host_waited FROM main.jobs")?;
954    }
955    if !added.is_empty() {
956        db.execute_batch(&format!(
957            "CREATE TEMP VIEW runners AS SELECT *, {added} FROM main.runners"
958        ))?;
959    }
960    Ok(read::sizing(db, since)?)
961}
962
963/// WAL, so the API reads while the writer writes; a busy timeout instead of an immediate error.
964fn configure(db: &Connection) -> rusqlite::Result<()> {
965    db.pragma_update(None, "journal_mode", "wal")?;
966    db.pragma_update(None, "synchronous", "normal")?;
967    db.pragma_update(None, "foreign_keys", "on")?;
968    db.busy_timeout(Duration::from_secs(5))
969}
970
971/// Runs the migrations the database lacks, each in its own transaction with its new version.
972fn migrate(db: &mut Connection) -> Result<(), StoreError> {
973    let found: u32 = db.pragma_query_value(None, "user_version", |row| row.get(0))?;
974    let supported = u32::try_from(MIGRATIONS.len()).unwrap_or(u32::MAX);
975    if found > supported {
976        return Err(StoreError::Newer { found, supported });
977    }
978    for (version, migration) in (1..).zip(MIGRATIONS).skip(found as usize) {
979        let transaction = db.transaction()?;
980        transaction.execute_batch(migration)?;
981        transaction.pragma_update(None, "user_version", version)?;
982        transaction.commit()?;
983    }
984    Ok(())
985}
986
987/// Logs a failed write; the writer goes on with the next one.
988fn logged<T>(what: &str, result: rusqlite::Result<T>) -> Option<T> {
989    result
990        .map_err(|error| warn!("job history: cannot {what}: {error}"))
991        .ok()
992}
993
994#[cfg(test)]
995mod tests;