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 include_str!("store/migrations/0015_repositories.sql"),
47];
48
49pub(crate) const LOG_BACKLOG: u64 = 8 << 20;
52
53const HOST_SAMPLE_BATCH: usize = 30;
56
57const EVENT_BUFFER: usize = 4096;
59
60#[derive(Debug, thiserror::Error)]
62pub enum StoreError {
63 #[error("{0}")]
65 Sqlite(#[from] rusqlite::Error),
66 #[error(
68 "the job history database is version {found}, newer than the {supported} this runnerd reads; \
69 run a newer runnerd or move the database aside"
70 )]
71 Newer {
72 found: u32,
74 supported: u32,
76 },
77 #[error("{0}")]
79 Io(#[from] std::io::Error),
80 #[error("a database read stopped: {0}")]
82 Join(#[from] tokio::task::JoinError),
83 #[error(
86 "{} holds changes not yet in the job history, which this user cannot read; run runnerd doctor as the user \
87 the daemon runs as",
88 .0.display()
89 )]
90 Unread(PathBuf),
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct RunnerRecord {
96 pub name: String,
98 pub repository: String,
100 pub profile: String,
102 pub slot: usize,
104 pub container: String,
106 pub started_at: u64,
108 pub memory_cap: Option<u64>,
110 pub cpu_cap: Option<u64>,
112 pub admission: Option<api::Admission>,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct SampleRecord {
119 pub runner: String,
121 pub at: u64,
123 pub cpu: u64,
125 pub cpu_usage: u64,
127 pub memory: u64,
129 pub network_received: u64,
131 pub network_sent: u64,
133 pub events: Events,
135}
136
137#[derive(Debug, Clone, Default, PartialEq, Eq)]
139pub struct Measured {
140 pub at: Option<u64>,
142 pub bytes: BTreeMap<String, u64>,
144}
145
146#[derive(Debug, Clone, Default, PartialEq, Eq)]
149pub struct Announced {
150 pub profiles: Vec<api::ProfileStatus>,
152 pub repositories: Vec<api::RepositoryStatus>,
154}
155
156#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct OpenJob {
159 pub id: u64,
161 pub repository: String,
163 pub state: api::JobState,
165 pub runner: Option<String>,
167 pub runner_removed_at: Option<u64>,
169 pub run_id: u64,
171 pub run_attempt: u32,
173 pub workflow: Option<String>,
175 pub queued_at: u64,
177 pub labels: Vec<String>,
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183pub struct LogPosition {
184 pub page: u64,
186 pub offset: u64,
188}
189
190#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
194pub struct MirrorPosition {
195 pub at: u64,
197 pub seen: u64,
199}
200
201#[derive(Debug)]
203enum Write {
204 Queued {
205 repository: String,
206 job: QueuedJob,
207 },
208 RunnerStarted(RunnerRecord),
209 RunnerAdmission {
210 runner: String,
211 admission: api::Admission,
212 },
213 RunnerOomKilled(String),
214 HostWaited {
215 jobs: Vec<u64>,
216 ms: u64,
217 },
218 BuildCache {
219 runner: String,
220 counts: api::BuildCacheCounts,
221 },
222 RunnerRemoved {
223 runner: String,
224 at: u64,
225 },
226 RunnersGone {
227 running: Vec<String>,
228 at: u64,
229 },
230 Claimed {
231 job: u64,
232 run_id: u64,
233 runner: String,
234 at: u64,
235 },
236 Details {
237 details: JobDetails,
238 },
239 Elsewhere {
240 details: JobDetails,
241 },
242 Lost {
243 job: u64,
244 at: u64,
245 },
246 Log {
247 job: u64,
248 lines: Vec<String>,
249 position: Option<LogPosition>,
251 max_bytes: u64,
252 _backlog: OwnedSemaphorePermit,
254 },
255 Samples(Vec<SampleRecord>),
256 HostSample(api::HostSample),
257 MirrorBytes {
258 bytes: Vec<(String, u64)>,
259 until: MirrorPosition,
260 done: oneshot::Sender<bool>,
262 },
263 CacheSizes {
264 at: u64,
265 sizes: BTreeMap<String, u64>,
266 },
267 Event(Box<Event>),
269 RecordMade(api::Record),
270 RecordSkipped {
271 job: u64,
272 why: String,
273 },
274 RecordRemoved {
275 job: u64,
276 at: u64,
277 by: api::RecordRemoval,
278 },
279 Paused {
280 repositories: Vec<String>,
281 done: oneshot::Sender<bool>,
283 },
284 Visibility {
285 repository: String,
286 visibility: api::Visibility,
287 at: u64,
288 },
289 SizingIntent {
290 change: api::SizingChange,
291 done: oneshot::Sender<Option<i64>>,
293 },
294 SizingResolved {
295 id: i64,
296 resolution: Resolution,
297 done: oneshot::Sender<bool>,
299 },
300 Notify {
301 change: NotifyChange,
302 done: oneshot::Sender<bool>,
304 },
305 Flush(oneshot::Sender<()>),
306}
307
308#[derive(Debug)]
310enum NotifyChange {
311 Record {
313 target: String,
314 set: Vec<(String, u64)>,
315 remove: Vec<String>,
316 },
317 Keep(Vec<String>),
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
323pub enum Resolution {
324 Done,
326 Failed,
328 Uncertain,
331}
332
333#[derive(Debug, Clone, Default, PartialEq, Eq)]
335pub struct AutoSizing {
336 pub last: BTreeMap<String, u64>,
338 pub pending: Vec<(i64, api::SizingChange)>,
340 pub cpus: Vec<sizing::CpusSet>,
343 pub memory: Vec<sizing::MemorySet>,
345}
346
347#[derive(Debug, Clone)]
349pub struct Store {
350 writes: mpsc::UnboundedSender<Write>,
351 reader: Arc<Mutex<Connection>>,
352 pruner: Arc<Mutex<Connection>>,
354 closed: Arc<AtomicBool>,
356 held: Arc<Mutex<Vec<api::HostSample>>>,
359 latest_host: Arc<Mutex<Option<api::HostSample>>>,
361 latest_host_memory: Arc<Mutex<Option<api::HostMemory>>>,
363 latest_samples: Arc<Mutex<BTreeMap<String, SampleRecord>>>,
365 events: broadcast::Sender<Arc<EventEnvelope>>,
366 backlog: Arc<Semaphore>,
367}
368
369impl Store {
370 pub fn open(path: &Path) -> Result<Store, StoreError> {
373 if let Some(dir) = path.parent().filter(|dir| !dir.as_os_str().is_empty()) {
374 std::fs::DirBuilder::new()
375 .recursive(true)
376 .mode(0o700)
377 .create(dir)?;
378 }
379 std::fs::OpenOptions::new()
381 .create(true)
382 .append(true)
383 .mode(0o600)
384 .open(path)?;
385 let mut db = Connection::open(path)?;
386 configure(&db)?;
387 migrate(&mut db)?;
388 let reader = Connection::open_with_flags(
389 path,
390 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
391 )?;
392 configure(&reader)?;
393 let pruner = Connection::open(path)?;
394 configure(&pruner)?;
395 let (events, _) = broadcast::channel(EVENT_BUFFER);
396 let (writes, receiver) = mpsc::unbounded_channel();
397 let thread_events = events.clone();
398 let held = Arc::new(Mutex::new(Vec::new()));
399 let thread_held = held.clone();
400 std::thread::Builder::new()
401 .name("runnerd-store".to_owned())
402 .spawn(move || write::run(db, receiver, &thread_events, &thread_held))?;
403 Ok(Store {
404 writes,
405 reader: Arc::new(Mutex::new(reader)),
406 pruner: Arc::new(Mutex::new(pruner)),
407 closed: Arc::new(AtomicBool::new(false)),
408 held,
409 latest_host: Arc::new(Mutex::new(None)),
410 latest_host_memory: Arc::new(Mutex::new(None)),
411 latest_samples: Arc::new(Mutex::new(BTreeMap::new())),
412 events,
413 backlog: Arc::new(Semaphore::new(
414 usize::try_from(LOG_BACKLOG).unwrap_or(usize::MAX),
415 )),
416 })
417 }
418
419 fn send(&self, write: Write) {
420 let _ = self.writes.send(write);
422 }
423
424 pub fn queued(&self, repository: &str, job: &QueuedJob) {
426 self.send(Write::Queued {
427 repository: repository.to_owned(),
428 job: job.clone(),
429 });
430 }
431
432 pub fn runner_started(&self, runner: RunnerRecord) {
434 self.send(Write::RunnerStarted(runner));
435 }
436
437 pub fn runner_removed(&self, runner: &str, at: u64) {
439 lock(&self.latest_samples).remove(runner);
440 self.send(Write::RunnerRemoved {
441 runner: runner.to_owned(),
442 at,
443 });
444 }
445
446 pub fn runners_gone(&self, running: Vec<String>, at: u64) {
449 lock(&self.latest_samples).retain(|runner, _| running.contains(runner));
450 self.send(Write::RunnersGone { running, at });
451 }
452
453 pub fn claimed(&self, job: u64, run_id: u64, runner: &str, at: u64) {
455 self.send(Write::Claimed {
456 job,
457 run_id,
458 runner: runner.to_owned(),
459 at,
460 });
461 }
462
463 pub fn details(&self, details: JobDetails) {
465 self.send(Write::Details { details });
466 }
467
468 pub fn elsewhere(&self, details: JobDetails) {
471 self.send(Write::Elsewhere { details });
472 }
473
474 pub fn lost(&self, job: u64, at: u64) {
476 self.send(Write::Lost { job, at });
477 }
478
479 pub async fn log(&self, job: u64, lines: Vec<String>, position: LogPosition, max_bytes: u64) {
482 let size = |line: &String| line.len() as u64 + 1;
483 let mut lines = lines.into_iter().map(|mut line| {
484 if size(&line) > LOG_BACKLOG {
486 let mut end = usize::try_from(LOG_BACKLOG / 2).unwrap_or(usize::MAX);
487 while !line.is_char_boundary(end) {
488 end -= 1;
489 }
490 line.truncate(end);
491 }
492 line
493 });
494 let mut next = lines.next();
495 loop {
496 let (mut part, mut bytes) = (Vec::new(), 0);
497 while let Some(line) = next.take_if(|line| bytes + size(line) <= LOG_BACKLOG) {
498 bytes += size(&line);
499 part.push(line);
500 next = lines.next();
501 }
502 let last = next.is_none();
503 let share = u32::try_from(bytes.max(1)).unwrap_or(u32::MAX);
504 let Ok(backlog) = self.backlog.clone().acquire_many_owned(share).await else {
506 return;
507 };
508 self.send(Write::Log {
509 job,
510 lines: part,
511 position: last.then_some(position),
512 max_bytes,
513 _backlog: backlog,
514 });
515 if last {
516 return;
517 }
518 }
519 }
520
521 pub fn samples(&self, samples: Vec<SampleRecord>) {
523 let mut latest = lock(&self.latest_samples);
524 for sample in &samples {
525 latest.insert(sample.runner.clone(), sample.clone());
526 }
527 drop(latest);
528 self.send(Write::Samples(samples));
529 }
530
531 pub fn host_sample(&self, sample: api::HostSample) {
534 *lock(&self.latest_host) = Some(sample.clone());
535 lock(&self.held).push(sample.clone());
536 self.send(Write::HostSample(sample));
537 }
538
539 pub fn latest_sample(&self, runner: &str) -> Option<SampleRecord> {
541 lock(&self.latest_samples).get(runner).cloned()
542 }
543
544 pub fn runner_admission(&self, runner: &str, admission: api::Admission) {
546 self.send(Write::RunnerAdmission {
547 runner: runner.to_owned(),
548 admission,
549 });
550 }
551
552 pub fn runner_oom_killed(&self, runner: &str) {
555 self.send(Write::RunnerOomKilled(runner.to_owned()));
556 }
557
558 pub fn build_cache(&self, runner: &str, counts: api::BuildCacheCounts) {
560 self.send(Write::BuildCache {
561 runner: runner.to_owned(),
562 counts,
563 });
564 }
565
566 pub fn host_memory(&self, memory: api::HostMemory) {
568 *lock(&self.latest_host_memory) = Some(memory);
569 }
570
571 pub fn latest_host_memory(&self) -> Option<api::HostMemory> {
573 lock(&self.latest_host_memory).clone()
574 }
575
576 pub fn host_waited(&self, jobs: Vec<u64>, ms: u64) {
579 self.send(Write::HostWaited { jobs, ms });
580 }
581
582 pub fn latest_host_sample(&self) -> Option<api::HostSample> {
584 lock(&self.latest_host).clone()
585 }
586
587 pub async fn mirror_bytes(&self, bytes: Vec<(String, u64)>, until: MirrorPosition) -> bool {
590 let (done, committed) = oneshot::channel();
591 self.send(Write::MirrorBytes { bytes, until, done });
592 committed.await.unwrap_or(false)
593 }
594
595 pub fn cache_sizes(&self, at: u64, sizes: BTreeMap<String, u64>) {
597 self.send(Write::CacheSizes { at, sizes });
598 }
599
600 pub fn event(&self, event: Event) {
602 self.send(Write::Event(Box::new(event)));
603 }
604
605 pub fn record_made(&self, record: api::Record) {
607 self.send(Write::RecordMade(record));
608 }
609
610 pub fn record_skipped(&self, job: u64, why: String) {
612 self.send(Write::RecordSkipped { job, why });
613 }
614
615 pub fn record_removed(&self, job: u64, at: u64, by: api::RecordRemoval) {
617 self.send(Write::RecordRemoved { job, at, by });
618 }
619
620 pub fn paused(&self, repositories: Vec<String>) -> impl Future<Output = bool> + use<> {
623 let (done, committed) = oneshot::channel();
624 self.send(Write::Paused { repositories, done });
625 async move { committed.await.unwrap_or(false) }
626 }
627
628 pub fn visibility(&self, repository: String, visibility: api::Visibility, at: u64) {
630 self.send(Write::Visibility {
631 repository,
632 visibility,
633 at,
634 });
635 }
636
637 pub fn sizing_intent(
640 &self,
641 change: api::SizingChange,
642 ) -> impl Future<Output = Option<i64>> + use<> {
643 let (done, committed) = oneshot::channel();
644 self.send(Write::SizingIntent { change, done });
645 async move { committed.await.ok().flatten() }
646 }
647
648 pub fn sizing_resolved(
651 &self,
652 id: i64,
653 resolution: Resolution,
654 ) -> impl Future<Output = bool> + use<> {
655 let (done, committed) = oneshot::channel();
656 self.send(Write::SizingResolved {
657 id,
658 resolution,
659 done,
660 });
661 async move { committed.await.unwrap_or(false) }
662 }
663
664 pub fn notify_record(
667 &self,
668 target: &str,
669 set: Vec<(String, u64)>,
670 remove: Vec<String>,
671 ) -> impl Future<Output = bool> + use<> {
672 let (done, committed) = oneshot::channel();
673 self.send(Write::Notify {
674 change: NotifyChange::Record {
675 target: target.to_owned(),
676 set,
677 remove,
678 },
679 done,
680 });
681 async move { committed.await.unwrap_or(false) }
682 }
683
684 pub fn notify_keep(&self, targets: Vec<String>) -> impl Future<Output = bool> + use<> {
686 let (done, committed) = oneshot::channel();
687 self.send(Write::Notify {
688 change: NotifyChange::Keep(targets),
689 done,
690 });
691 async move { committed.await.unwrap_or(false) }
692 }
693
694 pub fn prune(&self, now: u64, history: &History) -> tokio::task::JoinHandle<()> {
698 let pruner = self.pruner.clone();
699 let closed = self.closed.clone();
700 let history = history.clone();
701 tokio::task::spawn_blocking(move || {
702 let db = match pruner.try_lock() {
703 Ok(db) => db,
704 Err(TryLockError::Poisoned(poisoned)) => poisoned.into_inner(),
705 Err(TryLockError::WouldBlock) => return,
706 };
707 let db = prune::Db {
708 connection: &db,
709 stop: &closed,
710 };
711 if !db.stop.load(Ordering::Relaxed) {
712 prune::run(&db, now, &history);
713 }
714 })
715 }
716
717 pub fn close(&self) {
720 self.closed.store(true, Ordering::Relaxed);
721 }
722
723 pub async fn flush(&self) {
725 let (done, wait) = oneshot::channel();
726 self.send(Write::Flush(done));
727 let _ = wait.await;
728 }
729
730 pub fn subscribe(&self) -> broadcast::Receiver<Arc<EventEnvelope>> {
733 self.events.subscribe()
734 }
735
736 async fn read<T: Send + 'static>(
737 &self,
738 query: impl FnOnce(&Connection) -> rusqlite::Result<T> + Send + 'static,
739 ) -> Result<T, StoreError> {
740 let reader = self.reader.clone();
741 Ok(tokio::task::spawn_blocking(move || query(&lock(&reader))).await??)
743 }
744
745 pub async fn jobs(&self, query: api::JobQuery) -> Result<api::JobPage, StoreError> {
747 self.read(move |db| read::jobs(db, &query)).await
748 }
749
750 pub async fn job(&self, id: u64) -> Result<Option<api::Job>, StoreError> {
752 self.read(move |db| read::job(db, id)).await
753 }
754
755 pub async fn job_samples(
757 &self,
758 id: u64,
759 interval: Duration,
760 ) -> Result<Option<api::SampleSeries>, StoreError> {
761 self.read(move |db| read::job_samples(db, id, interval))
762 .await
763 }
764
765 pub async fn log_lines(
767 &self,
768 job: u64,
769 from: u64,
770 limit: u64,
771 ) -> Result<Vec<(u64, String)>, StoreError> {
772 self.read(move |db| read::log_lines(db, job, from, limit))
773 .await
774 }
775
776 pub async fn events_after(
778 &self,
779 after: u64,
780 limit: u64,
781 ) -> Result<Vec<EventEnvelope>, StoreError> {
782 self.read(move |db| read::events_after(db, after, limit))
783 .await
784 }
785
786 pub async fn sizing_changes(&self, since: u64) -> Result<Vec<api::SizingChange>, StoreError> {
788 self.read(move |db| read::sizing_changes(db, since)).await
789 }
790
791 pub async fn auto_sizing(&self) -> Result<AutoSizing, StoreError> {
793 self.read(read::auto_sizing).await
794 }
795
796 pub fn announced(&self) -> Result<Announced, StoreError> {
799 Ok(read::announced(&lock(&self.reader))?)
800 }
801
802 pub fn paused_repositories(&self) -> Result<Vec<String>, StoreError> {
804 Ok(read::paused(&lock(&self.reader))?)
805 }
806
807 pub async fn host_samples(&self, since: u64) -> Result<Vec<api::HostSample>, StoreError> {
809 let held = lock(&self.held).clone();
812 let mut samples = self.read(move |db| read::host_samples(db, since)).await?;
813 let stored = samples.last().map_or(0, |sample| sample.at);
814 samples.extend(
815 held.into_iter()
816 .filter(|sample| sample.at >= since && sample.at > stored),
817 );
818 Ok(samples)
819 }
820
821 pub async fn activity(&self, since: u64, now: u64) -> Result<api::Activity, StoreError> {
823 self.read(move |db| read::activity(db, since, now)).await
824 }
825
826 pub async fn stats(&self, query: api::StatsQuery, now: u64) -> Result<api::Stats, StoreError> {
828 self.read(move |db| read::stats(db, &query, now)).await
829 }
830
831 pub async fn sizing(&self, since: u64) -> Result<sizing::History, StoreError> {
833 self.read(move |db| read::sizing(db, since)).await
834 }
835
836 pub async fn pricing(&self, since: u64) -> Result<api::PricingHistory, StoreError> {
839 self.read(move |db| read::pricing(db, since)).await
840 }
841
842 pub async fn insights(&self, since: u64) -> Result<Vec<crate::insights::Run>, StoreError> {
844 self.read(move |db| read::insights(db, since)).await
845 }
846
847 pub async fn last_event(&self) -> Result<u64, StoreError> {
849 self.read(read::last_event).await
850 }
851
852 pub async fn notify_state(
854 &self,
855 ) -> Result<BTreeMap<String, BTreeMap<String, u64>>, StoreError> {
856 self.read(read::notify_state).await
857 }
858
859 pub async fn warm(&self, since: u64) -> Result<Vec<warm::Seen>, StoreError> {
861 self.read(move |db| read::warm(db, since)).await
862 }
863
864 pub async fn cache_sizes_now(&self) -> Result<Measured, StoreError> {
866 self.read(read::latest_cache_sizes).await
867 }
868
869 pub async fn build_cache_since(&self, since: u64) -> Result<api::BuildCacheCounts, StoreError> {
871 self.read(move |db| read::build_cache_since(db, since))
872 .await
873 }
874
875 pub async fn cache_history(&self, since: u64) -> Result<Vec<api::CacheSize>, StoreError> {
877 self.read(move |db| read::cache_history(db, since)).await
878 }
879
880 pub async fn open_jobs(&self) -> Result<Vec<OpenJob>, StoreError> {
882 self.read(read::open_jobs).await
883 }
884
885 pub async fn runner_job(&self, runner: &str) -> Result<Option<u64>, StoreError> {
887 let runner = runner.to_owned();
888 self.read(move |db| read::runner_job(db, &runner)).await
889 }
890
891 pub async fn runner_admitted(
893 &self,
894 runner: &str,
895 ) -> Result<Option<api::Admission>, StoreError> {
896 let runner = runner.to_owned();
897 self.read(move |db| read::runner_admission(db, &runner))
898 .await
899 }
900
901 pub async fn log_position(&self, job: u64) -> Result<LogPosition, StoreError> {
903 self.read(move |db| read::log_position(db, job)).await
904 }
905
906 pub async fn mirror_log_until(&self) -> Result<MirrorPosition, StoreError> {
908 self.read(read::mirror_log_until).await
909 }
910}
911
912pub fn sizing_at(path: &Path, since: u64) -> Result<Option<sizing::History>, StoreError> {
917 if !path.exists() {
918 return Ok(None);
919 }
920 let read = |path: &str, flags| -> Result<sizing::History, StoreError> {
921 let db = Connection::open_with_flags(path, flags)?;
922 db.busy_timeout(Duration::from_secs(5))?;
923 sizing_of(&db, since)
924 };
925 let name = path.to_string_lossy();
926 match read(&name, OpenFlags::SQLITE_OPEN_READ_ONLY) {
927 Err(StoreError::Sqlite(error))
928 if matches!(
929 error.sqlite_error_code(),
930 Some(rusqlite::ErrorCode::ReadOnly | rusqlite::ErrorCode::CannotOpen)
931 ) =>
932 {
933 let mut wal = path.as_os_str().to_owned();
935 wal.push("-wal");
936 let wal = PathBuf::from(wal);
937 if std::fs::metadata(&wal).is_ok_and(|wal| wal.len() > 0) {
938 return Err(StoreError::Unread(wal));
939 }
940 let uri = format!(
941 "file:{}?mode=ro&immutable=1",
942 name.replace('%', "%25")
943 .replace('?', "%3f")
944 .replace('#', "%23")
945 );
946 read(
947 &uri,
948 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_URI,
949 )
950 }
951 history => history,
952 }
953 .map(Some)
954}
955
956fn sizing_of(db: &Connection, since: u64) -> Result<sizing::History, StoreError> {
958 let found: u32 = db.pragma_query_value(None, "user_version", |row| row.get(0))?;
959 let supported = u32::try_from(MIGRATIONS.len()).unwrap_or(u32::MAX);
960 if found > supported {
961 return Err(StoreError::Newer { found, supported });
962 }
963 if found == 0 {
964 return Ok(sizing::History::default());
965 }
966 let mut added = match found {
968 1 => {
969 "NULL AS memory_cap, NULL AS cpu_cap, NULL AS admission, NULL AS oom_kills, NULL AS ooms, \
970 NULL AS memory_measure, "
971 }
972 2..=6 => "NULL AS admission, NULL AS oom_kills, NULL AS ooms, NULL AS memory_measure, ",
973 7 => "NULL AS ooms, NULL AS memory_measure, ",
974 _ => "",
975 }
976 .to_owned();
977 if found < 14 {
978 added.push_str("NULL AS local_ooms");
979 }
980 let added = added.trim_end_matches(", ");
981 if found < 13 {
982 db.execute_batch("CREATE TEMP VIEW jobs AS SELECT *, 0 AS host_waited FROM main.jobs")?;
983 }
984 if !added.is_empty() {
985 db.execute_batch(&format!(
986 "CREATE TEMP VIEW runners AS SELECT *, {added} FROM main.runners"
987 ))?;
988 }
989 let mut history = read::sizing(db, since)?;
990 if found >= 14 {
992 history.memory_set = read::auto_sizing(db)?.memory;
993 history.changes = read::sizing_changes(db, since.saturating_sub(sizing::memory::HISTORY))?;
994 }
995 Ok(history)
996}
997
998fn configure(db: &Connection) -> rusqlite::Result<()> {
1000 db.pragma_update(None, "journal_mode", "wal")?;
1001 db.pragma_update(None, "synchronous", "normal")?;
1002 db.pragma_update(None, "foreign_keys", "on")?;
1003 db.busy_timeout(Duration::from_secs(5))
1004}
1005
1006fn migrate(db: &mut Connection) -> Result<(), StoreError> {
1008 let found: u32 = db.pragma_query_value(None, "user_version", |row| row.get(0))?;
1009 let supported = u32::try_from(MIGRATIONS.len()).unwrap_or(u32::MAX);
1010 if found > supported {
1011 return Err(StoreError::Newer { found, supported });
1012 }
1013 for (version, migration) in (1..).zip(MIGRATIONS).skip(found as usize) {
1014 let transaction = db.transaction()?;
1015 transaction.execute_batch(migration)?;
1016 transaction.pragma_update(None, "user_version", version)?;
1017 transaction.commit()?;
1018 }
1019 Ok(())
1020}
1021
1022fn logged<T>(what: &str, result: rusqlite::Result<T>) -> Option<T> {
1024 result
1025 .map_err(|error| warn!("job history: cannot {what}: {error}"))
1026 .ok()
1027}
1028
1029#[cfg(test)]
1030mod tests;