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 SCHEMA: &str = include_str!("store/schema.sql");
32
33const VERSION: u32 = 15;
36
37pub(crate) const LOG_BACKLOG: u64 = 8 << 20;
40
41const HOST_SAMPLE_BATCH: usize = 30;
44
45const EVENT_BUFFER: usize = 4096;
47
48#[derive(Debug, thiserror::Error)]
50pub enum StoreError {
51 #[error("{0}")]
53 Sqlite(#[from] rusqlite::Error),
54 #[error(
56 "the job history database is version {found}, newer than the {supported} this runnerd reads; \
57 run a newer runnerd or move the database aside"
58 )]
59 Newer {
60 found: u32,
62 supported: u32,
64 },
65 #[error(
67 "the job history database is version {found}, from a runnerd before 0.0.19; this runnerd reads version \
68 {supported} only: move the database aside"
69 )]
70 Older {
71 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 Visibility {
284 repository: String,
285 visibility: api::Visibility,
286 at: u64,
287 },
288 SizingIntent {
289 change: api::SizingChange,
290 done: oneshot::Sender<Option<i64>>,
292 },
293 SizingResolved {
294 id: i64,
295 resolution: Resolution,
296 done: oneshot::Sender<bool>,
298 },
299 Notify {
300 change: NotifyChange,
301 done: oneshot::Sender<bool>,
303 },
304 Flush(oneshot::Sender<()>),
305}
306
307#[derive(Debug)]
309enum NotifyChange {
310 Record {
312 target: String,
313 set: Vec<(String, u64)>,
314 remove: Vec<String>,
315 },
316 Keep(Vec<String>),
318}
319
320#[derive(Debug, Clone, Copy, PartialEq, Eq)]
322pub enum Resolution {
323 Done,
325 Failed,
327 Uncertain,
330}
331
332#[derive(Debug, Clone, Default, PartialEq, Eq)]
334pub struct AutoSizing {
335 pub last: BTreeMap<String, u64>,
337 pub pending: Vec<(i64, api::SizingChange)>,
339 pub cpus: Vec<sizing::CpusSet>,
342 pub memory: Vec<sizing::MemorySet>,
344}
345
346#[derive(Debug, Clone)]
348pub struct Store {
349 writes: mpsc::UnboundedSender<Write>,
350 reader: Arc<Mutex<Connection>>,
351 pruner: Arc<Mutex<Connection>>,
353 closed: Arc<AtomicBool>,
355 held: Arc<Mutex<Vec<api::HostSample>>>,
358 latest_host: Arc<Mutex<Option<api::HostSample>>>,
360 latest_host_memory: Arc<Mutex<Option<api::HostMemory>>>,
362 latest_samples: Arc<Mutex<BTreeMap<String, SampleRecord>>>,
364 events: broadcast::Sender<Arc<EventEnvelope>>,
365 backlog: Arc<Semaphore>,
366}
367
368impl Store {
369 pub fn open(path: &Path) -> Result<Store, StoreError> {
372 if let Some(dir) = path.parent().filter(|dir| !dir.as_os_str().is_empty()) {
373 std::fs::DirBuilder::new()
374 .recursive(true)
375 .mode(0o700)
376 .create(dir)?;
377 }
378 std::fs::OpenOptions::new()
380 .create(true)
381 .append(true)
382 .mode(0o600)
383 .open(path)?;
384 let mut db = Connection::open(path)?;
385 configure(&db)?;
386 create(&mut db)?;
387 let reader = Connection::open_with_flags(
388 path,
389 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
390 )?;
391 configure(&reader)?;
392 let pruner = Connection::open(path)?;
393 configure(&pruner)?;
394 let (events, _) = broadcast::channel(EVENT_BUFFER);
395 let (writes, receiver) = mpsc::unbounded_channel();
396 let thread_events = events.clone();
397 let held = Arc::new(Mutex::new(Vec::new()));
398 let thread_held = held.clone();
399 std::thread::Builder::new()
400 .name("runnerd-store".to_owned())
401 .spawn(move || write::run(db, receiver, &thread_events, &thread_held))?;
402 Ok(Store {
403 writes,
404 reader: Arc::new(Mutex::new(reader)),
405 pruner: Arc::new(Mutex::new(pruner)),
406 closed: Arc::new(AtomicBool::new(false)),
407 held,
408 latest_host: Arc::new(Mutex::new(None)),
409 latest_host_memory: Arc::new(Mutex::new(None)),
410 latest_samples: Arc::new(Mutex::new(BTreeMap::new())),
411 events,
412 backlog: Arc::new(Semaphore::new(
413 usize::try_from(LOG_BACKLOG).unwrap_or(usize::MAX),
414 )),
415 })
416 }
417
418 fn send(&self, write: Write) {
419 let _ = self.writes.send(write);
421 }
422
423 pub fn queued(&self, repository: &str, job: &QueuedJob) {
425 self.send(Write::Queued {
426 repository: repository.to_owned(),
427 job: job.clone(),
428 });
429 }
430
431 pub fn runner_started(&self, runner: RunnerRecord) {
433 self.send(Write::RunnerStarted(runner));
434 }
435
436 pub fn runner_removed(&self, runner: &str, at: u64) {
438 lock(&self.latest_samples).remove(runner);
439 self.send(Write::RunnerRemoved {
440 runner: runner.to_owned(),
441 at,
442 });
443 }
444
445 pub fn runners_gone(&self, running: Vec<String>, at: u64) {
448 lock(&self.latest_samples).retain(|runner, _| running.contains(runner));
449 self.send(Write::RunnersGone { running, at });
450 }
451
452 pub fn claimed(&self, job: u64, run_id: u64, runner: &str, at: u64) {
454 self.send(Write::Claimed {
455 job,
456 run_id,
457 runner: runner.to_owned(),
458 at,
459 });
460 }
461
462 pub fn details(&self, details: JobDetails) {
464 self.send(Write::Details { details });
465 }
466
467 pub fn elsewhere(&self, details: JobDetails) {
470 self.send(Write::Elsewhere { details });
471 }
472
473 pub fn lost(&self, job: u64, at: u64) {
475 self.send(Write::Lost { job, at });
476 }
477
478 pub async fn log(&self, job: u64, lines: Vec<String>, position: LogPosition, max_bytes: u64) {
481 let size = |line: &String| line.len() as u64 + 1;
482 let mut lines = lines.into_iter().map(|mut line| {
483 if size(&line) > LOG_BACKLOG {
485 let mut end = usize::try_from(LOG_BACKLOG / 2).unwrap_or(usize::MAX);
486 while !line.is_char_boundary(end) {
487 end -= 1;
488 }
489 line.truncate(end);
490 }
491 line
492 });
493 let mut next = lines.next();
494 loop {
495 let (mut part, mut bytes) = (Vec::new(), 0);
496 while let Some(line) = next.take_if(|line| bytes + size(line) <= LOG_BACKLOG) {
497 bytes += size(&line);
498 part.push(line);
499 next = lines.next();
500 }
501 let last = next.is_none();
502 let share = u32::try_from(bytes.max(1)).unwrap_or(u32::MAX);
503 let Ok(backlog) = self.backlog.clone().acquire_many_owned(share).await else {
505 return;
506 };
507 self.send(Write::Log {
508 job,
509 lines: part,
510 position: last.then_some(position),
511 max_bytes,
512 _backlog: backlog,
513 });
514 if last {
515 return;
516 }
517 }
518 }
519
520 pub fn samples(&self, samples: Vec<SampleRecord>) {
522 let mut latest = lock(&self.latest_samples);
523 for sample in &samples {
524 latest.insert(sample.runner.clone(), sample.clone());
525 }
526 drop(latest);
527 self.send(Write::Samples(samples));
528 }
529
530 pub fn host_sample(&self, sample: api::HostSample) {
533 *lock(&self.latest_host) = Some(sample.clone());
534 lock(&self.held).push(sample.clone());
535 self.send(Write::HostSample(sample));
536 }
537
538 pub fn latest_sample(&self, runner: &str) -> Option<SampleRecord> {
540 lock(&self.latest_samples).get(runner).cloned()
541 }
542
543 pub fn runner_admission(&self, runner: &str, admission: api::Admission) {
545 self.send(Write::RunnerAdmission {
546 runner: runner.to_owned(),
547 admission,
548 });
549 }
550
551 pub fn runner_oom_killed(&self, runner: &str) {
554 self.send(Write::RunnerOomKilled(runner.to_owned()));
555 }
556
557 pub fn build_cache(&self, runner: &str, counts: api::BuildCacheCounts) {
559 self.send(Write::BuildCache {
560 runner: runner.to_owned(),
561 counts,
562 });
563 }
564
565 pub fn host_memory(&self, memory: api::HostMemory) {
567 *lock(&self.latest_host_memory) = Some(memory);
568 }
569
570 pub fn latest_host_memory(&self) -> Option<api::HostMemory> {
572 lock(&self.latest_host_memory).clone()
573 }
574
575 pub fn host_waited(&self, jobs: Vec<u64>, ms: u64) {
578 self.send(Write::HostWaited { jobs, ms });
579 }
580
581 pub fn latest_host_sample(&self) -> Option<api::HostSample> {
583 lock(&self.latest_host).clone()
584 }
585
586 pub async fn mirror_bytes(&self, bytes: Vec<(String, u64)>, until: MirrorPosition) -> bool {
589 let (done, committed) = oneshot::channel();
590 self.send(Write::MirrorBytes { bytes, until, done });
591 committed.await.unwrap_or(false)
592 }
593
594 pub fn cache_sizes(&self, at: u64, sizes: BTreeMap<String, u64>) {
596 self.send(Write::CacheSizes { at, sizes });
597 }
598
599 pub fn event(&self, event: Event) {
601 self.send(Write::Event(Box::new(event)));
602 }
603
604 pub fn record_made(&self, record: api::Record) {
606 self.send(Write::RecordMade(record));
607 }
608
609 pub fn record_skipped(&self, job: u64, why: String) {
611 self.send(Write::RecordSkipped { job, why });
612 }
613
614 pub fn record_removed(&self, job: u64, at: u64, by: api::RecordRemoval) {
616 self.send(Write::RecordRemoved { job, at, by });
617 }
618
619 pub fn paused(&self, repositories: Vec<String>) -> impl Future<Output = bool> + use<> {
622 let (done, committed) = oneshot::channel();
623 self.send(Write::Paused { repositories, done });
624 async move { committed.await.unwrap_or(false) }
625 }
626
627 pub fn visibility(&self, repository: String, visibility: api::Visibility, at: u64) {
629 self.send(Write::Visibility {
630 repository,
631 visibility,
632 at,
633 });
634 }
635
636 pub fn sizing_intent(
639 &self,
640 change: api::SizingChange,
641 ) -> impl Future<Output = Option<i64>> + use<> {
642 let (done, committed) = oneshot::channel();
643 self.send(Write::SizingIntent { change, done });
644 async move { committed.await.ok().flatten() }
645 }
646
647 pub fn sizing_resolved(
650 &self,
651 id: i64,
652 resolution: Resolution,
653 ) -> impl Future<Output = bool> + use<> {
654 let (done, committed) = oneshot::channel();
655 self.send(Write::SizingResolved {
656 id,
657 resolution,
658 done,
659 });
660 async move { committed.await.unwrap_or(false) }
661 }
662
663 pub fn notify_record(
666 &self,
667 target: &str,
668 set: Vec<(String, u64)>,
669 remove: Vec<String>,
670 ) -> impl Future<Output = bool> + use<> {
671 let (done, committed) = oneshot::channel();
672 self.send(Write::Notify {
673 change: NotifyChange::Record {
674 target: target.to_owned(),
675 set,
676 remove,
677 },
678 done,
679 });
680 async move { committed.await.unwrap_or(false) }
681 }
682
683 pub fn notify_keep(&self, targets: Vec<String>) -> impl Future<Output = bool> + use<> {
685 let (done, committed) = oneshot::channel();
686 self.send(Write::Notify {
687 change: NotifyChange::Keep(targets),
688 done,
689 });
690 async move { committed.await.unwrap_or(false) }
691 }
692
693 pub fn prune(&self, now: u64, history: &History) -> tokio::task::JoinHandle<()> {
697 let pruner = self.pruner.clone();
698 let closed = self.closed.clone();
699 let history = history.clone();
700 tokio::task::spawn_blocking(move || {
701 let db = match pruner.try_lock() {
702 Ok(db) => db,
703 Err(TryLockError::Poisoned(poisoned)) => poisoned.into_inner(),
704 Err(TryLockError::WouldBlock) => return,
705 };
706 let db = prune::Db {
707 connection: &db,
708 stop: &closed,
709 };
710 if !db.stop.load(Ordering::Relaxed) {
711 prune::run(&db, now, &history);
712 }
713 })
714 }
715
716 pub fn close(&self) {
719 self.closed.store(true, Ordering::Relaxed);
720 }
721
722 pub async fn flush(&self) {
724 let (done, wait) = oneshot::channel();
725 self.send(Write::Flush(done));
726 let _ = wait.await;
727 }
728
729 pub fn subscribe(&self) -> broadcast::Receiver<Arc<EventEnvelope>> {
732 self.events.subscribe()
733 }
734
735 async fn read<T: Send + 'static>(
736 &self,
737 query: impl FnOnce(&Connection) -> rusqlite::Result<T> + Send + 'static,
738 ) -> Result<T, StoreError> {
739 let reader = self.reader.clone();
740 Ok(tokio::task::spawn_blocking(move || query(&lock(&reader))).await??)
742 }
743
744 pub async fn jobs(&self, query: api::JobQuery) -> Result<api::JobPage, StoreError> {
746 self.read(move |db| read::jobs(db, &query)).await
747 }
748
749 pub async fn job(&self, id: u64) -> Result<Option<api::Job>, StoreError> {
751 self.read(move |db| read::job(db, id)).await
752 }
753
754 pub async fn job_samples(
756 &self,
757 id: u64,
758 interval: Duration,
759 ) -> Result<Option<api::SampleSeries>, StoreError> {
760 self.read(move |db| read::job_samples(db, id, interval))
761 .await
762 }
763
764 pub async fn log_lines(
766 &self,
767 job: u64,
768 from: u64,
769 limit: u64,
770 ) -> Result<Vec<(u64, String)>, StoreError> {
771 self.read(move |db| read::log_lines(db, job, from, limit))
772 .await
773 }
774
775 pub async fn events_after(
777 &self,
778 after: u64,
779 limit: u64,
780 ) -> Result<Vec<EventEnvelope>, StoreError> {
781 self.read(move |db| read::events_after(db, after, limit))
782 .await
783 }
784
785 pub async fn sizing_changes(&self, since: u64) -> Result<Vec<api::SizingChange>, StoreError> {
787 self.read(move |db| read::sizing_changes(db, since)).await
788 }
789
790 pub async fn auto_sizing(&self) -> Result<AutoSizing, StoreError> {
792 self.read(read::auto_sizing).await
793 }
794
795 pub fn announced(&self) -> Result<Announced, StoreError> {
798 Ok(read::announced(&lock(&self.reader))?)
799 }
800
801 pub fn paused_repositories(&self) -> Result<Vec<String>, StoreError> {
803 Ok(read::paused(&lock(&self.reader))?)
804 }
805
806 pub async fn host_samples(&self, since: u64) -> Result<Vec<api::HostSample>, StoreError> {
808 let held = lock(&self.held).clone();
811 let mut samples = self.read(move |db| read::host_samples(db, since)).await?;
812 let stored = samples.last().map_or(0, |sample| sample.at);
813 samples.extend(
814 held.into_iter()
815 .filter(|sample| sample.at >= since && sample.at > stored),
816 );
817 Ok(samples)
818 }
819
820 pub async fn activity(&self, since: u64, now: u64) -> Result<api::Activity, StoreError> {
822 self.read(move |db| read::activity(db, since, now)).await
823 }
824
825 pub async fn stats(&self, query: api::StatsQuery, now: u64) -> Result<api::Stats, StoreError> {
827 self.read(move |db| read::stats(db, &query, now)).await
828 }
829
830 pub async fn sizing(&self, since: u64) -> Result<sizing::History, StoreError> {
832 self.read(move |db| read::sizing(db, since)).await
833 }
834
835 pub async fn pricing(&self, since: u64) -> Result<api::PricingHistory, StoreError> {
838 self.read(move |db| read::pricing(db, since)).await
839 }
840
841 pub async fn insights(&self, since: u64) -> Result<Vec<crate::insights::Run>, StoreError> {
843 self.read(move |db| read::insights(db, since)).await
844 }
845
846 pub async fn last_event(&self) -> Result<u64, StoreError> {
848 self.read(read::last_event).await
849 }
850
851 pub async fn notify_state(
853 &self,
854 ) -> Result<BTreeMap<String, BTreeMap<String, u64>>, StoreError> {
855 self.read(read::notify_state).await
856 }
857
858 pub async fn warm(&self, since: u64) -> Result<Vec<warm::Seen>, StoreError> {
860 self.read(move |db| read::warm(db, since)).await
861 }
862
863 pub async fn cache_sizes_now(&self) -> Result<Measured, StoreError> {
865 self.read(read::latest_cache_sizes).await
866 }
867
868 pub async fn build_cache_since(&self, since: u64) -> Result<api::BuildCacheCounts, StoreError> {
870 self.read(move |db| read::build_cache_since(db, since))
871 .await
872 }
873
874 pub async fn cache_history(&self, since: u64) -> Result<Vec<api::CacheSize>, StoreError> {
876 self.read(move |db| read::cache_history(db, since)).await
877 }
878
879 pub async fn open_jobs(&self) -> Result<Vec<OpenJob>, StoreError> {
881 self.read(read::open_jobs).await
882 }
883
884 pub async fn runner_job(&self, runner: &str) -> Result<Option<u64>, StoreError> {
886 let runner = runner.to_owned();
887 self.read(move |db| read::runner_job(db, &runner)).await
888 }
889
890 pub async fn runner_admitted(
892 &self,
893 runner: &str,
894 ) -> Result<Option<api::Admission>, StoreError> {
895 let runner = runner.to_owned();
896 self.read(move |db| read::runner_admission(db, &runner))
897 .await
898 }
899
900 pub async fn log_position(&self, job: u64) -> Result<LogPosition, StoreError> {
902 self.read(move |db| read::log_position(db, job)).await
903 }
904
905 pub async fn mirror_log_until(&self) -> Result<MirrorPosition, StoreError> {
907 self.read(read::mirror_log_until).await
908 }
909}
910
911pub fn sizing_at(path: &Path, since: u64) -> Result<Option<sizing::History>, StoreError> {
916 if !path.exists() {
917 return Ok(None);
918 }
919 let read = |path: &str, flags| -> Result<sizing::History, StoreError> {
920 let db = Connection::open_with_flags(path, flags)?;
921 db.busy_timeout(Duration::from_secs(5))?;
922 sizing_of(&db, since)
923 };
924 let name = path.to_string_lossy();
925 match read(&name, OpenFlags::SQLITE_OPEN_READ_ONLY) {
926 Err(StoreError::Sqlite(error))
927 if matches!(
928 error.sqlite_error_code(),
929 Some(rusqlite::ErrorCode::ReadOnly | rusqlite::ErrorCode::CannotOpen)
930 ) =>
931 {
932 let mut wal = path.as_os_str().to_owned();
934 wal.push("-wal");
935 let wal = PathBuf::from(wal);
936 if std::fs::metadata(&wal).is_ok_and(|wal| wal.len() > 0) {
937 return Err(StoreError::Unread(wal));
938 }
939 let uri = format!(
940 "file:{}?mode=ro&immutable=1",
941 name.replace('%', "%25")
942 .replace('?', "%3f")
943 .replace('#', "%23")
944 );
945 read(
946 &uri,
947 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_URI,
948 )
949 }
950 history => history,
951 }
952 .map(Some)
953}
954
955fn sizing_of(db: &Connection, since: u64) -> Result<sizing::History, StoreError> {
957 if version(db)? == 0 {
958 return Ok(sizing::History::default());
959 }
960 let mut history = read::sizing(db, since)?;
961 history.memory_set = read::auto_sizing(db)?.memory;
962 history.changes = read::sizing_changes(db, since.saturating_sub(sizing::memory::HISTORY))?;
963 Ok(history)
964}
965
966fn version(db: &Connection) -> Result<u32, StoreError> {
968 let found: u32 = db.pragma_query_value(None, "user_version", |row| row.get(0))?;
969 match found {
970 0 | VERSION => Ok(found),
971 found if found > VERSION => Err(StoreError::Newer {
972 found,
973 supported: VERSION,
974 }),
975 found => Err(StoreError::Older {
976 found,
977 supported: VERSION,
978 }),
979 }
980}
981
982fn configure(db: &Connection) -> rusqlite::Result<()> {
984 db.pragma_update(None, "journal_mode", "wal")?;
985 db.pragma_update(None, "synchronous", "normal")?;
986 db.pragma_update(None, "foreign_keys", "on")?;
987 db.busy_timeout(Duration::from_secs(5))
988}
989
990fn create(db: &mut Connection) -> Result<(), StoreError> {
992 if version(db)? == 0 {
993 let transaction = db.transaction()?;
994 transaction.execute_batch(SCHEMA)?;
995 transaction.pragma_update(None, "user_version", VERSION)?;
996 transaction.commit()?;
997 }
998 Ok(())
999}
1000
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;