1use crate::types::{
2 Fingerprint, RequestId, RequestStatus, ReuseKey, RunId, RunStatus, SessionId, Timestamp,
3};
4use std::{
5 collections::{BTreeMap, BTreeSet},
6 path::{Path, PathBuf},
7 time::Duration,
8};
9
10use rusqlite::{OptionalExtension, params};
11use serde::{Deserialize, Serialize};
12
13use tokio_rusqlite::Connection;
14
15use super::{ExecutionOptions, Origin, Producer, Provenance, STATE_SCHEMA_VERSION};
16use crate::{
17 config::ProfileKind,
18 workspace::{canonical_target, outside_workspace, prepare_directory},
19};
20
21pub const DATABASE: &str = "state.sqlite";
22const INITIALIZATION_RETRY_INTERVAL: Duration = Duration::from_millis(20);
25
26const SCHEMA: &str =
45 "CREATE TABLE runs(id TEXT PRIMARY KEY, repo TEXT NOT NULL, status TEXT NOT NULL, data TEXT NOT NULL, request_key TEXT, revision INTEGER NOT NULL DEFAULT 0);
46 CREATE UNIQUE INDEX run_request_key ON runs(repo,request_key) WHERE request_key IS NOT NULL;
47 CREATE TABLE run_definitions(run_id TEXT PRIMARY KEY REFERENCES runs(id), data TEXT NOT NULL);
48 CREATE TABLE requests(id TEXT PRIMARY KEY, run_id TEXT NOT NULL REFERENCES runs(id), eval_id TEXT NOT NULL, ordinal INTEGER NOT NULL, execution_id TEXT REFERENCES executions(id), status TEXT NOT NULL, claimed_by TEXT, claimed_at TEXT, data TEXT NOT NULL, revision INTEGER NOT NULL DEFAULT 0, UNIQUE(run_id, eval_id), UNIQUE(run_id, ordinal));
49 CREATE TABLE executions(id TEXT PRIMARY KEY, key TEXT, eval_def_hash TEXT NOT NULL, status TEXT NOT NULL, owner_pid INTEGER NOT NULL, owner_start_time INTEGER NOT NULL, backend TEXT, profile_identity TEXT, completed_at TEXT, bytes INTEGER, last_used TEXT, data TEXT NOT NULL, revision INTEGER NOT NULL DEFAULT 0);
50 CREATE TABLE state_meta(name TEXT PRIMARY KEY, value TEXT NOT NULL);
51 CREATE INDEX active_request_execution ON requests(execution_id) WHERE status IN ('QUEUED','RUNNING','WAITING_HUMAN');
52 CREATE UNIQUE INDEX active_key ON executions(key,profile_identity) WHERE key IS NOT NULL AND status IN ('RUNNING','WAITING_HUMAN');
53 CREATE INDEX key_history ON executions(key, completed_at);
54 CREATE INDEX definition_history ON executions(eval_def_hash, completed_at);
55 CREATE INDEX running_backend ON executions(backend) WHERE status='RUNNING';";
56
57pub(super) const REVISIONS: &str =
64 "CREATE TRIGGER run_revision AFTER UPDATE ON runs FOR EACH ROW WHEN NEW.revision=OLD.revision BEGIN UPDATE runs SET revision=OLD.revision+1 WHERE rowid=NEW.rowid; END;
65 CREATE TRIGGER request_revision AFTER UPDATE ON requests FOR EACH ROW WHEN NEW.revision=OLD.revision BEGIN UPDATE requests SET revision=OLD.revision+1 WHERE rowid=NEW.rowid; END;
66 CREATE TRIGGER execution_revision AFTER UPDATE ON executions FOR EACH ROW WHEN NEW.revision=OLD.revision BEGIN UPDATE executions SET revision=OLD.revision+1 WHERE rowid=NEW.rowid; END;
67 CREATE INDEX request_status ON requests(status,run_id);
68 CREATE INDEX request_marks ON requests(run_id,ordinal,id,execution_id,revision);
69 CREATE INDEX request_execution ON requests(execution_id,run_id);
70 CREATE INDEX request_claim ON requests(execution_id,id,revision) WHERE claimed_by IS NOT NULL;
71 CREATE INDEX execution_marks ON executions(id,revision);";
72
73pub const EARLIER_STATE: &str = "This state was written by an earlier artifactize. Start a new state (set ARTIFACTIZE_STATE_HOME or move the old one away). Only schema 6 and later supported versions can be upgraded.";
75
76pub fn schema_error(version: u32) -> String {
78 match version {
79 0 => "Unsupported state schema version: 0.".into(),
80 version if version < STATE_SCHEMA_VERSION => EARLIER_STATE.into(),
81 version => {
82 format!("Unsupported state schema version: {version}; a newer artifactize wrote it.")
83 }
84 }
85}
86
87#[derive(Debug, thiserror::Error)]
88pub enum Error {
89 #[error(transparent)]
90 Sql(#[from] rusqlite::Error),
91 #[error(transparent)]
92 Json(#[from] serde_json::Error),
93 #[error("{0}")]
94 Invalid(String),
95 #[error("{0}")]
97 Unreadable(super::Unreadable),
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
101#[serde(try_from = "RunWire", into = "RunWire")]
102pub struct Run {
103 pub id: RunId,
104 pub repo_path: PathBuf,
105 pub repository: crate::repository::Identity,
107 pub state_dir: PathBuf,
108 pub state: super::RunState,
109 pub modes: crate::runs::RunModes,
110 pub control: Option<crate::runs::Control>,
111 pub request_key: Option<crate::types::RequestKey>,
112 pub comment: Option<crate::types::RunComment>,
114 pub created_at: Timestamp,
115 pub selection: crate::project::selection::Selection,
116 pub profile: Option<crate::project::selection::ProfileSelection>,
117 pub definitions: super::definitions::Definitions,
118 pub recursive: bool,
119 pub force: bool,
120 pub jobs: Option<usize>,
121 pub fingerprint_jobs: Option<usize>,
123 pub max_executions: Option<u64>,
124 pub executions_started: u64,
125 pub wait_timeout_ms: Option<Duration>,
126 pub wait_timed_out: bool,
127 pub reuse_only: BTreeSet<ProfileKind>,
129 pub validation: super::Validation,
130 pub stopped_backends: Vec<StoppedBackend>,
132 pub evidence: BTreeMap<crate::types::EvalId, crate::types::RequestStatus>,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
138#[serde(rename_all = "camelCase")]
139pub(crate) struct RunWire {
140 pub id: RunId,
141 #[serde(with = "crate::platform::path_serde")]
142 pub repo_path: PathBuf,
143 #[serde(flatten)]
145 pub repository: crate::repository::Identity,
146 #[serde(with = "crate::platform::path_serde")]
147 pub state_dir: PathBuf,
148 #[serde(default, skip_serializing_if = "crate::runs::RunModes::is_default")]
149 pub modes: crate::runs::RunModes,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub control: Option<crate::runs::Control>,
152 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub request_key: Option<crate::types::RequestKey>,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub comment: Option<crate::types::RunComment>,
156 pub status: RunStatus,
157 pub created_at: Timestamp,
158 pub completed_at: Option<Timestamp>,
159 pub selection: crate::project::selection::Selection,
160 #[serde(default)]
161 pub profile: Option<crate::project::selection::ProfileSelection>,
162 #[serde(default)]
163 pub definitions: super::definitions::Definitions,
164 #[serde(default)]
165 pub recursive: bool,
166 #[serde(default)]
167 pub force: bool,
168 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub jobs: Option<usize>,
170 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub fingerprint_jobs: Option<usize>,
173 #[serde(default)]
174 pub max_executions: Option<u64>,
175 #[serde(default)]
176 pub executions_started: u64,
177 #[serde(default, with = "super::wait_timeout")]
178 pub wait_timeout_ms: Option<Duration>,
179 #[serde(default)]
180 pub wait_timed_out: bool,
181 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
183 pub reuse_only: BTreeSet<ProfileKind>,
184 pub validation: super::Validation,
185 pub error: Option<String>,
186 #[serde(default, skip_serializing_if = "Vec::is_empty")]
188 pub stopped_backends: Vec<StoppedBackend>,
189 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
192 pub evidence: BTreeMap<crate::types::EvalId, crate::types::RequestStatus>,
193}
194
195impl TryFrom<RunWire> for Run {
196 type Error = String;
197 fn try_from(wire: RunWire) -> Result<Self, Self::Error> {
198 let state = super::RunState::from_parts(wire.status, wire.completed_at, wire.error)?;
199 Ok(Self {
200 state,
201 modes: wire.modes,
202 control: wire.control,
203 request_key: wire.request_key,
204 comment: wire.comment,
205 id: wire.id,
206 repo_path: wire.repo_path,
207 repository: wire.repository,
208 state_dir: wire.state_dir,
209 created_at: wire.created_at,
210 selection: wire.selection,
211 profile: wire.profile,
212 definitions: wire.definitions,
213 recursive: wire.recursive,
214 force: wire.force,
215 jobs: wire.jobs,
216 fingerprint_jobs: wire.fingerprint_jobs,
217 max_executions: wire.max_executions,
218 executions_started: wire.executions_started,
219 wait_timeout_ms: wire.wait_timeout_ms,
220 wait_timed_out: wire.wait_timed_out,
221 reuse_only: wire.reuse_only,
222 validation: wire.validation,
223 stopped_backends: wire.stopped_backends,
224 evidence: wire.evidence,
225 })
226 }
227}
228impl From<Run> for RunWire {
229 fn from(record: Run) -> Self {
230 Self {
231 modes: record.modes.clone(),
232 control: record.control.clone(),
233 request_key: record.request_key.clone(),
234 comment: record.comment.clone(),
235 status: record.status(),
236 completed_at: record.completed_at(),
237 error: record.error().map(str::to_owned),
238 id: record.id,
239 repo_path: record.repo_path,
240 repository: record.repository,
241 state_dir: record.state_dir,
242 created_at: record.created_at,
243 selection: record.selection,
244 profile: record.profile,
245 definitions: record.definitions,
246 recursive: record.recursive,
247 force: record.force,
248 jobs: record.jobs,
249 fingerprint_jobs: record.fingerprint_jobs,
250 max_executions: record.max_executions,
251 executions_started: record.executions_started,
252 wait_timeout_ms: record.wait_timeout_ms,
253 wait_timed_out: record.wait_timed_out,
254 reuse_only: record.reuse_only,
255 validation: record.validation,
256 stopped_backends: record.stopped_backends,
257 evidence: record.evidence,
258 }
259 }
260}
261impl Run {
262 pub fn status(&self) -> RunStatus {
263 self.state.status()
264 }
265 pub fn completed_at(&self) -> Option<Timestamp> {
266 self.state.completed_at()
267 }
268 pub fn error(&self) -> Option<&str> {
269 self.state.error()
270 }
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize)]
276#[serde(rename_all = "camelCase")]
277pub struct StoppedBackend {
278 pub backend: crate::config::Backend,
279 pub error_code: crate::types::BackendStopCode,
280 pub eval_id: crate::types::EvalId,
281 pub request_id: RequestId,
282 pub error: String,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
286#[serde(try_from = "RequestWire", into = "RequestWire")]
287pub struct Request {
288 pub state: super::RequestState,
289 pub id: RequestId,
290 pub run_id: RunId,
291 pub eval_id: crate::types::EvalId,
292 pub target: crate::types::ArtifactName,
293 pub title: String,
294 pub profile: crate::config::StoredProfile,
295 pub requested_profile: crate::config::StoredProfile,
296 pub eval_def_hash: crate::types::DefinitionHash,
297 pub options: ExecutionOptions,
299 pub execution_id: Option<crate::types::ExecutionId>,
300 pub provenance: Option<Provenance>,
301 pub usage: Option<Vec<crate::llm::Attempt>>,
303 pub reused_usage: Option<Vec<crate::llm::Attempt>>,
305 pub joined: bool,
308 pub producer: Option<Producer>,
310 pub reviewer: Option<crate::types::ReviewerId>,
312 pub origin: Option<Origin>,
314 pub session_id: Option<SessionId>,
318 pub session: Option<crate::agent::session::SessionRef>,
321 pub human_definition: Option<super::HumanDefinition>,
322 pub payload: crate::config::StoredPayload,
323 pub references: BTreeMap<crate::config::ReferenceName, crate::types::ArtifactName>,
324 pub deps: Vec<crate::types::ArtifactName>,
325 pub force: bool,
326 pub fingerprint: Option<Fingerprint>,
328 pub key: Option<ReuseKey>,
330 pub fingerprints: std::collections::BTreeMap<crate::types::ArtifactName, Fingerprint>,
332 pub created_at: Timestamp,
333 pub started_at: Option<Timestamp>,
334 pub cwd: PathBuf,
335 pub run_dir: Option<PathBuf>,
336 pub argv: Option<Vec<String>>,
337 pub child: Option<super::ChildIdentity>,
338 pub blocked_reason: Option<String>,
339 pub blocked_by: Vec<super::Blocker>,
341 pub queue: Option<QueueReason>,
343}
344
345#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
347#[serde(tag = "kind", rename_all = "camelCase")]
348pub enum QueueReason {
349 Slot { backend: crate::config::Backend },
351}
352
353#[derive(Debug, Clone, Serialize, Deserialize)]
354#[serde(rename_all = "camelCase")]
355pub(crate) struct RequestWire {
356 pub id: RequestId,
357 pub run_id: RunId,
358 pub eval_id: crate::types::EvalId,
359 pub target: crate::types::ArtifactName,
360 pub title: String,
361 pub profile: crate::config::StoredProfile,
362 pub requested_profile: crate::config::StoredProfile,
363 pub eval_def_hash: crate::types::DefinitionHash,
364 #[serde(default)]
366 pub options: ExecutionOptions,
367 pub execution_id: Option<crate::types::ExecutionId>,
368 pub provenance: Option<Provenance>,
369 pub usage: Option<Vec<crate::llm::Attempt>>,
371 #[serde(default, skip_serializing_if = "Option::is_none")]
373 pub reused_usage: Option<Vec<crate::llm::Attempt>>,
374 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
377 pub joined: bool,
378 #[serde(default, skip_serializing_if = "Option::is_none")]
380 pub producer: Option<Producer>,
381 #[serde(default, skip_serializing_if = "Option::is_none")]
383 pub reviewer: Option<crate::types::ReviewerId>,
384 #[serde(default, skip_serializing_if = "Option::is_none")]
386 pub origin: Option<Origin>,
387 #[serde(default, skip_serializing_if = "Option::is_none")]
391 pub session_id: Option<SessionId>,
392 #[serde(default, skip_serializing_if = "Option::is_none")]
395 pub session: Option<crate::agent::session::SessionRef>,
396 #[serde(default, skip_serializing_if = "Option::is_none")]
397 pub human_definition: Option<super::HumanDefinition>,
398 pub payload: crate::config::StoredPayload,
399 pub references: BTreeMap<crate::config::ReferenceName, crate::types::ArtifactName>,
400 pub deps: Vec<crate::types::ArtifactName>,
401 #[serde(default)]
402 pub force: bool,
403 #[serde(default)]
405 pub fingerprint: Option<Fingerprint>,
406 #[serde(default)]
408 pub key: Option<ReuseKey>,
409 #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
411 pub fingerprints: std::collections::BTreeMap<crate::types::ArtifactName, Fingerprint>,
412 pub status: RequestStatus,
413 pub created_at: Timestamp,
414 pub started_at: Option<Timestamp>,
415 pub completed_at: Option<Timestamp>,
416 #[serde(with = "crate::platform::path_serde")]
417 pub cwd: PathBuf,
418 #[serde(default, with = "crate::platform::path_serde::option")]
419 pub run_dir: Option<PathBuf>,
420 pub argv: Option<Vec<String>>,
421 pub child: Option<super::ChildIdentity>,
422 pub result: Option<super::ExecutionResult>,
423 pub error: Option<String>,
424 pub error_code: Option<crate::types::FailureCode>,
425 pub blocked_reason: Option<String>,
426 #[serde(default, skip_serializing_if = "Vec::is_empty")]
428 pub blocked_by: Vec<super::Blocker>,
429 #[serde(default, skip_serializing_if = "Option::is_none")]
431 pub queue: Option<QueueReason>,
432}
433
434impl TryFrom<RequestWire> for Request {
435 type Error = String;
436 fn try_from(wire: RequestWire) -> Result<Self, Self::Error> {
437 let state = super::RequestState::from_parts(
438 wire.status,
439 wire.result,
440 wire.error,
441 wire.error_code,
442 wire.completed_at,
443 )?;
444 Ok(Self {
445 state,
446 id: wire.id,
447 run_id: wire.run_id,
448 eval_id: wire.eval_id,
449 target: wire.target,
450 title: wire.title,
451 profile: wire.profile,
452 requested_profile: wire.requested_profile,
453 eval_def_hash: wire.eval_def_hash,
454 options: wire.options,
455 execution_id: wire.execution_id,
456 provenance: wire.provenance,
457 usage: wire.usage,
458 reused_usage: wire.reused_usage,
459 joined: wire.joined,
460 producer: wire.producer,
461 reviewer: wire.reviewer,
462 origin: wire.origin,
463 session_id: wire.session_id,
464 session: wire.session,
465 human_definition: wire.human_definition,
466 payload: wire.payload,
467 references: wire.references,
468 deps: wire.deps,
469 force: wire.force,
470 fingerprint: wire.fingerprint,
471 key: wire.key,
472 fingerprints: wire.fingerprints,
473 created_at: wire.created_at,
474 started_at: wire.started_at,
475 cwd: wire.cwd,
476 run_dir: wire.run_dir,
477 argv: wire.argv,
478 child: wire.child,
479 blocked_reason: wire.blocked_reason,
480 blocked_by: wire.blocked_by,
481 queue: wire.queue,
482 })
483 }
484}
485impl From<Request> for RequestWire {
486 fn from(record: Request) -> Self {
487 Self {
488 status: record.status(),
489 result: record.result().cloned(),
490 error: record.error().map(str::to_owned),
491 error_code: record.error_code(),
492 completed_at: record.completed_at(),
493 id: record.id,
494 run_id: record.run_id,
495 eval_id: record.eval_id,
496 target: record.target,
497 title: record.title,
498 profile: record.profile,
499 requested_profile: record.requested_profile,
500 eval_def_hash: record.eval_def_hash,
501 options: record.options,
502 execution_id: record.execution_id,
503 provenance: record.provenance,
504 usage: record.usage,
505 reused_usage: record.reused_usage,
506 joined: record.joined,
507 producer: record.producer,
508 reviewer: record.reviewer,
509 origin: record.origin,
510 session_id: record.session_id,
511 session: record.session,
512 human_definition: record.human_definition,
513 payload: record.payload,
514 references: record.references,
515 deps: record.deps,
516 force: record.force,
517 fingerprint: record.fingerprint,
518 key: record.key,
519 fingerprints: record.fingerprints,
520 created_at: record.created_at,
521 started_at: record.started_at,
522 cwd: record.cwd,
523 run_dir: record.run_dir,
524 argv: record.argv,
525 child: record.child,
526 blocked_reason: record.blocked_reason,
527 blocked_by: record.blocked_by,
528 queue: record.queue,
529 }
530 }
531}
532impl Request {
533 pub fn status(&self) -> crate::types::RequestStatus {
534 self.state.status()
535 }
536 pub fn result(&self) -> Option<&super::ExecutionResult> {
537 self.state.result()
538 }
539 pub fn error(&self) -> Option<&str> {
540 self.state.error()
541 }
542 pub fn error_code(&self) -> Option<crate::types::FailureCode> {
543 self.state.error_code()
544 }
545 pub fn completed_at(&self) -> Option<crate::types::Timestamp> {
546 self.state.completed_at()
547 }
548}
549
550#[derive(Debug, Serialize, Deserialize)]
551pub struct RunView {
552 #[serde(flatten)]
553 pub run: Run,
554 pub requests: Vec<Request>,
555 #[serde(default, skip_serializing_if = "Vec::is_empty")]
556 pub unreadable: Vec<super::Unreadable>,
557}
558
559#[derive(Clone)]
560pub struct Receipts {
561 pub(super) connection: NotifyingConnection,
562}
563
564#[cfg(any(test, feature = "test-support"))]
566pub struct OwnedReceipts(Receipts);
567
568#[cfg(any(test, feature = "test-support"))]
569impl OwnedReceipts {
570 pub async fn open(state: &Path, repo: &Path) -> Result<Self, String> {
571 Receipts::open(state, repo).await.map(Self)
572 }
573}
574
575#[cfg(any(test, feature = "test-support"))]
576impl std::ops::Deref for OwnedReceipts {
577 type Target = Receipts;
578 fn deref(&self) -> &Self::Target {
579 &self.0
580 }
581}
582
583#[cfg(any(test, feature = "test-support"))]
584impl Drop for OwnedReceipts {
585 fn drop(&mut self) {
586 let connection = self.0.connection.inner.clone();
587 let closed = std::thread::spawn(move || {
590 tokio::runtime::Builder::new_current_thread()
591 .build()
592 .expect("create the test writer's teardown runtime")
593 .block_on(connection.close())
594 })
595 .join();
596 if !std::thread::panicking() {
597 closed
598 .expect("join the test writer's teardown")
599 .expect("close the test writer's SQLite connection");
600 }
601 }
602}
603
604#[cfg(test)]
605mod ownership_tests {
606 use super::*;
607
608 #[tokio::test]
609 async fn failed_writer_initialization_releases_sqlite_before_returning_the_schema_error() {
610 let root = crate::test_os::tempdir();
611 let state = root.path().join("state");
612 let repo = root.path().join("repo");
613 std::fs::create_dir(&state).unwrap();
614 std::fs::create_dir(&repo).unwrap();
615 let database = state.join(DATABASE);
616 let connection = rusqlite::Connection::open(&database).unwrap();
617 connection
618 .execute_batch("CREATE TABLE unrelated(value TEXT)")
619 .unwrap();
620 drop(connection);
621 let error = match OwnedReceipts::open(&state, &repo).await {
622 Ok(_) => panic!("an unversioned nonempty database is not our schema"),
623 Err(error) => error,
624 };
625 assert!(error.contains(&schema_error(0)), "{error}");
626 assert!(crate::test_os::database_released(&database).unwrap());
627 root.close().unwrap();
628 }
629
630 #[tokio::test]
631 async fn explicit_writer_close_releases_sqlite_before_return_and_closes_clones() {
632 let root = crate::test_os::tempdir();
633 let state = root.path().join("state");
634 let repo = root.path().join("repo");
635 std::fs::create_dir(&repo).unwrap();
636 let writer = OwnedReceipts::open(&state, &repo).await.unwrap();
637 let retained = Receipts::clone(&writer);
638 let database = state.join(DATABASE);
639 assert!(!crate::test_os::database_released(&database).unwrap());
640 Receipts::clone(&writer).close().await.unwrap();
641 assert!(crate::test_os::database_released(&database).unwrap());
642 assert_eq!(retained.state_id().await.unwrap_err(), "ConnectionClosed");
643 root.close().unwrap();
644 }
645
646 #[tokio::test]
647 async fn owned_writer_closes_before_temp_state_removal_on_success_and_unwind() {
648 for unwind in [false, true] {
649 let root = crate::test_os::tempdir();
650 let state = root.path().join("state");
651 let repo = root.path().join("repo");
652 std::fs::create_dir(&repo).unwrap();
653 let writer = OwnedReceipts::open(&state, &repo).await.unwrap();
654 assert!(writer.state_id().await.is_ok());
655 let retained = Receipts::clone(&writer);
657 if unwind {
658 let failed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
659 let _writer = writer;
660 panic!("fixture assertion failure");
661 }));
662 assert!(failed.is_err());
663 } else {
664 drop(writer);
665 }
666 let error = retained.state_id().await.unwrap_err();
667 assert_eq!(error, "ConnectionClosed");
668 root.close().unwrap();
669 }
670 }
671}
672
673#[derive(Clone)]
677pub(crate) struct NotifyingConnection {
678 inner: Connection,
679 publisher: crate::changes::Publisher,
680 noting: std::sync::Arc<std::sync::atomic::AtomicBool>,
682}
683impl NotifyingConnection {
684 pub(super) async fn finish<T>(self, result: Result<T, String>) -> Result<T, String> {
685 super::connection::finish(self.inner, result).await
686 }
687 pub(crate) fn new(inner: Connection, state: &Path) -> Self {
688 Self {
689 inner,
690 publisher: crate::changes::Publisher::new(state),
691 noting: Default::default(),
692 }
693 }
694 pub(crate) async fn call<F, R, E>(&self, function: F) -> Result<R, tokio_rusqlite::Error<E>>
695 where
696 F: FnOnce(&mut rusqlite::Connection) -> Result<R, E> + Send + 'static,
697 R: Send + 'static,
698 E: Send + 'static,
699 {
700 let publisher = self.publisher.clone();
701 let noting = self.noting.clone();
702 self.inner
703 .call(move |db| {
704 if !noting.load(std::sync::atomic::Ordering::Relaxed)
705 && db.is_autocommit()
706 && super::touched::install(db).unwrap_or(false)
707 {
708 noting.store(true, std::sync::atomic::Ordering::Relaxed);
709 }
710 let before = db.total_changes();
711 let result = function(db);
712 if db.is_autocommit() && db.total_changes() != before {
713 for change in super::touched::take(db) {
714 publisher.notify(change);
715 }
716 }
717 result
718 })
719 .await
720 }
721}
722
723impl Receipts {
724 pub async fn close(self) -> Result<(), String> {
727 self.connection.finish(Ok(())).await
728 }
729
730 pub async fn open(state: &Path, repo: &Path) -> Result<Self, String> {
731 let state = prepare_directory(state, repo).map_err(|e| e.to_string())?;
732 check_files(&state)?;
733 let state_id: crate::types::StateId = crate::agent::uuid()?.parse()?;
734 let connection = Connection::open(state.join(DATABASE))
735 .await
736 .map_err(|e| e.to_string())?;
737 let connection = NotifyingConnection::new(connection, &state);
738 let deadline = tokio::time::Instant::now() + super::SQLITE_BUSY_TIMEOUT;
740 loop {
741 let state_id = state_id.clone();
742 let initialized = connection
743 .call(move |db| -> Result<(), Error> {
744 super::wait_when_busy(db)?;
745 db.pragma_update(None, "foreign_keys", true)?;
746 let version: u32 =
747 db.pragma_query_value(None, "user_version", |row| row.get(0))?;
748 if version != 0
749 && !(super::migrations::FIRST_VERSION..=STATE_SCHEMA_VERSION)
750 .contains(&version)
751 {
752 return Err(Error::Invalid(schema_error(version)));
753 }
754 db.pragma_update(None, "journal_mode", "WAL")?;
755 let transaction =
756 db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
757 super::migrations::apply(&transaction)?;
760 if !schema_initialized(&transaction)? {
761 transaction.execute_batch(SCHEMA)?;
762 transaction.execute_batch(REVISIONS)?;
763 transaction.execute(
764 "INSERT INTO state_meta(name,value) VALUES ('id',?)",
765 [&state_id],
766 )?;
767 transaction.pragma_update(None, "user_version", STATE_SCHEMA_VERSION)?;
768 }
769 transaction.commit()?;
770 Ok(())
771 })
772 .await;
773 match initialized {
774 Ok(()) => break,
775 Err(tokio_rusqlite::Error::Error(Error::Sql(rusqlite::Error::SqliteFailure(
776 error,
777 _,
778 )))) if error.code == rusqlite::ErrorCode::DatabaseBusy
779 && tokio::time::Instant::now() < deadline =>
780 {
781 tokio::time::sleep(INITIALIZATION_RETRY_INTERVAL).await;
783 }
784 Err(error) => return connection.finish(Err(error.to_string())).await,
785 }
786 }
787 Ok(Self { connection })
788 }
789
790 pub async fn state_id(&self) -> Result<crate::types::StateId, String> {
792 self.connection
793 .call(|db| -> Result<crate::types::StateId, Error> {
794 Ok(
795 db.query_row("SELECT value FROM state_meta WHERE name='id'", [], |row| {
796 row.get(0)
797 })?,
798 )
799 })
800 .await
801 .map_err(|e| e.to_string())
802 }
803
804 pub async fn create_run(&self, run: &Run, requests: &[Request]) -> Result<(), String> {
805 for request in requests {
806 if request.run_id != run.id {
807 return Err("Request belongs to another Run.".into());
808 }
809 }
810 let run = run.clone();
811 let requests = requests.to_vec();
812 self.connection
813 .call(move |db| -> Result<(), Error> {
814 let transaction = db.transaction()?;
815 super::run_record::insert(&transaction, &run)?;
816 for (ordinal, request) in requests.iter().enumerate() {
817 transaction.execute(
818 "INSERT INTO requests(id,run_id,eval_id,ordinal,status,data) VALUES (?,?,?,?,?,?)",
819 params![
820 request.id,
821 run.id,
822 request.eval_id,
823 ordinal as i64,
824 request.status(),
825 serde_json::to_string(request)?
826 ],
827 )?;
828 }
829 transaction.commit()?;
830 Ok(())
831 })
832 .await
833 .map_err(|e| e.to_string())
834 }
835
836 pub async fn save_request(&self, request: &Request) -> Result<(), String> {
837 let request = request.clone();
838 self.connection
839 .call(move |db| update_request(db, &request))
840 .await
841 .map_err(|e| e.to_string())
842 }
843
844 pub async fn save_run(&self, run: &Run) -> Result<(), String> {
845 let run = run.clone();
846 self.connection
847 .call(move |db| -> Result<(), Error> {
848 let transaction =
849 db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
850 super::run_control::save(&transaction, run)?;
851 transaction.commit()?;
852 Ok(())
853 })
854 .await
855 .map_err(|e| e.to_string())
856 }
857
858 pub async fn finish(&self, run: &Run, requests: &[Request]) -> Result<(), String> {
859 let run = run.clone();
860 let requests = requests.to_vec();
861 self.connection
862 .call(move |db| -> Result<(), Error> {
863 let transaction =
864 db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
865 for request in requests {
866 if request.status().is_human_wait() {
868 continue;
869 }
870 transaction.execute("UPDATE requests SET status=?,data=?,execution_id=? WHERE id=? AND status NOT IN ('GREEN','RED','ERROR')", params![request.status(),serde_json::to_string(&request)?,request.execution_id,request.id])?;
871 }
872 super::run_control::save(&transaction, run)?;
873 transaction.commit()?;
874 Ok(())
875 })
876 .await
877 .map_err(|e| e.to_string())
878 }
879}
880
881pub(super) fn update_request(db: &rusqlite::Connection, request: &Request) -> Result<(), Error> {
882 if db.execute(
883 "UPDATE requests SET status=?,data=?,execution_id=? WHERE id=?",
884 params![
885 request.status(),
886 serde_json::to_string(request)?,
887 request.execution_id,
888 request.id
889 ],
890 )? != 1
891 {
892 return Err(Error::Invalid("Review request not found.".into()));
893 }
894 Ok(())
895}
896
897pub(super) fn schema_initialized(transaction: &rusqlite::Transaction<'_>) -> Result<bool, Error> {
898 let version: u32 = transaction.pragma_query_value(None, "user_version", |row| row.get(0))?;
899 if version == 0
901 && !transaction.query_row("SELECT EXISTS(SELECT 1 FROM sqlite_master)", [], |row| {
902 row.get::<_, bool>(0)
903 })?
904 {
905 return Ok(false);
906 }
907 if version != STATE_SCHEMA_VERSION {
908 return Err(Error::Invalid(schema_error(version)));
909 }
910 Ok(true)
911}
912
913pub(super) fn check_files(state: &Path) -> Result<(), String> {
915 match state_schema(state)? {
916 Some(version)
917 if (super::migrations::FIRST_VERSION..STATE_SCHEMA_VERSION).contains(&version) =>
918 {
919 super::migrations::open_existing(state)
920 }
921 Some(version) if version != 0 && version != STATE_SCHEMA_VERSION => {
922 Err(schema_error(version))
923 }
924 _ => Ok(()),
925 }
926}
927
928pub fn state_schema(state: &Path) -> Result<Option<u32>, String> {
931 let state = canonical_target(state).map_err(|e| e.to_string())?;
932 regular_files(&state)?;
933 let database = state.join(DATABASE);
934 if !database.try_exists().map_err(|e| e.to_string())? {
935 return Ok(None);
936 }
937 let read = || -> Result<u32, rusqlite::Error> {
938 let db = rusqlite::Connection::open_with_flags(
939 &database,
940 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
941 )?;
942 super::wait_when_busy(&db)?;
943 db.pragma_query_value(None, "user_version", |row| row.get(0))
944 };
945 let version = read().map_err(|e| e.to_string())?;
946 if (super::migrations::FIRST_VERSION..STATE_SCHEMA_VERSION).contains(&version) {
947 super::migrations::open_existing(&state)?;
948 return read().map(Some).map_err(|e| e.to_string());
949 }
950 Ok(Some(version))
951}
952
953pub fn read_state_id(state: &Path) -> Result<Option<crate::types::StateId>, String> {
956 let state = canonical_target(state).map_err(|e| e.to_string())?;
957 check_files(&state)?;
958 let database = state.join(DATABASE);
959 if !database.try_exists().map_err(|e| e.to_string())? {
960 return Ok(None);
961 }
962 let read = || -> Result<Option<crate::types::StateId>, Error> {
963 let mut db = rusqlite::Connection::open_with_flags(
964 &database,
965 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
966 )?;
967 super::wait_when_busy(&db)?;
968 let transaction = db.transaction()?;
969 if !schema_initialized(&transaction)? {
970 return Ok(None);
971 }
972 Ok(transaction
973 .query_row("SELECT value FROM state_meta WHERE name='id'", [], |row| {
974 row.get(0)
975 })
976 .optional()?)
977 };
978 read().map_err(|e| e.to_string())
979}
980
981pub(crate) fn regular_files(state: &Path) -> Result<(), String> {
982 regular_files_with_retry(state, || {
983 std::thread::sleep(super::TRANSIENT_ACCESS_BACKOFF);
984 })
985}
986
987pub(super) fn regular_files_with_retry(
990 state: &Path,
991 mut before_retry: impl FnMut(),
992) -> Result<(), String> {
993 for suffix in ["", "-wal", "-shm"] {
994 let path = state.join(format!("{DATABASE}{suffix}"));
995 match super::retry_transient_access(|| crate::platform::path_kind(&path), &mut before_retry)
996 {
997 Ok(kind) if kind != crate::platform::FileKind::File => {
998 return Err(format!(
999 "State files must be regular files: {}",
1000 crate::platform::path_text(&path)
1001 ));
1002 }
1003 Err(error) if error.kind() != std::io::ErrorKind::NotFound => {
1004 return Err(error.to_string());
1005 }
1006 _ => {}
1007 }
1008 }
1009 Ok(())
1010}
1011
1012#[derive(Debug, Clone, Serialize)]
1013#[serde(rename_all = "camelCase")]
1014pub struct LastRequest {
1015 pub run_id: RunId,
1016 pub verdict: RequestStatus,
1017 #[serde(skip_serializing_if = "Option::is_none")]
1018 pub fingerprint: Option<Fingerprint>,
1019}
1020
1021pub async fn read_latest_requests(
1023 state: &Path,
1024 repo: &Path,
1025) -> Result<std::collections::BTreeMap<crate::types::EvalId, LastRequest>, String> {
1026 let state = canonical_target(state).map_err(|e| e.to_string())?;
1027 outside_workspace(repo, &state).map_err(|e| e.to_string())?;
1028 check_files(&state)?;
1029 if !state
1030 .join(DATABASE)
1031 .try_exists()
1032 .map_err(|e| e.to_string())?
1033 {
1034 return Ok(Default::default());
1035 }
1036 let connection = Connection::open_with_flags(
1037 state.join(DATABASE),
1038 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
1039 )
1040 .await
1041 .map_err(|e| e.to_string())?;
1042 let repo = repo.to_path_buf();
1043 let result = connection
1044 .call(move |db| -> Result<_, Error> {
1045 super::wait_when_busy(db)?;
1046 let transaction = db.transaction()?;
1047 if !schema_initialized(&transaction)? {
1048 return Ok(Default::default());
1049 }
1050 let latest = {
1051 let mut statement = transaction.prepare(
1052 "SELECT data,run_id FROM (
1053 SELECT q.eval_id,q.data,q.run_id,
1054 row_number() OVER (PARTITION BY q.eval_id ORDER BY r.rowid DESC) AS rank
1055 FROM requests q JOIN runs r ON r.id=q.run_id
1056 WHERE r.repo=?
1057 ) WHERE rank=1 ORDER BY eval_id",
1058 )?;
1059 let mut run_data = transaction.prepare("SELECT data FROM runs WHERE id=?")?;
1062 let mut readable = std::collections::BTreeMap::<String, bool>::new();
1063 statement
1064 .query_map([crate::platform::path_text(&repo)], |row| {
1065 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
1066 })?
1067 .filter_map(|row| match row {
1068 Err(error) => Some(Err(error)),
1069 Ok((data, run)) => {
1070 let run_readable = match readable.get(&run) {
1071 Some(known) => *known,
1072 None => {
1073 let known = match run_data
1074 .query_row([&run], |row| row.get::<_, String>(0))
1075 {
1076 Ok(run_data) => {
1077 super::unreadable::evidence::<Run>("run", &run_data)
1078 .is_some()
1079 }
1080 Err(error) => return Some(Err(error)),
1081 };
1082 readable.insert(run, known);
1083 known
1084 }
1085 };
1086 run_readable
1087 .then(|| super::unreadable::evidence::<Request>("request", &data))
1088 .flatten()
1089 .map(|request| {
1090 Ok((
1091 request.eval_id.clone(),
1092 LastRequest {
1093 run_id: request.run_id.clone(),
1094 verdict: request.status(),
1095 fingerprint: request.fingerprint,
1096 },
1097 ))
1098 })
1099 }
1100 })
1101 .collect::<Result<_, _>>()?
1102 };
1103 transaction.commit()?;
1104 Ok(latest)
1105 })
1106 .await
1107 .map_err(|e| e.to_string());
1108 super::connection::finish(connection, result).await
1109}
1110
1111pub async fn read_run(state: &Path, id: &str) -> Result<RunView, String> {
1113 let state = canonical_target(state).map_err(|e| e.to_string())?;
1114 check_files(&state)?;
1115 let connection = Connection::open_with_flags(
1116 state.join(DATABASE),
1117 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
1118 )
1119 .await
1120 .map_err(|e| e.to_string())?;
1121 let result = async {
1122 let id: RunId = id.parse()?;
1123 connection
1124 .call(move |db| -> Result<RunView, Error> {
1125 super::wait_when_busy(db)?;
1126 let transaction = db.transaction()?;
1127 if !schema_initialized(&transaction)? {
1128 return Err(Error::Invalid("Run not found.".into()));
1129 }
1130 let saved: Option<(String, String)> = transaction
1131 .query_row("SELECT repo,data FROM runs WHERE id=?", [&id], |row| {
1132 Ok((row.get(0)?, row.get(1)?))
1133 })
1134 .optional()?;
1135 let (repo, data) = saved.ok_or_else(|| Error::Invalid("Run not found.".into()))?;
1136 outside_workspace(Path::new(&repo), &state)
1137 .map_err(|e| Error::Invalid(e.to_string()))?;
1138 let mut run: Run = super::unreadable::decode("run", &id, &data)
1139 .map_err(|error| Error::Invalid(error.to_string()))?;
1140 run.definitions = super::run_record::definitions(&transaction, &id)?
1141 .map_err(|error| Error::Invalid(error.to_string()))?;
1142 let requests = {
1143 let mut statement = transaction
1144 .prepare("SELECT data FROM requests WHERE run_id=? ORDER BY ordinal")?;
1145 statement
1146 .query_map([&id], |row| row.get::<_, String>(0))?
1147 .filter_map(|row| match row {
1148 Ok(data) => super::unreadable::evidence("request", &data).map(Ok),
1149 Err(error) => Some(Err(Error::Sql(error))),
1150 })
1151 .collect::<Result<Vec<Request>, Error>>()?
1152 };
1153 let unreadable = super::unreadable::in_run(&transaction, &id)?;
1154 transaction.commit()?;
1155 Ok(RunView {
1156 run,
1157 requests,
1158 unreadable,
1159 })
1160 })
1161 .await
1162 .map_err(|e| e.to_string())
1163 }
1164 .await;
1165 super::connection::finish(connection, result).await
1168}