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