1use 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
30const 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
47pub(crate) const LOG_BACKLOG: u64 = 8 << 20;
50
51const HOST_SAMPLE_BATCH: usize = 30;
54
55const EVENT_BUFFER: usize = 4096;
57
58#[derive(Debug, thiserror::Error)]
60pub enum StoreError {
61 #[error("{0}")]
63 Sqlite(#[from] rusqlite::Error),
64 #[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 found: u32,
72 supported: u32,
74 },
75 #[error("{0}")]
77 Io(#[from] std::io::Error),
78 #[error("a database read stopped: {0}")]
80 Join(#[from] tokio::task::JoinError),
81 #[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#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct RunnerRecord {
94 pub name: String,
96 pub repository: String,
98 pub profile: String,
100 pub slot: usize,
102 pub container: String,
104 pub started_at: u64,
106 pub memory_cap: Option<u64>,
108 pub cpu_cap: Option<u64>,
110 pub admission: Option<api::Admission>,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct SampleRecord {
117 pub runner: String,
119 pub at: u64,
121 pub cpu: u64,
123 pub cpu_usage: u64,
125 pub memory: u64,
127 pub network_received: u64,
129 pub network_sent: u64,
131 pub events: Events,
133}
134
135#[derive(Debug, Clone, Default, PartialEq, Eq)]
137pub struct Measured {
138 pub at: Option<u64>,
140 pub bytes: BTreeMap<String, u64>,
142}
143
144#[derive(Debug, Clone, Default, PartialEq, Eq)]
147pub struct Announced {
148 pub profiles: Vec<api::ProfileStatus>,
150 pub repositories: Vec<api::RepositoryStatus>,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct OpenJob {
157 pub id: u64,
159 pub repository: String,
161 pub state: api::JobState,
163 pub runner: Option<String>,
165 pub runner_removed_at: Option<u64>,
167 pub run_id: u64,
169 pub run_attempt: u32,
171 pub workflow: Option<String>,
173 pub queued_at: u64,
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub struct LogPosition {
180 pub page: u64,
182 pub offset: u64,
184}
185
186#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
190pub struct MirrorPosition {
191 pub at: u64,
193 pub seen: u64,
195}
196
197#[derive(Debug)]
199enum Write {
200 Queued {
201 repository: String,
202 job: QueuedJob,
203 },
204 RunnerStarted(RunnerRecord),
205 RunnerAdmission {
206 runner: String,
207 admission: api::Admission,
208 },
209 RunnerOomKilled(String),
210 HostWaited {
211 jobs: Vec<u64>,
212 ms: u64,
213 },
214 BuildCache {
215 runner: String,
216 counts: api::BuildCacheCounts,
217 },
218 RunnerRemoved {
219 runner: String,
220 at: u64,
221 },
222 RunnersGone {
223 running: Vec<String>,
224 at: u64,
225 },
226 Claimed {
227 job: u64,
228 run_id: u64,
229 runner: String,
230 at: u64,
231 },
232 Details {
233 details: JobDetails,
234 },
235 Elsewhere {
236 details: JobDetails,
237 },
238 Lost {
239 job: u64,
240 at: u64,
241 },
242 Log {
243 job: u64,
244 lines: Vec<String>,
245 position: Option<LogPosition>,
247 max_bytes: u64,
248 _backlog: OwnedSemaphorePermit,
250 },
251 Samples(Vec<SampleRecord>),
252 HostSample(api::HostSample),
253 MirrorBytes {
254 bytes: Vec<(String, u64)>,
255 until: MirrorPosition,
256 done: oneshot::Sender<bool>,
258 },
259 CacheSizes {
260 at: u64,
261 sizes: BTreeMap<String, u64>,
262 },
263 Event(Box<Event>),
265 RecordMade(api::Record),
266 RecordSkipped {
267 job: u64,
268 why: String,
269 },
270 RecordRemoved {
271 job: u64,
272 at: u64,
273 by: api::RecordRemoval,
274 },
275 Paused {
276 repositories: Vec<String>,
277 done: oneshot::Sender<bool>,
279 },
280 SizingIntent {
281 change: api::SizingChange,
282 done: oneshot::Sender<Option<i64>>,
284 },
285 SizingResolved {
286 id: i64,
287 resolution: Resolution,
288 done: oneshot::Sender<bool>,
290 },
291 Notify {
292 change: NotifyChange,
293 done: oneshot::Sender<bool>,
295 },
296 Flush(oneshot::Sender<()>),
297}
298
299#[derive(Debug)]
301enum NotifyChange {
302 Record {
304 target: String,
305 set: Vec<(String, u64)>,
306 remove: Vec<String>,
307 },
308 Keep(Vec<String>),
310}
311
312#[derive(Debug, Clone, Copy, PartialEq, Eq)]
314pub enum Resolution {
315 Done,
317 Failed,
319 Uncertain,
322}
323
324#[derive(Debug, Clone, Default, PartialEq, Eq)]
326pub struct AutoSizing {
327 pub last: BTreeMap<String, u64>,
329 pub pending: Vec<(i64, api::SizingChange)>,
331}
332
333#[derive(Debug, Clone)]
335pub struct Store {
336 writes: mpsc::UnboundedSender<Write>,
337 reader: Arc<Mutex<Connection>>,
338 pruner: Arc<Mutex<Connection>>,
340 closed: Arc<AtomicBool>,
342 held: Arc<Mutex<Vec<api::HostSample>>>,
345 latest_host: Arc<Mutex<Option<api::HostSample>>>,
347 latest_host_memory: Arc<Mutex<Option<api::HostMemory>>>,
349 latest_samples: Arc<Mutex<BTreeMap<String, SampleRecord>>>,
351 events: broadcast::Sender<Arc<EventEnvelope>>,
352 backlog: Arc<Semaphore>,
353}
354
355impl Store {
356 pub fn open(path: &Path) -> Result<Store, StoreError> {
359 if let Some(dir) = path.parent().filter(|dir| !dir.as_os_str().is_empty()) {
360 std::fs::DirBuilder::new()
361 .recursive(true)
362 .mode(0o700)
363 .create(dir)?;
364 }
365 std::fs::OpenOptions::new()
367 .create(true)
368 .append(true)
369 .mode(0o600)
370 .open(path)?;
371 let mut db = Connection::open(path)?;
372 configure(&db)?;
373 migrate(&mut db)?;
374 let reader = Connection::open_with_flags(
375 path,
376 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
377 )?;
378 configure(&reader)?;
379 let pruner = Connection::open(path)?;
380 configure(&pruner)?;
381 let (events, _) = broadcast::channel(EVENT_BUFFER);
382 let (writes, receiver) = mpsc::unbounded_channel();
383 let thread_events = events.clone();
384 let held = Arc::new(Mutex::new(Vec::new()));
385 let thread_held = held.clone();
386 std::thread::Builder::new()
387 .name("runnerd-store".to_owned())
388 .spawn(move || write::run(db, receiver, &thread_events, &thread_held))?;
389 Ok(Store {
390 writes,
391 reader: Arc::new(Mutex::new(reader)),
392 pruner: Arc::new(Mutex::new(pruner)),
393 closed: Arc::new(AtomicBool::new(false)),
394 held,
395 latest_host: Arc::new(Mutex::new(None)),
396 latest_host_memory: Arc::new(Mutex::new(None)),
397 latest_samples: Arc::new(Mutex::new(BTreeMap::new())),
398 events,
399 backlog: Arc::new(Semaphore::new(
400 usize::try_from(LOG_BACKLOG).unwrap_or(usize::MAX),
401 )),
402 })
403 }
404
405 fn send(&self, write: Write) {
406 let _ = self.writes.send(write);
408 }
409
410 pub fn queued(&self, repository: &str, job: &QueuedJob) {
412 self.send(Write::Queued {
413 repository: repository.to_owned(),
414 job: job.clone(),
415 });
416 }
417
418 pub fn runner_started(&self, runner: RunnerRecord) {
420 self.send(Write::RunnerStarted(runner));
421 }
422
423 pub fn runner_removed(&self, runner: &str, at: u64) {
425 lock(&self.latest_samples).remove(runner);
426 self.send(Write::RunnerRemoved {
427 runner: runner.to_owned(),
428 at,
429 });
430 }
431
432 pub fn runners_gone(&self, running: Vec<String>, at: u64) {
435 lock(&self.latest_samples).retain(|runner, _| running.contains(runner));
436 self.send(Write::RunnersGone { running, at });
437 }
438
439 pub fn claimed(&self, job: u64, run_id: u64, runner: &str, at: u64) {
441 self.send(Write::Claimed {
442 job,
443 run_id,
444 runner: runner.to_owned(),
445 at,
446 });
447 }
448
449 pub fn details(&self, details: JobDetails) {
451 self.send(Write::Details { details });
452 }
453
454 pub fn elsewhere(&self, details: JobDetails) {
456 self.send(Write::Elsewhere { details });
457 }
458
459 pub fn lost(&self, job: u64, at: u64) {
461 self.send(Write::Lost { job, at });
462 }
463
464 pub async fn log(&self, job: u64, lines: Vec<String>, position: LogPosition, max_bytes: u64) {
467 let size = |line: &String| line.len() as u64 + 1;
468 let mut lines = lines.into_iter().map(|mut line| {
469 if size(&line) > LOG_BACKLOG {
471 let mut end = usize::try_from(LOG_BACKLOG / 2).unwrap_or(usize::MAX);
472 while !line.is_char_boundary(end) {
473 end -= 1;
474 }
475 line.truncate(end);
476 }
477 line
478 });
479 let mut next = lines.next();
480 loop {
481 let (mut part, mut bytes) = (Vec::new(), 0);
482 while let Some(line) = next.take_if(|line| bytes + size(line) <= LOG_BACKLOG) {
483 bytes += size(&line);
484 part.push(line);
485 next = lines.next();
486 }
487 let last = next.is_none();
488 let share = u32::try_from(bytes.max(1)).unwrap_or(u32::MAX);
489 let Ok(backlog) = self.backlog.clone().acquire_many_owned(share).await else {
491 return;
492 };
493 self.send(Write::Log {
494 job,
495 lines: part,
496 position: last.then_some(position),
497 max_bytes,
498 _backlog: backlog,
499 });
500 if last {
501 return;
502 }
503 }
504 }
505
506 pub fn samples(&self, samples: Vec<SampleRecord>) {
508 let mut latest = lock(&self.latest_samples);
509 for sample in &samples {
510 latest.insert(sample.runner.clone(), sample.clone());
511 }
512 drop(latest);
513 self.send(Write::Samples(samples));
514 }
515
516 pub fn host_sample(&self, sample: api::HostSample) {
519 *lock(&self.latest_host) = Some(sample.clone());
520 lock(&self.held).push(sample.clone());
521 self.send(Write::HostSample(sample));
522 }
523
524 pub fn latest_sample(&self, runner: &str) -> Option<SampleRecord> {
526 lock(&self.latest_samples).get(runner).cloned()
527 }
528
529 pub fn runner_admission(&self, runner: &str, admission: api::Admission) {
531 self.send(Write::RunnerAdmission {
532 runner: runner.to_owned(),
533 admission,
534 });
535 }
536
537 pub fn runner_oom_killed(&self, runner: &str) {
540 self.send(Write::RunnerOomKilled(runner.to_owned()));
541 }
542
543 pub fn build_cache(&self, runner: &str, counts: api::BuildCacheCounts) {
545 self.send(Write::BuildCache {
546 runner: runner.to_owned(),
547 counts,
548 });
549 }
550
551 pub fn host_memory(&self, memory: api::HostMemory) {
553 *lock(&self.latest_host_memory) = Some(memory);
554 }
555
556 pub fn latest_host_memory(&self) -> Option<api::HostMemory> {
558 *lock(&self.latest_host_memory)
559 }
560
561 pub fn host_waited(&self, jobs: Vec<u64>, ms: u64) {
564 self.send(Write::HostWaited { jobs, ms });
565 }
566
567 pub fn latest_host_sample(&self) -> Option<api::HostSample> {
569 lock(&self.latest_host).clone()
570 }
571
572 pub async fn mirror_bytes(&self, bytes: Vec<(String, u64)>, until: MirrorPosition) -> bool {
575 let (done, committed) = oneshot::channel();
576 self.send(Write::MirrorBytes { bytes, until, done });
577 committed.await.unwrap_or(false)
578 }
579
580 pub fn cache_sizes(&self, at: u64, sizes: BTreeMap<String, u64>) {
582 self.send(Write::CacheSizes { at, sizes });
583 }
584
585 pub fn event(&self, event: Event) {
587 self.send(Write::Event(Box::new(event)));
588 }
589
590 pub fn record_made(&self, record: api::Record) {
592 self.send(Write::RecordMade(record));
593 }
594
595 pub fn record_skipped(&self, job: u64, why: String) {
597 self.send(Write::RecordSkipped { job, why });
598 }
599
600 pub fn record_removed(&self, job: u64, at: u64, by: api::RecordRemoval) {
602 self.send(Write::RecordRemoved { job, at, by });
603 }
604
605 pub fn paused(&self, repositories: Vec<String>) -> impl Future<Output = bool> + use<> {
608 let (done, committed) = oneshot::channel();
609 self.send(Write::Paused { repositories, done });
610 async move { committed.await.unwrap_or(false) }
611 }
612
613 pub fn sizing_intent(
616 &self,
617 change: api::SizingChange,
618 ) -> impl Future<Output = Option<i64>> + use<> {
619 let (done, committed) = oneshot::channel();
620 self.send(Write::SizingIntent { change, done });
621 async move { committed.await.ok().flatten() }
622 }
623
624 pub fn sizing_resolved(
627 &self,
628 id: i64,
629 resolution: Resolution,
630 ) -> impl Future<Output = bool> + use<> {
631 let (done, committed) = oneshot::channel();
632 self.send(Write::SizingResolved {
633 id,
634 resolution,
635 done,
636 });
637 async move { committed.await.unwrap_or(false) }
638 }
639
640 pub fn notify_record(
643 &self,
644 target: &str,
645 set: Vec<(String, u64)>,
646 remove: Vec<String>,
647 ) -> impl Future<Output = bool> + use<> {
648 let (done, committed) = oneshot::channel();
649 self.send(Write::Notify {
650 change: NotifyChange::Record {
651 target: target.to_owned(),
652 set,
653 remove,
654 },
655 done,
656 });
657 async move { committed.await.unwrap_or(false) }
658 }
659
660 pub fn notify_keep(&self, targets: Vec<String>) -> impl Future<Output = bool> + use<> {
662 let (done, committed) = oneshot::channel();
663 self.send(Write::Notify {
664 change: NotifyChange::Keep(targets),
665 done,
666 });
667 async move { committed.await.unwrap_or(false) }
668 }
669
670 pub fn prune(&self, now: u64, history: &History) -> tokio::task::JoinHandle<()> {
674 let pruner = self.pruner.clone();
675 let closed = self.closed.clone();
676 let history = history.clone();
677 tokio::task::spawn_blocking(move || {
678 let db = match pruner.try_lock() {
679 Ok(db) => db,
680 Err(TryLockError::Poisoned(poisoned)) => poisoned.into_inner(),
681 Err(TryLockError::WouldBlock) => return,
682 };
683 let db = prune::Db {
684 connection: &db,
685 stop: &closed,
686 };
687 if !db.stop.load(Ordering::Relaxed) {
688 prune::run(&db, now, &history);
689 }
690 })
691 }
692
693 pub fn close(&self) {
696 self.closed.store(true, Ordering::Relaxed);
697 }
698
699 pub async fn flush(&self) {
701 let (done, wait) = oneshot::channel();
702 self.send(Write::Flush(done));
703 let _ = wait.await;
704 }
705
706 pub fn subscribe(&self) -> broadcast::Receiver<Arc<EventEnvelope>> {
709 self.events.subscribe()
710 }
711
712 async fn read<T: Send + 'static>(
713 &self,
714 query: impl FnOnce(&Connection) -> rusqlite::Result<T> + Send + 'static,
715 ) -> Result<T, StoreError> {
716 let reader = self.reader.clone();
717 Ok(tokio::task::spawn_blocking(move || query(&lock(&reader))).await??)
719 }
720
721 pub async fn jobs(&self, query: api::JobQuery) -> Result<api::JobPage, StoreError> {
723 self.read(move |db| read::jobs(db, &query)).await
724 }
725
726 pub async fn job(&self, id: u64) -> Result<Option<api::Job>, StoreError> {
728 self.read(move |db| read::job(db, id)).await
729 }
730
731 pub async fn job_samples(
733 &self,
734 id: u64,
735 interval: Duration,
736 ) -> Result<Option<api::SampleSeries>, StoreError> {
737 self.read(move |db| read::job_samples(db, id, interval))
738 .await
739 }
740
741 pub async fn log_lines(
743 &self,
744 job: u64,
745 from: u64,
746 limit: u64,
747 ) -> Result<Vec<(u64, String)>, StoreError> {
748 self.read(move |db| read::log_lines(db, job, from, limit))
749 .await
750 }
751
752 pub async fn events_after(
754 &self,
755 after: u64,
756 limit: u64,
757 ) -> Result<Vec<EventEnvelope>, StoreError> {
758 self.read(move |db| read::events_after(db, after, limit))
759 .await
760 }
761
762 pub async fn sizing_changes(&self, since: u64) -> Result<Vec<api::SizingChange>, StoreError> {
764 self.read(move |db| read::sizing_changes(db, since)).await
765 }
766
767 pub async fn auto_sizing(&self) -> Result<AutoSizing, StoreError> {
769 self.read(read::auto_sizing).await
770 }
771
772 pub fn announced(&self) -> Result<Announced, StoreError> {
775 Ok(read::announced(&lock(&self.reader))?)
776 }
777
778 pub fn paused_repositories(&self) -> Result<Vec<String>, StoreError> {
780 Ok(read::paused(&lock(&self.reader))?)
781 }
782
783 pub async fn host_samples(&self, since: u64) -> Result<Vec<api::HostSample>, StoreError> {
785 let held = lock(&self.held).clone();
788 let mut samples = self.read(move |db| read::host_samples(db, since)).await?;
789 let stored = samples.last().map_or(0, |sample| sample.at);
790 samples.extend(
791 held.into_iter()
792 .filter(|sample| sample.at >= since && sample.at > stored),
793 );
794 Ok(samples)
795 }
796
797 pub async fn activity(&self, since: u64, now: u64) -> Result<api::Activity, StoreError> {
799 self.read(move |db| read::activity(db, since, now)).await
800 }
801
802 pub async fn stats(&self, query: api::StatsQuery, now: u64) -> Result<api::Stats, StoreError> {
804 self.read(move |db| read::stats(db, &query, now)).await
805 }
806
807 pub async fn sizing(&self, since: u64) -> Result<sizing::History, StoreError> {
809 self.read(move |db| read::sizing(db, since)).await
810 }
811
812 pub async fn insights(&self, since: u64) -> Result<Vec<crate::insights::Run>, StoreError> {
814 self.read(move |db| read::insights(db, since)).await
815 }
816
817 pub async fn last_event(&self) -> Result<u64, StoreError> {
819 self.read(read::last_event).await
820 }
821
822 pub async fn notify_state(
824 &self,
825 ) -> Result<BTreeMap<String, BTreeMap<String, u64>>, StoreError> {
826 self.read(read::notify_state).await
827 }
828
829 pub async fn warm(&self, since: u64) -> Result<Vec<warm::Seen>, StoreError> {
831 self.read(move |db| read::warm(db, since)).await
832 }
833
834 pub async fn cache_sizes_now(&self) -> Result<Measured, StoreError> {
836 self.read(read::latest_cache_sizes).await
837 }
838
839 pub async fn build_cache_since(&self, since: u64) -> Result<api::BuildCacheCounts, StoreError> {
841 self.read(move |db| read::build_cache_since(db, since))
842 .await
843 }
844
845 pub async fn cache_history(&self, since: u64) -> Result<Vec<api::CacheSize>, StoreError> {
847 self.read(move |db| read::cache_history(db, since)).await
848 }
849
850 pub async fn open_jobs(&self) -> Result<Vec<OpenJob>, StoreError> {
852 self.read(read::open_jobs).await
853 }
854
855 pub async fn runner_job(&self, runner: &str) -> Result<Option<u64>, StoreError> {
857 let runner = runner.to_owned();
858 self.read(move |db| read::runner_job(db, &runner)).await
859 }
860
861 pub async fn runner_admitted(
863 &self,
864 runner: &str,
865 ) -> Result<Option<api::Admission>, StoreError> {
866 let runner = runner.to_owned();
867 self.read(move |db| read::runner_admission(db, &runner))
868 .await
869 }
870
871 pub async fn log_position(&self, job: u64) -> Result<LogPosition, StoreError> {
873 self.read(move |db| read::log_position(db, job)).await
874 }
875
876 pub async fn mirror_log_until(&self) -> Result<MirrorPosition, StoreError> {
878 self.read(read::mirror_log_until).await
879 }
880}
881
882pub fn sizing_at(path: &Path, since: u64) -> Result<Option<sizing::History>, StoreError> {
887 if !path.exists() {
888 return Ok(None);
889 }
890 let read = |path: &str, flags| -> Result<sizing::History, StoreError> {
891 let db = Connection::open_with_flags(path, flags)?;
892 db.busy_timeout(Duration::from_secs(5))?;
893 sizing_of(&db, since)
894 };
895 let name = path.to_string_lossy();
896 match read(&name, OpenFlags::SQLITE_OPEN_READ_ONLY) {
897 Err(StoreError::Sqlite(error))
898 if matches!(
899 error.sqlite_error_code(),
900 Some(rusqlite::ErrorCode::ReadOnly | rusqlite::ErrorCode::CannotOpen)
901 ) =>
902 {
903 let mut wal = path.as_os_str().to_owned();
905 wal.push("-wal");
906 let wal = PathBuf::from(wal);
907 if std::fs::metadata(&wal).is_ok_and(|wal| wal.len() > 0) {
908 return Err(StoreError::Unread(wal));
909 }
910 let uri = format!(
911 "file:{}?mode=ro&immutable=1",
912 name.replace('%', "%25")
913 .replace('?', "%3f")
914 .replace('#', "%23")
915 );
916 read(
917 &uri,
918 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_URI,
919 )
920 }
921 history => history,
922 }
923 .map(Some)
924}
925
926fn sizing_of(db: &Connection, since: u64) -> Result<sizing::History, StoreError> {
928 let found: u32 = db.pragma_query_value(None, "user_version", |row| row.get(0))?;
929 let supported = u32::try_from(MIGRATIONS.len()).unwrap_or(u32::MAX);
930 if found > supported {
931 return Err(StoreError::Newer { found, supported });
932 }
933 if found == 0 {
934 return Ok(sizing::History::default());
935 }
936 let added = match found {
938 1 => {
939 "NULL AS memory_cap, NULL AS cpu_cap, NULL AS admission, NULL AS oom_kills, NULL AS ooms, \
940 NULL AS memory_measure"
941 }
942 2..=6 => "NULL AS admission, NULL AS oom_kills, NULL AS ooms, NULL AS memory_measure",
943 7 => "NULL AS ooms, NULL AS memory_measure",
944 _ => "",
945 };
946 if found < 13 {
947 db.execute_batch("CREATE TEMP VIEW jobs AS SELECT *, 0 AS host_waited FROM main.jobs")?;
948 }
949 if !added.is_empty() {
950 db.execute_batch(&format!(
951 "CREATE TEMP VIEW runners AS SELECT *, {added} FROM main.runners"
952 ))?;
953 }
954 Ok(read::sizing(db, since)?)
955}
956
957fn configure(db: &Connection) -> rusqlite::Result<()> {
959 db.pragma_update(None, "journal_mode", "wal")?;
960 db.pragma_update(None, "synchronous", "normal")?;
961 db.pragma_update(None, "foreign_keys", "on")?;
962 db.busy_timeout(Duration::from_secs(5))
963}
964
965fn migrate(db: &mut Connection) -> Result<(), StoreError> {
967 let found: u32 = db.pragma_query_value(None, "user_version", |row| row.get(0))?;
968 let supported = u32::try_from(MIGRATIONS.len()).unwrap_or(u32::MAX);
969 if found > supported {
970 return Err(StoreError::Newer { found, supported });
971 }
972 for (version, migration) in (1..).zip(MIGRATIONS).skip(found as usize) {
973 let transaction = db.transaction()?;
974 transaction.execute_batch(migration)?;
975 transaction.pragma_update(None, "user_version", version)?;
976 transaction.commit()?;
977 }
978 Ok(())
979}
980
981fn logged<T>(what: &str, result: rusqlite::Result<T>) -> Option<T> {
983 result
984 .map_err(|error| warn!("job history: cannot {what}: {error}"))
985 .ok()
986}
987
988#[cfg(test)]
989mod tests;