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