1use std::collections::BTreeMap;
8use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt};
9use std::path::Path;
10use std::sync::{Arc, Mutex};
11use std::time::Duration;
12
13use rusqlite::{Connection, OpenFlags};
14use tokio::sync::{OwnedSemaphorePermit, Semaphore, broadcast, mpsc, oneshot};
15use tracing::warn;
16
17use crate::api::{self, Event, EventEnvelope};
18use crate::config::History;
19use crate::github::{JobDetails, QueuedJob};
20use crate::sync::lock;
21
22mod read;
23mod write;
24
25const MIGRATIONS: &[&str] = &[
27 include_str!("store/migrations/0001_history.sql"),
28 include_str!("store/migrations/0002_runner_caps.sql"),
29 include_str!("store/migrations/0003_paused.sql"),
30];
31
32pub(crate) const LOG_BACKLOG: u64 = 8 << 20;
35
36const EVENT_BUFFER: usize = 4096;
38
39#[derive(Debug, thiserror::Error)]
41pub enum StoreError {
42 #[error("{0}")]
44 Sqlite(#[from] rusqlite::Error),
45 #[error(
47 "the job history database is version {found}, newer than the {supported} this runnerd reads; \
48 run a newer runnerd or move the database aside"
49 )]
50 Newer {
51 found: u32,
53 supported: u32,
55 },
56 #[error("{0}")]
58 Io(#[from] std::io::Error),
59 #[error("a database read stopped: {0}")]
61 Join(#[from] tokio::task::JoinError),
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct RunnerRecord {
67 pub name: String,
69 pub repository: String,
71 pub profile: String,
73 pub slot: usize,
75 pub container: String,
77 pub started_at: u64,
79 pub memory_cap: Option<u64>,
81 pub cpu_cap: Option<u64>,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct SampleRecord {
88 pub runner: String,
90 pub at: u64,
92 pub cpu: u64,
94 pub cpu_usage: u64,
96 pub memory: u64,
98 pub network_received: u64,
100 pub network_sent: u64,
102}
103
104#[derive(Debug, Clone, Default, PartialEq, Eq)]
106pub struct Measured {
107 pub at: Option<u64>,
109 pub bytes: BTreeMap<String, u64>,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
115pub struct OpenJob {
116 pub id: u64,
118 pub repository: String,
120 pub state: api::JobState,
122 pub runner: Option<String>,
124 pub runner_removed_at: Option<u64>,
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub struct LogPosition {
131 pub page: u64,
133 pub offset: u64,
135}
136
137#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
141pub struct MirrorPosition {
142 pub at: u64,
144 pub seen: u64,
146}
147
148#[derive(Debug)]
150enum Write {
151 Queued {
152 repository: String,
153 job: QueuedJob,
154 },
155 RunnerStarted(RunnerRecord),
156 RunnerRemoved {
157 runner: String,
158 at: u64,
159 },
160 Claimed {
161 job: u64,
162 run_id: u64,
163 runner: String,
164 at: u64,
165 },
166 Details {
167 details: JobDetails,
168 },
169 Elsewhere {
170 details: JobDetails,
171 },
172 Lost {
173 job: u64,
174 at: u64,
175 },
176 Log {
177 job: u64,
178 lines: Vec<String>,
179 position: Option<LogPosition>,
181 max_bytes: u64,
182 _backlog: OwnedSemaphorePermit,
184 },
185 Samples(Vec<SampleRecord>),
186 HostSample(api::HostSample),
187 MirrorBytes {
188 bytes: Vec<(String, u64)>,
189 until: MirrorPosition,
190 done: oneshot::Sender<bool>,
192 },
193 CacheSizes {
194 at: u64,
195 sizes: BTreeMap<String, u64>,
196 },
197 Event(Event),
198 Paused {
199 repositories: Vec<String>,
200 done: oneshot::Sender<bool>,
202 },
203 Prune {
204 now: u64,
205 history: History,
206 },
207 Flush(oneshot::Sender<()>),
208}
209
210#[derive(Debug, Clone)]
212pub struct Store {
213 writes: mpsc::UnboundedSender<Write>,
214 reader: Arc<Mutex<Connection>>,
215 events: broadcast::Sender<Arc<EventEnvelope>>,
216 backlog: Arc<Semaphore>,
217}
218
219impl Store {
220 pub fn open(path: &Path) -> Result<Store, StoreError> {
223 if let Some(dir) = path.parent().filter(|dir| !dir.as_os_str().is_empty()) {
224 std::fs::DirBuilder::new()
225 .recursive(true)
226 .mode(0o700)
227 .create(dir)?;
228 }
229 std::fs::OpenOptions::new()
231 .create(true)
232 .append(true)
233 .mode(0o600)
234 .open(path)?;
235 let mut db = Connection::open(path)?;
236 configure(&db)?;
237 migrate(&mut db)?;
238 let reader = Connection::open_with_flags(
239 path,
240 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
241 )?;
242 configure(&reader)?;
243 let (events, _) = broadcast::channel(EVENT_BUFFER);
244 let (writes, receiver) = mpsc::unbounded_channel();
245 let thread_events = events.clone();
246 std::thread::Builder::new()
247 .name("runnerd-store".to_owned())
248 .spawn(move || write::run(db, receiver, &thread_events))?;
249 Ok(Store {
250 writes,
251 reader: Arc::new(Mutex::new(reader)),
252 events,
253 backlog: Arc::new(Semaphore::new(
254 usize::try_from(LOG_BACKLOG).unwrap_or(usize::MAX),
255 )),
256 })
257 }
258
259 fn send(&self, write: Write) {
260 let _ = self.writes.send(write);
262 }
263
264 pub fn queued(&self, repository: &str, job: &QueuedJob) {
266 self.send(Write::Queued {
267 repository: repository.to_owned(),
268 job: job.clone(),
269 });
270 }
271
272 pub fn runner_started(&self, runner: RunnerRecord) {
274 self.send(Write::RunnerStarted(runner));
275 }
276
277 pub fn runner_removed(&self, runner: &str, at: u64) {
279 self.send(Write::RunnerRemoved {
280 runner: runner.to_owned(),
281 at,
282 });
283 }
284
285 pub fn claimed(&self, job: u64, run_id: u64, runner: &str, at: u64) {
287 self.send(Write::Claimed {
288 job,
289 run_id,
290 runner: runner.to_owned(),
291 at,
292 });
293 }
294
295 pub fn details(&self, details: JobDetails) {
297 self.send(Write::Details { details });
298 }
299
300 pub fn elsewhere(&self, details: JobDetails) {
302 self.send(Write::Elsewhere { details });
303 }
304
305 pub fn lost(&self, job: u64, at: u64) {
307 self.send(Write::Lost { job, at });
308 }
309
310 pub async fn log(&self, job: u64, lines: Vec<String>, position: LogPosition, max_bytes: u64) {
313 let size = |line: &String| line.len() as u64 + 1;
314 let mut lines = lines.into_iter().map(|mut line| {
315 if size(&line) > LOG_BACKLOG {
317 let mut end = usize::try_from(LOG_BACKLOG / 2).unwrap_or(usize::MAX);
318 while !line.is_char_boundary(end) {
319 end -= 1;
320 }
321 line.truncate(end);
322 }
323 line
324 });
325 let mut next = lines.next();
326 loop {
327 let (mut part, mut bytes) = (Vec::new(), 0);
328 while let Some(line) = next.take_if(|line| bytes + size(line) <= LOG_BACKLOG) {
329 bytes += size(&line);
330 part.push(line);
331 next = lines.next();
332 }
333 let last = next.is_none();
334 let share = u32::try_from(bytes.max(1)).unwrap_or(u32::MAX);
335 let Ok(backlog) = self.backlog.clone().acquire_many_owned(share).await else {
337 return;
338 };
339 self.send(Write::Log {
340 job,
341 lines: part,
342 position: last.then_some(position),
343 max_bytes,
344 _backlog: backlog,
345 });
346 if last {
347 return;
348 }
349 }
350 }
351
352 pub fn samples(&self, samples: Vec<SampleRecord>) {
354 self.send(Write::Samples(samples));
355 }
356
357 pub fn host_sample(&self, sample: api::HostSample) {
359 self.send(Write::HostSample(sample));
360 }
361
362 pub async fn mirror_bytes(&self, bytes: Vec<(String, u64)>, until: MirrorPosition) -> bool {
365 let (done, committed) = oneshot::channel();
366 self.send(Write::MirrorBytes { bytes, until, done });
367 committed.await.unwrap_or(false)
368 }
369
370 pub fn cache_sizes(&self, at: u64, sizes: BTreeMap<String, u64>) {
372 self.send(Write::CacheSizes { at, sizes });
373 }
374
375 pub fn event(&self, event: Event) {
377 self.send(Write::Event(event));
378 }
379
380 pub fn paused(&self, repositories: Vec<String>) -> impl Future<Output = bool> + use<> {
383 let (done, committed) = oneshot::channel();
384 self.send(Write::Paused { repositories, done });
385 async move { committed.await.unwrap_or(false) }
386 }
387
388 pub fn prune(&self, now: u64, history: &History) {
390 self.send(Write::Prune {
391 now,
392 history: history.clone(),
393 });
394 }
395
396 pub async fn flush(&self) {
398 let (done, wait) = oneshot::channel();
399 self.send(Write::Flush(done));
400 let _ = wait.await;
401 }
402
403 pub fn subscribe(&self) -> broadcast::Receiver<Arc<EventEnvelope>> {
406 self.events.subscribe()
407 }
408
409 async fn read<T: Send + 'static>(
410 &self,
411 query: impl FnOnce(&Connection) -> rusqlite::Result<T> + Send + 'static,
412 ) -> Result<T, StoreError> {
413 let reader = self.reader.clone();
414 Ok(tokio::task::spawn_blocking(move || query(&lock(&reader))).await??)
416 }
417
418 pub async fn jobs(&self, query: api::JobQuery) -> Result<api::JobPage, StoreError> {
420 self.read(move |db| read::jobs(db, &query)).await
421 }
422
423 pub async fn job(&self, id: u64) -> Result<Option<api::Job>, StoreError> {
425 self.read(move |db| read::job(db, id)).await
426 }
427
428 pub async fn job_samples(
430 &self,
431 id: u64,
432 interval: Duration,
433 ) -> Result<Option<api::SampleSeries>, StoreError> {
434 self.read(move |db| read::job_samples(db, id, interval))
435 .await
436 }
437
438 pub async fn log_lines(
440 &self,
441 job: u64,
442 from: u64,
443 limit: u64,
444 ) -> Result<Vec<(u64, String)>, StoreError> {
445 self.read(move |db| read::log_lines(db, job, from, limit))
446 .await
447 }
448
449 pub async fn events_after(
451 &self,
452 after: u64,
453 limit: u64,
454 ) -> Result<Vec<EventEnvelope>, StoreError> {
455 self.read(move |db| read::events_after(db, after, limit))
456 .await
457 }
458
459 pub fn announced_repositories(&self) -> Result<Vec<api::RepositoryStatus>, StoreError> {
462 Ok(read::announced_repositories(&lock(&self.reader))?)
463 }
464
465 pub fn paused_repositories(&self) -> Result<Vec<String>, StoreError> {
467 Ok(read::paused(&lock(&self.reader))?)
468 }
469
470 pub async fn host_samples(&self, since: u64) -> Result<Vec<api::HostSample>, StoreError> {
472 self.read(move |db| read::host_samples(db, since)).await
473 }
474
475 pub async fn stats(&self, query: api::StatsQuery, now: u64) -> Result<api::Stats, StoreError> {
477 self.read(move |db| read::stats(db, &query, now)).await
478 }
479
480 pub async fn cache_sizes_now(&self) -> Result<Measured, StoreError> {
482 self.read(read::latest_cache_sizes).await
483 }
484
485 pub async fn cache_history(&self, since: u64) -> Result<Vec<api::CacheSize>, StoreError> {
487 self.read(move |db| read::cache_history(db, since)).await
488 }
489
490 pub async fn open_jobs(&self) -> Result<Vec<OpenJob>, StoreError> {
492 self.read(read::open_jobs).await
493 }
494
495 pub async fn runner_job(&self, runner: &str) -> Result<Option<u64>, StoreError> {
497 let runner = runner.to_owned();
498 self.read(move |db| read::runner_job(db, &runner)).await
499 }
500
501 pub async fn log_position(&self, job: u64) -> Result<LogPosition, StoreError> {
503 self.read(move |db| read::log_position(db, job)).await
504 }
505
506 pub async fn mirror_log_until(&self) -> Result<MirrorPosition, StoreError> {
508 self.read(read::mirror_log_until).await
509 }
510}
511
512fn configure(db: &Connection) -> rusqlite::Result<()> {
514 db.pragma_update(None, "journal_mode", "wal")?;
515 db.pragma_update(None, "synchronous", "normal")?;
516 db.pragma_update(None, "foreign_keys", "on")?;
517 db.busy_timeout(Duration::from_secs(5))
518}
519
520fn migrate(db: &mut Connection) -> Result<(), StoreError> {
522 let found: u32 = db.pragma_query_value(None, "user_version", |row| row.get(0))?;
523 let supported = u32::try_from(MIGRATIONS.len()).unwrap_or(u32::MAX);
524 if found > supported {
525 return Err(StoreError::Newer { found, supported });
526 }
527 for (version, migration) in (1..).zip(MIGRATIONS).skip(found as usize) {
528 let transaction = db.transaction()?;
529 transaction.execute_batch(migration)?;
530 transaction.pragma_update(None, "user_version", version)?;
531 transaction.commit()?;
532 }
533 Ok(())
534}
535
536fn logged<T>(what: &str, result: rusqlite::Result<T>) -> Option<T> {
538 result
539 .map_err(|error| warn!("job history: cannot {what}: {error}"))
540 .ok()
541}
542
543#[cfg(test)]
544mod tests;