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 pub labels: Vec<String>,
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181pub struct LogPosition {
182 pub page: u64,
184 pub offset: u64,
186}
187
188#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
192pub struct MirrorPosition {
193 pub at: u64,
195 pub seen: u64,
197}
198
199#[derive(Debug)]
201enum Write {
202 Queued {
203 repository: String,
204 job: QueuedJob,
205 },
206 RunnerStarted(RunnerRecord),
207 RunnerAdmission {
208 runner: String,
209 admission: api::Admission,
210 },
211 RunnerOomKilled(String),
212 HostWaited {
213 jobs: Vec<u64>,
214 ms: u64,
215 },
216 BuildCache {
217 runner: String,
218 counts: api::BuildCacheCounts,
219 },
220 RunnerRemoved {
221 runner: String,
222 at: u64,
223 },
224 RunnersGone {
225 running: Vec<String>,
226 at: u64,
227 },
228 Claimed {
229 job: u64,
230 run_id: u64,
231 runner: String,
232 at: u64,
233 },
234 Details {
235 details: JobDetails,
236 },
237 Elsewhere {
238 details: JobDetails,
239 },
240 Lost {
241 job: u64,
242 at: u64,
243 },
244 Log {
245 job: u64,
246 lines: Vec<String>,
247 position: Option<LogPosition>,
249 max_bytes: u64,
250 _backlog: OwnedSemaphorePermit,
252 },
253 Samples(Vec<SampleRecord>),
254 HostSample(api::HostSample),
255 MirrorBytes {
256 bytes: Vec<(String, u64)>,
257 until: MirrorPosition,
258 done: oneshot::Sender<bool>,
260 },
261 CacheSizes {
262 at: u64,
263 sizes: BTreeMap<String, u64>,
264 },
265 Event(Box<Event>),
267 RecordMade(api::Record),
268 RecordSkipped {
269 job: u64,
270 why: String,
271 },
272 RecordRemoved {
273 job: u64,
274 at: u64,
275 by: api::RecordRemoval,
276 },
277 Paused {
278 repositories: Vec<String>,
279 done: oneshot::Sender<bool>,
281 },
282 SizingIntent {
283 change: api::SizingChange,
284 done: oneshot::Sender<Option<i64>>,
286 },
287 SizingResolved {
288 id: i64,
289 resolution: Resolution,
290 done: oneshot::Sender<bool>,
292 },
293 Notify {
294 change: NotifyChange,
295 done: oneshot::Sender<bool>,
297 },
298 Flush(oneshot::Sender<()>),
299}
300
301#[derive(Debug)]
303enum NotifyChange {
304 Record {
306 target: String,
307 set: Vec<(String, u64)>,
308 remove: Vec<String>,
309 },
310 Keep(Vec<String>),
312}
313
314#[derive(Debug, Clone, Copy, PartialEq, Eq)]
316pub enum Resolution {
317 Done,
319 Failed,
321 Uncertain,
324}
325
326#[derive(Debug, Clone, Default, PartialEq, Eq)]
328pub struct AutoSizing {
329 pub last: BTreeMap<String, u64>,
331 pub pending: Vec<(i64, api::SizingChange)>,
333 pub cpus: Vec<sizing::CpusSet>,
336}
337
338#[derive(Debug, Clone)]
340pub struct Store {
341 writes: mpsc::UnboundedSender<Write>,
342 reader: Arc<Mutex<Connection>>,
343 pruner: Arc<Mutex<Connection>>,
345 closed: Arc<AtomicBool>,
347 held: Arc<Mutex<Vec<api::HostSample>>>,
350 latest_host: Arc<Mutex<Option<api::HostSample>>>,
352 latest_host_memory: Arc<Mutex<Option<api::HostMemory>>>,
354 latest_samples: Arc<Mutex<BTreeMap<String, SampleRecord>>>,
356 events: broadcast::Sender<Arc<EventEnvelope>>,
357 backlog: Arc<Semaphore>,
358}
359
360impl Store {
361 pub fn open(path: &Path) -> Result<Store, StoreError> {
364 if let Some(dir) = path.parent().filter(|dir| !dir.as_os_str().is_empty()) {
365 std::fs::DirBuilder::new()
366 .recursive(true)
367 .mode(0o700)
368 .create(dir)?;
369 }
370 std::fs::OpenOptions::new()
372 .create(true)
373 .append(true)
374 .mode(0o600)
375 .open(path)?;
376 let mut db = Connection::open(path)?;
377 configure(&db)?;
378 migrate(&mut db)?;
379 let reader = Connection::open_with_flags(
380 path,
381 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
382 )?;
383 configure(&reader)?;
384 let pruner = Connection::open(path)?;
385 configure(&pruner)?;
386 let (events, _) = broadcast::channel(EVENT_BUFFER);
387 let (writes, receiver) = mpsc::unbounded_channel();
388 let thread_events = events.clone();
389 let held = Arc::new(Mutex::new(Vec::new()));
390 let thread_held = held.clone();
391 std::thread::Builder::new()
392 .name("runnerd-store".to_owned())
393 .spawn(move || write::run(db, receiver, &thread_events, &thread_held))?;
394 Ok(Store {
395 writes,
396 reader: Arc::new(Mutex::new(reader)),
397 pruner: Arc::new(Mutex::new(pruner)),
398 closed: Arc::new(AtomicBool::new(false)),
399 held,
400 latest_host: Arc::new(Mutex::new(None)),
401 latest_host_memory: Arc::new(Mutex::new(None)),
402 latest_samples: Arc::new(Mutex::new(BTreeMap::new())),
403 events,
404 backlog: Arc::new(Semaphore::new(
405 usize::try_from(LOG_BACKLOG).unwrap_or(usize::MAX),
406 )),
407 })
408 }
409
410 fn send(&self, write: Write) {
411 let _ = self.writes.send(write);
413 }
414
415 pub fn queued(&self, repository: &str, job: &QueuedJob) {
417 self.send(Write::Queued {
418 repository: repository.to_owned(),
419 job: job.clone(),
420 });
421 }
422
423 pub fn runner_started(&self, runner: RunnerRecord) {
425 self.send(Write::RunnerStarted(runner));
426 }
427
428 pub fn runner_removed(&self, runner: &str, at: u64) {
430 lock(&self.latest_samples).remove(runner);
431 self.send(Write::RunnerRemoved {
432 runner: runner.to_owned(),
433 at,
434 });
435 }
436
437 pub fn runners_gone(&self, running: Vec<String>, at: u64) {
440 lock(&self.latest_samples).retain(|runner, _| running.contains(runner));
441 self.send(Write::RunnersGone { running, at });
442 }
443
444 pub fn claimed(&self, job: u64, run_id: u64, runner: &str, at: u64) {
446 self.send(Write::Claimed {
447 job,
448 run_id,
449 runner: runner.to_owned(),
450 at,
451 });
452 }
453
454 pub fn details(&self, details: JobDetails) {
456 self.send(Write::Details { details });
457 }
458
459 pub fn elsewhere(&self, details: JobDetails) {
462 self.send(Write::Elsewhere { details });
463 }
464
465 pub fn lost(&self, job: u64, at: u64) {
467 self.send(Write::Lost { job, at });
468 }
469
470 pub async fn log(&self, job: u64, lines: Vec<String>, position: LogPosition, max_bytes: u64) {
473 let size = |line: &String| line.len() as u64 + 1;
474 let mut lines = lines.into_iter().map(|mut line| {
475 if size(&line) > LOG_BACKLOG {
477 let mut end = usize::try_from(LOG_BACKLOG / 2).unwrap_or(usize::MAX);
478 while !line.is_char_boundary(end) {
479 end -= 1;
480 }
481 line.truncate(end);
482 }
483 line
484 });
485 let mut next = lines.next();
486 loop {
487 let (mut part, mut bytes) = (Vec::new(), 0);
488 while let Some(line) = next.take_if(|line| bytes + size(line) <= LOG_BACKLOG) {
489 bytes += size(&line);
490 part.push(line);
491 next = lines.next();
492 }
493 let last = next.is_none();
494 let share = u32::try_from(bytes.max(1)).unwrap_or(u32::MAX);
495 let Ok(backlog) = self.backlog.clone().acquire_many_owned(share).await else {
497 return;
498 };
499 self.send(Write::Log {
500 job,
501 lines: part,
502 position: last.then_some(position),
503 max_bytes,
504 _backlog: backlog,
505 });
506 if last {
507 return;
508 }
509 }
510 }
511
512 pub fn samples(&self, samples: Vec<SampleRecord>) {
514 let mut latest = lock(&self.latest_samples);
515 for sample in &samples {
516 latest.insert(sample.runner.clone(), sample.clone());
517 }
518 drop(latest);
519 self.send(Write::Samples(samples));
520 }
521
522 pub fn host_sample(&self, sample: api::HostSample) {
525 *lock(&self.latest_host) = Some(sample.clone());
526 lock(&self.held).push(sample.clone());
527 self.send(Write::HostSample(sample));
528 }
529
530 pub fn latest_sample(&self, runner: &str) -> Option<SampleRecord> {
532 lock(&self.latest_samples).get(runner).cloned()
533 }
534
535 pub fn runner_admission(&self, runner: &str, admission: api::Admission) {
537 self.send(Write::RunnerAdmission {
538 runner: runner.to_owned(),
539 admission,
540 });
541 }
542
543 pub fn runner_oom_killed(&self, runner: &str) {
546 self.send(Write::RunnerOomKilled(runner.to_owned()));
547 }
548
549 pub fn build_cache(&self, runner: &str, counts: api::BuildCacheCounts) {
551 self.send(Write::BuildCache {
552 runner: runner.to_owned(),
553 counts,
554 });
555 }
556
557 pub fn host_memory(&self, memory: api::HostMemory) {
559 *lock(&self.latest_host_memory) = Some(memory);
560 }
561
562 pub fn latest_host_memory(&self) -> Option<api::HostMemory> {
564 lock(&self.latest_host_memory).clone()
565 }
566
567 pub fn host_waited(&self, jobs: Vec<u64>, ms: u64) {
570 self.send(Write::HostWaited { jobs, ms });
571 }
572
573 pub fn latest_host_sample(&self) -> Option<api::HostSample> {
575 lock(&self.latest_host).clone()
576 }
577
578 pub async fn mirror_bytes(&self, bytes: Vec<(String, u64)>, until: MirrorPosition) -> bool {
581 let (done, committed) = oneshot::channel();
582 self.send(Write::MirrorBytes { bytes, until, done });
583 committed.await.unwrap_or(false)
584 }
585
586 pub fn cache_sizes(&self, at: u64, sizes: BTreeMap<String, u64>) {
588 self.send(Write::CacheSizes { at, sizes });
589 }
590
591 pub fn event(&self, event: Event) {
593 self.send(Write::Event(Box::new(event)));
594 }
595
596 pub fn record_made(&self, record: api::Record) {
598 self.send(Write::RecordMade(record));
599 }
600
601 pub fn record_skipped(&self, job: u64, why: String) {
603 self.send(Write::RecordSkipped { job, why });
604 }
605
606 pub fn record_removed(&self, job: u64, at: u64, by: api::RecordRemoval) {
608 self.send(Write::RecordRemoved { job, at, by });
609 }
610
611 pub fn paused(&self, repositories: Vec<String>) -> impl Future<Output = bool> + use<> {
614 let (done, committed) = oneshot::channel();
615 self.send(Write::Paused { repositories, done });
616 async move { committed.await.unwrap_or(false) }
617 }
618
619 pub fn sizing_intent(
622 &self,
623 change: api::SizingChange,
624 ) -> impl Future<Output = Option<i64>> + use<> {
625 let (done, committed) = oneshot::channel();
626 self.send(Write::SizingIntent { change, done });
627 async move { committed.await.ok().flatten() }
628 }
629
630 pub fn sizing_resolved(
633 &self,
634 id: i64,
635 resolution: Resolution,
636 ) -> impl Future<Output = bool> + use<> {
637 let (done, committed) = oneshot::channel();
638 self.send(Write::SizingResolved {
639 id,
640 resolution,
641 done,
642 });
643 async move { committed.await.unwrap_or(false) }
644 }
645
646 pub fn notify_record(
649 &self,
650 target: &str,
651 set: Vec<(String, u64)>,
652 remove: Vec<String>,
653 ) -> impl Future<Output = bool> + use<> {
654 let (done, committed) = oneshot::channel();
655 self.send(Write::Notify {
656 change: NotifyChange::Record {
657 target: target.to_owned(),
658 set,
659 remove,
660 },
661 done,
662 });
663 async move { committed.await.unwrap_or(false) }
664 }
665
666 pub fn notify_keep(&self, targets: Vec<String>) -> impl Future<Output = bool> + use<> {
668 let (done, committed) = oneshot::channel();
669 self.send(Write::Notify {
670 change: NotifyChange::Keep(targets),
671 done,
672 });
673 async move { committed.await.unwrap_or(false) }
674 }
675
676 pub fn prune(&self, now: u64, history: &History) -> tokio::task::JoinHandle<()> {
680 let pruner = self.pruner.clone();
681 let closed = self.closed.clone();
682 let history = history.clone();
683 tokio::task::spawn_blocking(move || {
684 let db = match pruner.try_lock() {
685 Ok(db) => db,
686 Err(TryLockError::Poisoned(poisoned)) => poisoned.into_inner(),
687 Err(TryLockError::WouldBlock) => return,
688 };
689 let db = prune::Db {
690 connection: &db,
691 stop: &closed,
692 };
693 if !db.stop.load(Ordering::Relaxed) {
694 prune::run(&db, now, &history);
695 }
696 })
697 }
698
699 pub fn close(&self) {
702 self.closed.store(true, Ordering::Relaxed);
703 }
704
705 pub async fn flush(&self) {
707 let (done, wait) = oneshot::channel();
708 self.send(Write::Flush(done));
709 let _ = wait.await;
710 }
711
712 pub fn subscribe(&self) -> broadcast::Receiver<Arc<EventEnvelope>> {
715 self.events.subscribe()
716 }
717
718 async fn read<T: Send + 'static>(
719 &self,
720 query: impl FnOnce(&Connection) -> rusqlite::Result<T> + Send + 'static,
721 ) -> Result<T, StoreError> {
722 let reader = self.reader.clone();
723 Ok(tokio::task::spawn_blocking(move || query(&lock(&reader))).await??)
725 }
726
727 pub async fn jobs(&self, query: api::JobQuery) -> Result<api::JobPage, StoreError> {
729 self.read(move |db| read::jobs(db, &query)).await
730 }
731
732 pub async fn job(&self, id: u64) -> Result<Option<api::Job>, StoreError> {
734 self.read(move |db| read::job(db, id)).await
735 }
736
737 pub async fn job_samples(
739 &self,
740 id: u64,
741 interval: Duration,
742 ) -> Result<Option<api::SampleSeries>, StoreError> {
743 self.read(move |db| read::job_samples(db, id, interval))
744 .await
745 }
746
747 pub async fn log_lines(
749 &self,
750 job: u64,
751 from: u64,
752 limit: u64,
753 ) -> Result<Vec<(u64, String)>, StoreError> {
754 self.read(move |db| read::log_lines(db, job, from, limit))
755 .await
756 }
757
758 pub async fn events_after(
760 &self,
761 after: u64,
762 limit: u64,
763 ) -> Result<Vec<EventEnvelope>, StoreError> {
764 self.read(move |db| read::events_after(db, after, limit))
765 .await
766 }
767
768 pub async fn sizing_changes(&self, since: u64) -> Result<Vec<api::SizingChange>, StoreError> {
770 self.read(move |db| read::sizing_changes(db, since)).await
771 }
772
773 pub async fn auto_sizing(&self) -> Result<AutoSizing, StoreError> {
775 self.read(read::auto_sizing).await
776 }
777
778 pub fn announced(&self) -> Result<Announced, StoreError> {
781 Ok(read::announced(&lock(&self.reader))?)
782 }
783
784 pub fn paused_repositories(&self) -> Result<Vec<String>, StoreError> {
786 Ok(read::paused(&lock(&self.reader))?)
787 }
788
789 pub async fn host_samples(&self, since: u64) -> Result<Vec<api::HostSample>, StoreError> {
791 let held = lock(&self.held).clone();
794 let mut samples = self.read(move |db| read::host_samples(db, since)).await?;
795 let stored = samples.last().map_or(0, |sample| sample.at);
796 samples.extend(
797 held.into_iter()
798 .filter(|sample| sample.at >= since && sample.at > stored),
799 );
800 Ok(samples)
801 }
802
803 pub async fn activity(&self, since: u64, now: u64) -> Result<api::Activity, StoreError> {
805 self.read(move |db| read::activity(db, since, now)).await
806 }
807
808 pub async fn stats(&self, query: api::StatsQuery, now: u64) -> Result<api::Stats, StoreError> {
810 self.read(move |db| read::stats(db, &query, now)).await
811 }
812
813 pub async fn sizing(&self, since: u64) -> Result<sizing::History, StoreError> {
815 self.read(move |db| read::sizing(db, since)).await
816 }
817
818 pub async fn insights(&self, since: u64) -> Result<Vec<crate::insights::Run>, StoreError> {
820 self.read(move |db| read::insights(db, since)).await
821 }
822
823 pub async fn last_event(&self) -> Result<u64, StoreError> {
825 self.read(read::last_event).await
826 }
827
828 pub async fn notify_state(
830 &self,
831 ) -> Result<BTreeMap<String, BTreeMap<String, u64>>, StoreError> {
832 self.read(read::notify_state).await
833 }
834
835 pub async fn warm(&self, since: u64) -> Result<Vec<warm::Seen>, StoreError> {
837 self.read(move |db| read::warm(db, since)).await
838 }
839
840 pub async fn cache_sizes_now(&self) -> Result<Measured, StoreError> {
842 self.read(read::latest_cache_sizes).await
843 }
844
845 pub async fn build_cache_since(&self, since: u64) -> Result<api::BuildCacheCounts, StoreError> {
847 self.read(move |db| read::build_cache_since(db, since))
848 .await
849 }
850
851 pub async fn cache_history(&self, since: u64) -> Result<Vec<api::CacheSize>, StoreError> {
853 self.read(move |db| read::cache_history(db, since)).await
854 }
855
856 pub async fn open_jobs(&self) -> Result<Vec<OpenJob>, StoreError> {
858 self.read(read::open_jobs).await
859 }
860
861 pub async fn runner_job(&self, runner: &str) -> Result<Option<u64>, StoreError> {
863 let runner = runner.to_owned();
864 self.read(move |db| read::runner_job(db, &runner)).await
865 }
866
867 pub async fn runner_admitted(
869 &self,
870 runner: &str,
871 ) -> Result<Option<api::Admission>, StoreError> {
872 let runner = runner.to_owned();
873 self.read(move |db| read::runner_admission(db, &runner))
874 .await
875 }
876
877 pub async fn log_position(&self, job: u64) -> Result<LogPosition, StoreError> {
879 self.read(move |db| read::log_position(db, job)).await
880 }
881
882 pub async fn mirror_log_until(&self) -> Result<MirrorPosition, StoreError> {
884 self.read(read::mirror_log_until).await
885 }
886}
887
888pub fn sizing_at(path: &Path, since: u64) -> Result<Option<sizing::History>, StoreError> {
893 if !path.exists() {
894 return Ok(None);
895 }
896 let read = |path: &str, flags| -> Result<sizing::History, StoreError> {
897 let db = Connection::open_with_flags(path, flags)?;
898 db.busy_timeout(Duration::from_secs(5))?;
899 sizing_of(&db, since)
900 };
901 let name = path.to_string_lossy();
902 match read(&name, OpenFlags::SQLITE_OPEN_READ_ONLY) {
903 Err(StoreError::Sqlite(error))
904 if matches!(
905 error.sqlite_error_code(),
906 Some(rusqlite::ErrorCode::ReadOnly | rusqlite::ErrorCode::CannotOpen)
907 ) =>
908 {
909 let mut wal = path.as_os_str().to_owned();
911 wal.push("-wal");
912 let wal = PathBuf::from(wal);
913 if std::fs::metadata(&wal).is_ok_and(|wal| wal.len() > 0) {
914 return Err(StoreError::Unread(wal));
915 }
916 let uri = format!(
917 "file:{}?mode=ro&immutable=1",
918 name.replace('%', "%25")
919 .replace('?', "%3f")
920 .replace('#', "%23")
921 );
922 read(
923 &uri,
924 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_URI,
925 )
926 }
927 history => history,
928 }
929 .map(Some)
930}
931
932fn sizing_of(db: &Connection, since: u64) -> Result<sizing::History, StoreError> {
934 let found: u32 = db.pragma_query_value(None, "user_version", |row| row.get(0))?;
935 let supported = u32::try_from(MIGRATIONS.len()).unwrap_or(u32::MAX);
936 if found > supported {
937 return Err(StoreError::Newer { found, supported });
938 }
939 if found == 0 {
940 return Ok(sizing::History::default());
941 }
942 let added = match found {
944 1 => {
945 "NULL AS memory_cap, NULL AS cpu_cap, NULL AS admission, NULL AS oom_kills, NULL AS ooms, \
946 NULL AS memory_measure"
947 }
948 2..=6 => "NULL AS admission, NULL AS oom_kills, NULL AS ooms, NULL AS memory_measure",
949 7 => "NULL AS ooms, NULL AS memory_measure",
950 _ => "",
951 };
952 if found < 13 {
953 db.execute_batch("CREATE TEMP VIEW jobs AS SELECT *, 0 AS host_waited FROM main.jobs")?;
954 }
955 if !added.is_empty() {
956 db.execute_batch(&format!(
957 "CREATE TEMP VIEW runners AS SELECT *, {added} FROM main.runners"
958 ))?;
959 }
960 Ok(read::sizing(db, since)?)
961}
962
963fn configure(db: &Connection) -> rusqlite::Result<()> {
965 db.pragma_update(None, "journal_mode", "wal")?;
966 db.pragma_update(None, "synchronous", "normal")?;
967 db.pragma_update(None, "foreign_keys", "on")?;
968 db.busy_timeout(Duration::from_secs(5))
969}
970
971fn migrate(db: &mut Connection) -> Result<(), StoreError> {
973 let found: u32 = db.pragma_query_value(None, "user_version", |row| row.get(0))?;
974 let supported = u32::try_from(MIGRATIONS.len()).unwrap_or(u32::MAX);
975 if found > supported {
976 return Err(StoreError::Newer { found, supported });
977 }
978 for (version, migration) in (1..).zip(MIGRATIONS).skip(found as usize) {
979 let transaction = db.transaction()?;
980 transaction.execute_batch(migration)?;
981 transaction.pragma_update(None, "user_version", version)?;
982 transaction.commit()?;
983 }
984 Ok(())
985}
986
987fn logged<T>(what: &str, result: rusqlite::Result<T>) -> Option<T> {
989 result
990 .map_err(|error| warn!("job history: cannot {what}: {error}"))
991 .ok()
992}
993
994#[cfg(test)]
995mod tests;