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