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 include_str!("store/migrations/0014_local_ooms.sql"),
46];
47
48pub(crate) const LOG_BACKLOG: u64 = 8 << 20;
51
52const HOST_SAMPLE_BATCH: usize = 30;
55
56const EVENT_BUFFER: usize = 4096;
58
59#[derive(Debug, thiserror::Error)]
61pub enum StoreError {
62 #[error("{0}")]
64 Sqlite(#[from] rusqlite::Error),
65 #[error(
67 "the job history database is version {found}, newer than the {supported} this runnerd reads; \
68 run a newer runnerd or move the database aside"
69 )]
70 Newer {
71 found: u32,
73 supported: u32,
75 },
76 #[error("{0}")]
78 Io(#[from] std::io::Error),
79 #[error("a database read stopped: {0}")]
81 Join(#[from] tokio::task::JoinError),
82 #[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#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct RunnerRecord {
95 pub name: String,
97 pub repository: String,
99 pub profile: String,
101 pub slot: usize,
103 pub container: String,
105 pub started_at: u64,
107 pub memory_cap: Option<u64>,
109 pub cpu_cap: Option<u64>,
111 pub admission: Option<api::Admission>,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
117pub struct SampleRecord {
118 pub runner: String,
120 pub at: u64,
122 pub cpu: u64,
124 pub cpu_usage: u64,
126 pub memory: u64,
128 pub network_received: u64,
130 pub network_sent: u64,
132 pub events: Events,
134}
135
136#[derive(Debug, Clone, Default, PartialEq, Eq)]
138pub struct Measured {
139 pub at: Option<u64>,
141 pub bytes: BTreeMap<String, u64>,
143}
144
145#[derive(Debug, Clone, Default, PartialEq, Eq)]
148pub struct Announced {
149 pub profiles: Vec<api::ProfileStatus>,
151 pub repositories: Vec<api::RepositoryStatus>,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct OpenJob {
158 pub id: u64,
160 pub repository: String,
162 pub state: api::JobState,
164 pub runner: Option<String>,
166 pub runner_removed_at: Option<u64>,
168 pub run_id: u64,
170 pub run_attempt: u32,
172 pub workflow: Option<String>,
174 pub queued_at: u64,
176 pub labels: Vec<String>,
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182pub struct LogPosition {
183 pub page: u64,
185 pub offset: u64,
187}
188
189#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
193pub struct MirrorPosition {
194 pub at: u64,
196 pub seen: u64,
198}
199
200#[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 position: Option<LogPosition>,
250 max_bytes: u64,
251 _backlog: OwnedSemaphorePermit,
253 },
254 Samples(Vec<SampleRecord>),
255 HostSample(api::HostSample),
256 MirrorBytes {
257 bytes: Vec<(String, u64)>,
258 until: MirrorPosition,
259 done: oneshot::Sender<bool>,
261 },
262 CacheSizes {
263 at: u64,
264 sizes: BTreeMap<String, u64>,
265 },
266 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 done: oneshot::Sender<bool>,
282 },
283 SizingIntent {
284 change: api::SizingChange,
285 done: oneshot::Sender<Option<i64>>,
287 },
288 SizingResolved {
289 id: i64,
290 resolution: Resolution,
291 done: oneshot::Sender<bool>,
293 },
294 Notify {
295 change: NotifyChange,
296 done: oneshot::Sender<bool>,
298 },
299 Flush(oneshot::Sender<()>),
300}
301
302#[derive(Debug)]
304enum NotifyChange {
305 Record {
307 target: String,
308 set: Vec<(String, u64)>,
309 remove: Vec<String>,
310 },
311 Keep(Vec<String>),
313}
314
315#[derive(Debug, Clone, Copy, PartialEq, Eq)]
317pub enum Resolution {
318 Done,
320 Failed,
322 Uncertain,
325}
326
327#[derive(Debug, Clone, Default, PartialEq, Eq)]
329pub struct AutoSizing {
330 pub last: BTreeMap<String, u64>,
332 pub pending: Vec<(i64, api::SizingChange)>,
334 pub cpus: Vec<sizing::CpusSet>,
337 pub memory: Vec<sizing::MemorySet>,
339}
340
341#[derive(Debug, Clone)]
343pub struct Store {
344 writes: mpsc::UnboundedSender<Write>,
345 reader: Arc<Mutex<Connection>>,
346 pruner: Arc<Mutex<Connection>>,
348 closed: Arc<AtomicBool>,
350 held: Arc<Mutex<Vec<api::HostSample>>>,
353 latest_host: Arc<Mutex<Option<api::HostSample>>>,
355 latest_host_memory: Arc<Mutex<Option<api::HostMemory>>>,
357 latest_samples: Arc<Mutex<BTreeMap<String, SampleRecord>>>,
359 events: broadcast::Sender<Arc<EventEnvelope>>,
360 backlog: Arc<Semaphore>,
361}
362
363impl Store {
364 pub fn open(path: &Path) -> Result<Store, StoreError> {
367 if let Some(dir) = path.parent().filter(|dir| !dir.as_os_str().is_empty()) {
368 std::fs::DirBuilder::new()
369 .recursive(true)
370 .mode(0o700)
371 .create(dir)?;
372 }
373 std::fs::OpenOptions::new()
375 .create(true)
376 .append(true)
377 .mode(0o600)
378 .open(path)?;
379 let mut db = Connection::open(path)?;
380 configure(&db)?;
381 migrate(&mut db)?;
382 let reader = Connection::open_with_flags(
383 path,
384 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
385 )?;
386 configure(&reader)?;
387 let pruner = Connection::open(path)?;
388 configure(&pruner)?;
389 let (events, _) = broadcast::channel(EVENT_BUFFER);
390 let (writes, receiver) = mpsc::unbounded_channel();
391 let thread_events = events.clone();
392 let held = Arc::new(Mutex::new(Vec::new()));
393 let thread_held = held.clone();
394 std::thread::Builder::new()
395 .name("runnerd-store".to_owned())
396 .spawn(move || write::run(db, receiver, &thread_events, &thread_held))?;
397 Ok(Store {
398 writes,
399 reader: Arc::new(Mutex::new(reader)),
400 pruner: Arc::new(Mutex::new(pruner)),
401 closed: Arc::new(AtomicBool::new(false)),
402 held,
403 latest_host: Arc::new(Mutex::new(None)),
404 latest_host_memory: Arc::new(Mutex::new(None)),
405 latest_samples: Arc::new(Mutex::new(BTreeMap::new())),
406 events,
407 backlog: Arc::new(Semaphore::new(
408 usize::try_from(LOG_BACKLOG).unwrap_or(usize::MAX),
409 )),
410 })
411 }
412
413 fn send(&self, write: Write) {
414 let _ = self.writes.send(write);
416 }
417
418 pub fn queued(&self, repository: &str, job: &QueuedJob) {
420 self.send(Write::Queued {
421 repository: repository.to_owned(),
422 job: job.clone(),
423 });
424 }
425
426 pub fn runner_started(&self, runner: RunnerRecord) {
428 self.send(Write::RunnerStarted(runner));
429 }
430
431 pub fn runner_removed(&self, runner: &str, at: u64) {
433 lock(&self.latest_samples).remove(runner);
434 self.send(Write::RunnerRemoved {
435 runner: runner.to_owned(),
436 at,
437 });
438 }
439
440 pub fn runners_gone(&self, running: Vec<String>, at: u64) {
443 lock(&self.latest_samples).retain(|runner, _| running.contains(runner));
444 self.send(Write::RunnersGone { running, at });
445 }
446
447 pub fn claimed(&self, job: u64, run_id: u64, runner: &str, at: u64) {
449 self.send(Write::Claimed {
450 job,
451 run_id,
452 runner: runner.to_owned(),
453 at,
454 });
455 }
456
457 pub fn details(&self, details: JobDetails) {
459 self.send(Write::Details { details });
460 }
461
462 pub fn elsewhere(&self, details: JobDetails) {
465 self.send(Write::Elsewhere { details });
466 }
467
468 pub fn lost(&self, job: u64, at: u64) {
470 self.send(Write::Lost { job, at });
471 }
472
473 pub async fn log(&self, job: u64, lines: Vec<String>, position: LogPosition, max_bytes: u64) {
476 let size = |line: &String| line.len() as u64 + 1;
477 let mut lines = lines.into_iter().map(|mut line| {
478 if size(&line) > LOG_BACKLOG {
480 let mut end = usize::try_from(LOG_BACKLOG / 2).unwrap_or(usize::MAX);
481 while !line.is_char_boundary(end) {
482 end -= 1;
483 }
484 line.truncate(end);
485 }
486 line
487 });
488 let mut next = lines.next();
489 loop {
490 let (mut part, mut bytes) = (Vec::new(), 0);
491 while let Some(line) = next.take_if(|line| bytes + size(line) <= LOG_BACKLOG) {
492 bytes += size(&line);
493 part.push(line);
494 next = lines.next();
495 }
496 let last = next.is_none();
497 let share = u32::try_from(bytes.max(1)).unwrap_or(u32::MAX);
498 let Ok(backlog) = self.backlog.clone().acquire_many_owned(share).await else {
500 return;
501 };
502 self.send(Write::Log {
503 job,
504 lines: part,
505 position: last.then_some(position),
506 max_bytes,
507 _backlog: backlog,
508 });
509 if last {
510 return;
511 }
512 }
513 }
514
515 pub fn samples(&self, samples: Vec<SampleRecord>) {
517 let mut latest = lock(&self.latest_samples);
518 for sample in &samples {
519 latest.insert(sample.runner.clone(), sample.clone());
520 }
521 drop(latest);
522 self.send(Write::Samples(samples));
523 }
524
525 pub fn host_sample(&self, sample: api::HostSample) {
528 *lock(&self.latest_host) = Some(sample.clone());
529 lock(&self.held).push(sample.clone());
530 self.send(Write::HostSample(sample));
531 }
532
533 pub fn latest_sample(&self, runner: &str) -> Option<SampleRecord> {
535 lock(&self.latest_samples).get(runner).cloned()
536 }
537
538 pub fn runner_admission(&self, runner: &str, admission: api::Admission) {
540 self.send(Write::RunnerAdmission {
541 runner: runner.to_owned(),
542 admission,
543 });
544 }
545
546 pub fn runner_oom_killed(&self, runner: &str) {
549 self.send(Write::RunnerOomKilled(runner.to_owned()));
550 }
551
552 pub fn build_cache(&self, runner: &str, counts: api::BuildCacheCounts) {
554 self.send(Write::BuildCache {
555 runner: runner.to_owned(),
556 counts,
557 });
558 }
559
560 pub fn host_memory(&self, memory: api::HostMemory) {
562 *lock(&self.latest_host_memory) = Some(memory);
563 }
564
565 pub fn latest_host_memory(&self) -> Option<api::HostMemory> {
567 lock(&self.latest_host_memory).clone()
568 }
569
570 pub fn host_waited(&self, jobs: Vec<u64>, ms: u64) {
573 self.send(Write::HostWaited { jobs, ms });
574 }
575
576 pub fn latest_host_sample(&self) -> Option<api::HostSample> {
578 lock(&self.latest_host).clone()
579 }
580
581 pub async fn mirror_bytes(&self, bytes: Vec<(String, u64)>, until: MirrorPosition) -> bool {
584 let (done, committed) = oneshot::channel();
585 self.send(Write::MirrorBytes { bytes, until, done });
586 committed.await.unwrap_or(false)
587 }
588
589 pub fn cache_sizes(&self, at: u64, sizes: BTreeMap<String, u64>) {
591 self.send(Write::CacheSizes { at, sizes });
592 }
593
594 pub fn event(&self, event: Event) {
596 self.send(Write::Event(Box::new(event)));
597 }
598
599 pub fn record_made(&self, record: api::Record) {
601 self.send(Write::RecordMade(record));
602 }
603
604 pub fn record_skipped(&self, job: u64, why: String) {
606 self.send(Write::RecordSkipped { job, why });
607 }
608
609 pub fn record_removed(&self, job: u64, at: u64, by: api::RecordRemoval) {
611 self.send(Write::RecordRemoved { job, at, by });
612 }
613
614 pub fn paused(&self, repositories: Vec<String>) -> impl Future<Output = bool> + use<> {
617 let (done, committed) = oneshot::channel();
618 self.send(Write::Paused { repositories, done });
619 async move { committed.await.unwrap_or(false) }
620 }
621
622 pub fn sizing_intent(
625 &self,
626 change: api::SizingChange,
627 ) -> impl Future<Output = Option<i64>> + use<> {
628 let (done, committed) = oneshot::channel();
629 self.send(Write::SizingIntent { change, done });
630 async move { committed.await.ok().flatten() }
631 }
632
633 pub fn sizing_resolved(
636 &self,
637 id: i64,
638 resolution: Resolution,
639 ) -> impl Future<Output = bool> + use<> {
640 let (done, committed) = oneshot::channel();
641 self.send(Write::SizingResolved {
642 id,
643 resolution,
644 done,
645 });
646 async move { committed.await.unwrap_or(false) }
647 }
648
649 pub fn notify_record(
652 &self,
653 target: &str,
654 set: Vec<(String, u64)>,
655 remove: Vec<String>,
656 ) -> impl Future<Output = bool> + use<> {
657 let (done, committed) = oneshot::channel();
658 self.send(Write::Notify {
659 change: NotifyChange::Record {
660 target: target.to_owned(),
661 set,
662 remove,
663 },
664 done,
665 });
666 async move { committed.await.unwrap_or(false) }
667 }
668
669 pub fn notify_keep(&self, targets: Vec<String>) -> impl Future<Output = bool> + use<> {
671 let (done, committed) = oneshot::channel();
672 self.send(Write::Notify {
673 change: NotifyChange::Keep(targets),
674 done,
675 });
676 async move { committed.await.unwrap_or(false) }
677 }
678
679 pub fn prune(&self, now: u64, history: &History) -> tokio::task::JoinHandle<()> {
683 let pruner = self.pruner.clone();
684 let closed = self.closed.clone();
685 let history = history.clone();
686 tokio::task::spawn_blocking(move || {
687 let db = match pruner.try_lock() {
688 Ok(db) => db,
689 Err(TryLockError::Poisoned(poisoned)) => poisoned.into_inner(),
690 Err(TryLockError::WouldBlock) => return,
691 };
692 let db = prune::Db {
693 connection: &db,
694 stop: &closed,
695 };
696 if !db.stop.load(Ordering::Relaxed) {
697 prune::run(&db, now, &history);
698 }
699 })
700 }
701
702 pub fn close(&self) {
705 self.closed.store(true, Ordering::Relaxed);
706 }
707
708 pub async fn flush(&self) {
710 let (done, wait) = oneshot::channel();
711 self.send(Write::Flush(done));
712 let _ = wait.await;
713 }
714
715 pub fn subscribe(&self) -> broadcast::Receiver<Arc<EventEnvelope>> {
718 self.events.subscribe()
719 }
720
721 async fn read<T: Send + 'static>(
722 &self,
723 query: impl FnOnce(&Connection) -> rusqlite::Result<T> + Send + 'static,
724 ) -> Result<T, StoreError> {
725 let reader = self.reader.clone();
726 Ok(tokio::task::spawn_blocking(move || query(&lock(&reader))).await??)
728 }
729
730 pub async fn jobs(&self, query: api::JobQuery) -> Result<api::JobPage, StoreError> {
732 self.read(move |db| read::jobs(db, &query)).await
733 }
734
735 pub async fn job(&self, id: u64) -> Result<Option<api::Job>, StoreError> {
737 self.read(move |db| read::job(db, id)).await
738 }
739
740 pub async fn job_samples(
742 &self,
743 id: u64,
744 interval: Duration,
745 ) -> Result<Option<api::SampleSeries>, StoreError> {
746 self.read(move |db| read::job_samples(db, id, interval))
747 .await
748 }
749
750 pub async fn log_lines(
752 &self,
753 job: u64,
754 from: u64,
755 limit: u64,
756 ) -> Result<Vec<(u64, String)>, StoreError> {
757 self.read(move |db| read::log_lines(db, job, from, limit))
758 .await
759 }
760
761 pub async fn events_after(
763 &self,
764 after: u64,
765 limit: u64,
766 ) -> Result<Vec<EventEnvelope>, StoreError> {
767 self.read(move |db| read::events_after(db, after, limit))
768 .await
769 }
770
771 pub async fn sizing_changes(&self, since: u64) -> Result<Vec<api::SizingChange>, StoreError> {
773 self.read(move |db| read::sizing_changes(db, since)).await
774 }
775
776 pub async fn auto_sizing(&self) -> Result<AutoSizing, StoreError> {
778 self.read(read::auto_sizing).await
779 }
780
781 pub fn announced(&self) -> Result<Announced, StoreError> {
784 Ok(read::announced(&lock(&self.reader))?)
785 }
786
787 pub fn paused_repositories(&self) -> Result<Vec<String>, StoreError> {
789 Ok(read::paused(&lock(&self.reader))?)
790 }
791
792 pub async fn host_samples(&self, since: u64) -> Result<Vec<api::HostSample>, StoreError> {
794 let held = lock(&self.held).clone();
797 let mut samples = self.read(move |db| read::host_samples(db, since)).await?;
798 let stored = samples.last().map_or(0, |sample| sample.at);
799 samples.extend(
800 held.into_iter()
801 .filter(|sample| sample.at >= since && sample.at > stored),
802 );
803 Ok(samples)
804 }
805
806 pub async fn activity(&self, since: u64, now: u64) -> Result<api::Activity, StoreError> {
808 self.read(move |db| read::activity(db, since, now)).await
809 }
810
811 pub async fn stats(&self, query: api::StatsQuery, now: u64) -> Result<api::Stats, StoreError> {
813 self.read(move |db| read::stats(db, &query, now)).await
814 }
815
816 pub async fn sizing(&self, since: u64) -> Result<sizing::History, StoreError> {
818 self.read(move |db| read::sizing(db, since)).await
819 }
820
821 pub async fn insights(&self, since: u64) -> Result<Vec<crate::insights::Run>, StoreError> {
823 self.read(move |db| read::insights(db, since)).await
824 }
825
826 pub async fn last_event(&self) -> Result<u64, StoreError> {
828 self.read(read::last_event).await
829 }
830
831 pub async fn notify_state(
833 &self,
834 ) -> Result<BTreeMap<String, BTreeMap<String, u64>>, StoreError> {
835 self.read(read::notify_state).await
836 }
837
838 pub async fn warm(&self, since: u64) -> Result<Vec<warm::Seen>, StoreError> {
840 self.read(move |db| read::warm(db, since)).await
841 }
842
843 pub async fn cache_sizes_now(&self) -> Result<Measured, StoreError> {
845 self.read(read::latest_cache_sizes).await
846 }
847
848 pub async fn build_cache_since(&self, since: u64) -> Result<api::BuildCacheCounts, StoreError> {
850 self.read(move |db| read::build_cache_since(db, since))
851 .await
852 }
853
854 pub async fn cache_history(&self, since: u64) -> Result<Vec<api::CacheSize>, StoreError> {
856 self.read(move |db| read::cache_history(db, since)).await
857 }
858
859 pub async fn open_jobs(&self) -> Result<Vec<OpenJob>, StoreError> {
861 self.read(read::open_jobs).await
862 }
863
864 pub async fn runner_job(&self, runner: &str) -> Result<Option<u64>, StoreError> {
866 let runner = runner.to_owned();
867 self.read(move |db| read::runner_job(db, &runner)).await
868 }
869
870 pub async fn runner_admitted(
872 &self,
873 runner: &str,
874 ) -> Result<Option<api::Admission>, StoreError> {
875 let runner = runner.to_owned();
876 self.read(move |db| read::runner_admission(db, &runner))
877 .await
878 }
879
880 pub async fn log_position(&self, job: u64) -> Result<LogPosition, StoreError> {
882 self.read(move |db| read::log_position(db, job)).await
883 }
884
885 pub async fn mirror_log_until(&self) -> Result<MirrorPosition, StoreError> {
887 self.read(read::mirror_log_until).await
888 }
889}
890
891pub fn sizing_at(path: &Path, since: u64) -> Result<Option<sizing::History>, StoreError> {
896 if !path.exists() {
897 return Ok(None);
898 }
899 let read = |path: &str, flags| -> Result<sizing::History, StoreError> {
900 let db = Connection::open_with_flags(path, flags)?;
901 db.busy_timeout(Duration::from_secs(5))?;
902 sizing_of(&db, since)
903 };
904 let name = path.to_string_lossy();
905 match read(&name, OpenFlags::SQLITE_OPEN_READ_ONLY) {
906 Err(StoreError::Sqlite(error))
907 if matches!(
908 error.sqlite_error_code(),
909 Some(rusqlite::ErrorCode::ReadOnly | rusqlite::ErrorCode::CannotOpen)
910 ) =>
911 {
912 let mut wal = path.as_os_str().to_owned();
914 wal.push("-wal");
915 let wal = PathBuf::from(wal);
916 if std::fs::metadata(&wal).is_ok_and(|wal| wal.len() > 0) {
917 return Err(StoreError::Unread(wal));
918 }
919 let uri = format!(
920 "file:{}?mode=ro&immutable=1",
921 name.replace('%', "%25")
922 .replace('?', "%3f")
923 .replace('#', "%23")
924 );
925 read(
926 &uri,
927 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_URI,
928 )
929 }
930 history => history,
931 }
932 .map(Some)
933}
934
935fn sizing_of(db: &Connection, since: u64) -> Result<sizing::History, StoreError> {
937 let found: u32 = db.pragma_query_value(None, "user_version", |row| row.get(0))?;
938 let supported = u32::try_from(MIGRATIONS.len()).unwrap_or(u32::MAX);
939 if found > supported {
940 return Err(StoreError::Newer { found, supported });
941 }
942 if found == 0 {
943 return Ok(sizing::History::default());
944 }
945 let mut added = match found {
947 1 => {
948 "NULL AS memory_cap, NULL AS cpu_cap, NULL AS admission, NULL AS oom_kills, NULL AS ooms, \
949 NULL AS memory_measure, "
950 }
951 2..=6 => "NULL AS admission, NULL AS oom_kills, NULL AS ooms, NULL AS memory_measure, ",
952 7 => "NULL AS ooms, NULL AS memory_measure, ",
953 _ => "",
954 }
955 .to_owned();
956 if found < 14 {
957 added.push_str("NULL AS local_ooms");
958 }
959 let added = added.trim_end_matches(", ");
960 if found < 13 {
961 db.execute_batch("CREATE TEMP VIEW jobs AS SELECT *, 0 AS host_waited FROM main.jobs")?;
962 }
963 if !added.is_empty() {
964 db.execute_batch(&format!(
965 "CREATE TEMP VIEW runners AS SELECT *, {added} FROM main.runners"
966 ))?;
967 }
968 let mut history = read::sizing(db, since)?;
969 if found >= 14 {
971 history.memory_set = read::auto_sizing(db)?.memory;
972 history.changes = read::sizing_changes(db, since.saturating_sub(sizing::memory::HISTORY))?;
973 }
974 Ok(history)
975}
976
977fn configure(db: &Connection) -> rusqlite::Result<()> {
979 db.pragma_update(None, "journal_mode", "wal")?;
980 db.pragma_update(None, "synchronous", "normal")?;
981 db.pragma_update(None, "foreign_keys", "on")?;
982 db.busy_timeout(Duration::from_secs(5))
983}
984
985fn migrate(db: &mut Connection) -> Result<(), StoreError> {
987 let found: u32 = db.pragma_query_value(None, "user_version", |row| row.get(0))?;
988 let supported = u32::try_from(MIGRATIONS.len()).unwrap_or(u32::MAX);
989 if found > supported {
990 return Err(StoreError::Newer { found, supported });
991 }
992 for (version, migration) in (1..).zip(MIGRATIONS).skip(found as usize) {
993 let transaction = db.transaction()?;
994 transaction.execute_batch(migration)?;
995 transaction.pragma_update(None, "user_version", version)?;
996 transaction.commit()?;
997 }
998 Ok(())
999}
1000
1001fn 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;