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(super) fn show_signoffs(
268 &mut self,
269 mut statuses: impl Iterator<Item = crate::types::RequestStatus>,
270 ) {
271 let open = statuses.any(crate::types::RequestStatus::is_human_wait);
272 self.state = self.state.clone().with_signoffs(open);
273 }
274 pub fn completed_at(&self) -> Option<Timestamp> {
275 self.state.completed_at()
276 }
277 pub fn error(&self) -> Option<&str> {
278 self.state.error()
279 }
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
285#[serde(rename_all = "camelCase")]
286pub struct StoppedBackend {
287 pub backend: crate::config::Backend,
288 pub error_code: crate::types::BackendStopCode,
289 pub eval_id: crate::types::EvalId,
290 pub request_id: RequestId,
291 pub error: String,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
295#[serde(try_from = "RequestWire", into = "RequestWire")]
296pub struct Request {
297 pub state: super::RequestState,
298 pub id: RequestId,
299 pub run_id: RunId,
300 pub eval_id: crate::types::EvalId,
301 pub target: crate::types::ArtifactName,
302 pub title: String,
303 pub profile: crate::config::StoredProfile,
304 pub requested_profile: crate::config::StoredProfile,
305 pub eval_def_hash: crate::types::DefinitionHash,
306 pub options: ExecutionOptions,
308 pub execution_id: Option<crate::types::ExecutionId>,
309 pub provenance: Option<Provenance>,
310 pub usage: Option<Vec<crate::llm::Attempt>>,
312 pub reused_usage: Option<Vec<crate::llm::Attempt>>,
314 pub joined: bool,
317 pub producer: Option<Producer>,
319 pub reviewer: Option<crate::types::ReviewerId>,
321 pub origin: Option<Origin>,
323 pub session_id: Option<SessionId>,
327 pub session: Option<crate::agent::session::SessionRef>,
330 pub human_definition: Option<super::HumanDefinition>,
331 pub payload: crate::config::StoredPayload,
332 pub references: BTreeMap<crate::config::ReferenceName, crate::types::ArtifactName>,
333 pub deps: Vec<crate::types::ArtifactName>,
334 pub force: bool,
335 pub fingerprint: Option<Fingerprint>,
337 pub key: Option<ReuseKey>,
339 pub fingerprints: std::collections::BTreeMap<crate::types::ArtifactName, Fingerprint>,
341 pub created_at: Timestamp,
342 pub started_at: Option<Timestamp>,
343 pub cwd: PathBuf,
344 pub run_dir: Option<PathBuf>,
345 pub argv: Option<Vec<String>>,
346 pub child: Option<super::ChildIdentity>,
347 pub blocked_reason: Option<String>,
348 pub blocked_by: Vec<super::Blocker>,
350 pub queue: Option<QueueReason>,
352}
353
354#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
356#[serde(tag = "kind", rename_all = "camelCase")]
357pub enum QueueReason {
358 Slot { backend: crate::config::Backend },
360}
361
362#[derive(Debug, Clone, Serialize, Deserialize)]
363#[serde(rename_all = "camelCase")]
364pub(crate) struct RequestWire {
365 pub id: RequestId,
366 pub run_id: RunId,
367 pub eval_id: crate::types::EvalId,
368 pub target: crate::types::ArtifactName,
369 pub title: String,
370 pub profile: crate::config::StoredProfile,
371 pub requested_profile: crate::config::StoredProfile,
372 pub eval_def_hash: crate::types::DefinitionHash,
373 #[serde(default)]
375 pub options: ExecutionOptions,
376 pub execution_id: Option<crate::types::ExecutionId>,
377 pub provenance: Option<Provenance>,
378 pub usage: Option<Vec<crate::llm::Attempt>>,
380 #[serde(default, skip_serializing_if = "Option::is_none")]
382 pub reused_usage: Option<Vec<crate::llm::Attempt>>,
383 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
386 pub joined: bool,
387 #[serde(default, skip_serializing_if = "Option::is_none")]
389 pub producer: Option<Producer>,
390 #[serde(default, skip_serializing_if = "Option::is_none")]
392 pub reviewer: Option<crate::types::ReviewerId>,
393 #[serde(default, skip_serializing_if = "Option::is_none")]
395 pub origin: Option<Origin>,
396 #[serde(default, skip_serializing_if = "Option::is_none")]
400 pub session_id: Option<SessionId>,
401 #[serde(default, skip_serializing_if = "Option::is_none")]
404 pub session: Option<crate::agent::session::SessionRef>,
405 #[serde(default, skip_serializing_if = "Option::is_none")]
406 pub human_definition: Option<super::HumanDefinition>,
407 pub payload: crate::config::StoredPayload,
408 pub references: BTreeMap<crate::config::ReferenceName, crate::types::ArtifactName>,
409 pub deps: Vec<crate::types::ArtifactName>,
410 #[serde(default)]
411 pub force: bool,
412 #[serde(default)]
414 pub fingerprint: Option<Fingerprint>,
415 #[serde(default)]
417 pub key: Option<ReuseKey>,
418 #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
420 pub fingerprints: std::collections::BTreeMap<crate::types::ArtifactName, Fingerprint>,
421 pub status: RequestStatus,
422 pub created_at: Timestamp,
423 pub started_at: Option<Timestamp>,
424 pub completed_at: Option<Timestamp>,
425 #[serde(with = "crate::platform::path_serde")]
426 pub cwd: PathBuf,
427 #[serde(default, with = "crate::platform::path_serde::option")]
428 pub run_dir: Option<PathBuf>,
429 pub argv: Option<Vec<String>>,
430 pub child: Option<super::ChildIdentity>,
431 pub result: Option<super::ExecutionResult>,
432 pub error: Option<String>,
433 pub error_code: Option<crate::types::FailureCode>,
434 pub blocked_reason: Option<String>,
435 #[serde(default, skip_serializing_if = "Vec::is_empty")]
437 pub blocked_by: Vec<super::Blocker>,
438 #[serde(default, skip_serializing_if = "Option::is_none")]
440 pub queue: Option<QueueReason>,
441}
442
443impl TryFrom<RequestWire> for Request {
444 type Error = String;
445 fn try_from(wire: RequestWire) -> Result<Self, Self::Error> {
446 let state = super::RequestState::from_parts(
447 wire.status,
448 wire.result,
449 wire.error,
450 wire.error_code,
451 wire.completed_at,
452 )?;
453 Ok(Self {
454 state,
455 id: wire.id,
456 run_id: wire.run_id,
457 eval_id: wire.eval_id,
458 target: wire.target,
459 title: wire.title,
460 profile: wire.profile,
461 requested_profile: wire.requested_profile,
462 eval_def_hash: wire.eval_def_hash,
463 options: wire.options,
464 execution_id: wire.execution_id,
465 provenance: wire.provenance,
466 usage: wire.usage,
467 reused_usage: wire.reused_usage,
468 joined: wire.joined,
469 producer: wire.producer,
470 reviewer: wire.reviewer,
471 origin: wire.origin,
472 session_id: wire.session_id,
473 session: wire.session,
474 human_definition: wire.human_definition,
475 payload: wire.payload,
476 references: wire.references,
477 deps: wire.deps,
478 force: wire.force,
479 fingerprint: wire.fingerprint,
480 key: wire.key,
481 fingerprints: wire.fingerprints,
482 created_at: wire.created_at,
483 started_at: wire.started_at,
484 cwd: wire.cwd,
485 run_dir: wire.run_dir,
486 argv: wire.argv,
487 child: wire.child,
488 blocked_reason: wire.blocked_reason,
489 blocked_by: wire.blocked_by,
490 queue: wire.queue,
491 })
492 }
493}
494impl From<Request> for RequestWire {
495 fn from(record: Request) -> Self {
496 Self {
497 status: record.status(),
498 result: record.result().cloned(),
499 error: record.error().map(str::to_owned),
500 error_code: record.error_code(),
501 completed_at: record.completed_at(),
502 id: record.id,
503 run_id: record.run_id,
504 eval_id: record.eval_id,
505 target: record.target,
506 title: record.title,
507 profile: record.profile,
508 requested_profile: record.requested_profile,
509 eval_def_hash: record.eval_def_hash,
510 options: record.options,
511 execution_id: record.execution_id,
512 provenance: record.provenance,
513 usage: record.usage,
514 reused_usage: record.reused_usage,
515 joined: record.joined,
516 producer: record.producer,
517 reviewer: record.reviewer,
518 origin: record.origin,
519 session_id: record.session_id,
520 session: record.session,
521 human_definition: record.human_definition,
522 payload: record.payload,
523 references: record.references,
524 deps: record.deps,
525 force: record.force,
526 fingerprint: record.fingerprint,
527 key: record.key,
528 fingerprints: record.fingerprints,
529 created_at: record.created_at,
530 started_at: record.started_at,
531 cwd: record.cwd,
532 run_dir: record.run_dir,
533 argv: record.argv,
534 child: record.child,
535 blocked_reason: record.blocked_reason,
536 blocked_by: record.blocked_by,
537 queue: record.queue,
538 }
539 }
540}
541impl Request {
542 pub fn status(&self) -> crate::types::RequestStatus {
543 self.state.status()
544 }
545 pub fn result(&self) -> Option<&super::ExecutionResult> {
546 self.state.result()
547 }
548 pub fn error(&self) -> Option<&str> {
549 self.state.error()
550 }
551 pub fn error_code(&self) -> Option<crate::types::FailureCode> {
552 self.state.error_code()
553 }
554 pub fn completed_at(&self) -> Option<crate::types::Timestamp> {
555 self.state.completed_at()
556 }
557}
558
559#[derive(Debug, Serialize, Deserialize)]
560pub struct RunView {
561 #[serde(flatten)]
562 pub run: Run,
563 pub requests: Vec<std::sync::Arc<Request>>,
564 #[serde(default, skip_serializing_if = "Vec::is_empty")]
565 pub unreadable: Vec<super::Unreadable>,
566}
567
568#[derive(Clone)]
569pub struct Receipts {
570 pub(super) connection: NotifyingConnection,
571}
572
573#[cfg(any(test, feature = "test-support"))]
575pub struct OwnedReceipts(Receipts);
576
577#[cfg(any(test, feature = "test-support"))]
578impl OwnedReceipts {
579 pub async fn open(state: &Path, repo: &Path) -> Result<Self, String> {
580 Receipts::open(state, repo).await.map(Self)
581 }
582}
583
584#[cfg(any(test, feature = "test-support"))]
585impl std::ops::Deref for OwnedReceipts {
586 type Target = Receipts;
587 fn deref(&self) -> &Self::Target {
588 &self.0
589 }
590}
591
592#[cfg(any(test, feature = "test-support"))]
593impl Drop for OwnedReceipts {
594 fn drop(&mut self) {
595 let connection = self.0.connection.inner.clone();
596 let closed = std::thread::spawn(move || {
599 tokio::runtime::Builder::new_current_thread()
600 .build()
601 .expect("create the test writer's teardown runtime")
602 .block_on(connection.close())
603 })
604 .join();
605 if !std::thread::panicking() {
606 closed
607 .expect("join the test writer's teardown")
608 .expect("close the test writer's SQLite connection");
609 }
610 }
611}
612
613#[cfg(test)]
614mod ownership_tests {
615 use super::*;
616
617 #[tokio::test]
618 async fn failed_writer_initialization_releases_sqlite_before_returning_the_schema_error() {
619 let root = crate::test_os::tempdir();
620 let state = root.path().join("state");
621 let repo = root.path().join("repo");
622 std::fs::create_dir(&state).unwrap();
623 std::fs::create_dir(&repo).unwrap();
624 let database = state.join(DATABASE);
625 let connection = rusqlite::Connection::open(&database).unwrap();
626 connection
627 .execute_batch("CREATE TABLE unrelated(value TEXT)")
628 .unwrap();
629 drop(connection);
630 let error = match OwnedReceipts::open(&state, &repo).await {
631 Ok(_) => panic!("an unversioned nonempty database is not our schema"),
632 Err(error) => error,
633 };
634 assert!(error.contains(&schema_error(0)), "{error}");
635 assert!(crate::test_os::database_released(&database).unwrap());
636 root.close().unwrap();
637 }
638
639 #[tokio::test]
640 async fn explicit_writer_close_releases_sqlite_before_return_and_closes_clones() {
641 let root = crate::test_os::tempdir();
642 let state = root.path().join("state");
643 let repo = root.path().join("repo");
644 std::fs::create_dir(&repo).unwrap();
645 let writer = OwnedReceipts::open(&state, &repo).await.unwrap();
646 let retained = Receipts::clone(&writer);
647 let database = state.join(DATABASE);
648 assert!(!crate::test_os::database_released(&database).unwrap());
649 Receipts::clone(&writer).close().await.unwrap();
650 assert!(crate::test_os::database_released(&database).unwrap());
651 assert_eq!(retained.state_id().await.unwrap_err(), "ConnectionClosed");
652 root.close().unwrap();
653 }
654
655 #[tokio::test]
656 async fn owned_writer_closes_before_temp_state_removal_on_success_and_unwind() {
657 for unwind in [false, true] {
658 let root = crate::test_os::tempdir();
659 let state = root.path().join("state");
660 let repo = root.path().join("repo");
661 std::fs::create_dir(&repo).unwrap();
662 let writer = OwnedReceipts::open(&state, &repo).await.unwrap();
663 assert!(writer.state_id().await.is_ok());
664 let retained = Receipts::clone(&writer);
666 if unwind {
667 let failed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
668 let _writer = writer;
669 panic!("fixture assertion failure");
670 }));
671 assert!(failed.is_err());
672 } else {
673 drop(writer);
674 }
675 let error = retained.state_id().await.unwrap_err();
676 assert_eq!(error, "ConnectionClosed");
677 root.close().unwrap();
678 }
679 }
680}
681
682#[derive(Clone)]
686pub(crate) struct NotifyingConnection {
687 inner: Connection,
688 publisher: crate::changes::Publisher,
689 noting: std::sync::Arc<std::sync::atomic::AtomicBool>,
691}
692impl NotifyingConnection {
693 pub(super) async fn finish<T>(self, result: Result<T, String>) -> Result<T, String> {
694 super::connection::finish(self.inner, result).await
695 }
696 pub(crate) fn new(inner: Connection, state: &Path) -> Self {
697 Self {
698 inner,
699 publisher: crate::changes::Publisher::new(state),
700 noting: Default::default(),
701 }
702 }
703 pub(crate) async fn call<F, R, E>(&self, function: F) -> Result<R, tokio_rusqlite::Error<E>>
704 where
705 F: FnOnce(&mut rusqlite::Connection) -> Result<R, E> + Send + 'static,
706 R: Send + 'static,
707 E: Send + 'static,
708 {
709 let publisher = self.publisher.clone();
710 let noting = self.noting.clone();
711 self.inner
712 .call(move |db| {
713 if !noting.load(std::sync::atomic::Ordering::Relaxed)
714 && db.is_autocommit()
715 && super::touched::install(db).unwrap_or(false)
716 {
717 noting.store(true, std::sync::atomic::Ordering::Relaxed);
718 }
719 let before = db.total_changes();
720 let result = function(db);
721 if db.is_autocommit() && db.total_changes() != before {
722 for change in super::touched::take(db) {
723 publisher.notify(change);
724 }
725 }
726 result
727 })
728 .await
729 }
730}
731
732impl Receipts {
733 pub async fn close(self) -> Result<(), String> {
736 self.connection.finish(Ok(())).await
737 }
738
739 pub async fn open(state: &Path, repo: &Path) -> Result<Self, String> {
740 let state = prepare_directory(state, repo).map_err(|e| e.to_string())?;
741 check_files(&state)?;
742 let state_id: crate::types::StateId = crate::agent::uuid()?.parse()?;
743 let connection = Connection::open(state.join(DATABASE))
744 .await
745 .map_err(|e| e.to_string())?;
746 let connection = NotifyingConnection::new(connection, &state);
747 let deadline = tokio::time::Instant::now() + super::SQLITE_BUSY_TIMEOUT;
749 loop {
750 let state_id = state_id.clone();
751 let initialized = connection
752 .call(move |db| -> Result<(), Error> {
753 super::wait_when_busy(db)?;
754 db.pragma_update(None, "foreign_keys", true)?;
755 let version: u32 =
756 db.pragma_query_value(None, "user_version", |row| row.get(0))?;
757 if version != 0
758 && !(super::migrations::FIRST_VERSION..=STATE_SCHEMA_VERSION)
759 .contains(&version)
760 {
761 return Err(Error::Invalid(schema_error(version)));
762 }
763 db.pragma_update(None, "journal_mode", "WAL")?;
764 let transaction =
765 db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
766 super::migrations::apply(&transaction)?;
769 if !schema_initialized(&transaction)? {
770 transaction.execute_batch(SCHEMA)?;
771 transaction.execute_batch(REVISIONS)?;
772 transaction.execute(
773 "INSERT INTO state_meta(name,value) VALUES ('id',?)",
774 [&state_id],
775 )?;
776 transaction.pragma_update(None, "user_version", STATE_SCHEMA_VERSION)?;
777 }
778 transaction.commit()?;
779 Ok(())
780 })
781 .await;
782 match initialized {
783 Ok(()) => break,
784 Err(tokio_rusqlite::Error::Error(Error::Sql(rusqlite::Error::SqliteFailure(
785 error,
786 _,
787 )))) if error.code == rusqlite::ErrorCode::DatabaseBusy
788 && tokio::time::Instant::now() < deadline =>
789 {
790 tokio::time::sleep(INITIALIZATION_RETRY_INTERVAL).await;
792 }
793 Err(error) => return connection.finish(Err(error.to_string())).await,
794 }
795 }
796 Ok(Self { connection })
797 }
798
799 pub async fn state_id(&self) -> Result<crate::types::StateId, String> {
801 self.connection
802 .call(|db| -> Result<crate::types::StateId, Error> {
803 Ok(
804 db.query_row("SELECT value FROM state_meta WHERE name='id'", [], |row| {
805 row.get(0)
806 })?,
807 )
808 })
809 .await
810 .map_err(|e| e.to_string())
811 }
812
813 pub async fn create_run(&self, run: &Run, requests: &[Request]) -> Result<(), String> {
814 for request in requests {
815 if request.run_id != run.id {
816 return Err("Request belongs to another Run.".into());
817 }
818 }
819 let run = run.clone();
820 let requests = requests.to_vec();
821 self.connection
822 .call(move |db| -> Result<(), Error> {
823 let transaction = db.transaction()?;
824 super::run_record::insert(&transaction, &run)?;
825 for (ordinal, request) in requests.iter().enumerate() {
826 transaction.execute(
827 "INSERT INTO requests(id,run_id,eval_id,ordinal,status,data) VALUES (?,?,?,?,?,?)",
828 params![
829 request.id,
830 run.id,
831 request.eval_id,
832 ordinal as i64,
833 request.status(),
834 serde_json::to_string(request)?
835 ],
836 )?;
837 }
838 transaction.commit()?;
839 Ok(())
840 })
841 .await
842 .map_err(|e| e.to_string())
843 }
844
845 pub async fn save_request(&self, request: &Request) -> Result<(), String> {
846 let request = request.clone();
847 self.connection
848 .call(move |db| update_request(db, &request))
849 .await
850 .map_err(|e| e.to_string())
851 }
852
853 pub async fn save_run(&self, run: &Run) -> Result<(), String> {
854 let run = run.clone();
855 self.connection
856 .call(move |db| -> Result<(), Error> {
857 let transaction =
858 db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
859 super::run_control::save(&transaction, run)?;
860 transaction.commit()?;
861 Ok(())
862 })
863 .await
864 .map_err(|e| e.to_string())
865 }
866
867 pub async fn finish(&self, run: &Run, requests: &[Request]) -> Result<(), String> {
868 let run = run.clone();
869 let requests = requests.to_vec();
870 self.connection
871 .call(move |db| -> Result<(), Error> {
872 let transaction =
873 db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
874 for request in requests {
875 if request.status().is_human_wait() {
877 continue;
878 }
879 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])?;
880 }
881 super::run_control::save(&transaction, run)?;
882 transaction.commit()?;
883 Ok(())
884 })
885 .await
886 .map_err(|e| e.to_string())
887 }
888}
889
890pub(super) fn update_request(db: &rusqlite::Connection, request: &Request) -> Result<(), Error> {
891 if db.execute(
892 "UPDATE requests SET status=?,data=?,execution_id=? WHERE id=?",
893 params![
894 request.status(),
895 serde_json::to_string(request)?,
896 request.execution_id,
897 request.id
898 ],
899 )? != 1
900 {
901 return Err(Error::Invalid("Review request not found.".into()));
902 }
903 Ok(())
904}
905
906pub(super) fn schema_initialized(transaction: &rusqlite::Transaction<'_>) -> Result<bool, Error> {
907 let version: u32 = transaction.pragma_query_value(None, "user_version", |row| row.get(0))?;
908 if version == 0
910 && !transaction.query_row("SELECT EXISTS(SELECT 1 FROM sqlite_master)", [], |row| {
911 row.get::<_, bool>(0)
912 })?
913 {
914 return Ok(false);
915 }
916 if version != STATE_SCHEMA_VERSION {
917 return Err(Error::Invalid(schema_error(version)));
918 }
919 Ok(true)
920}
921
922pub(super) fn check_files(state: &Path) -> Result<(), String> {
924 match state_schema(state)? {
925 Some(version)
926 if (super::migrations::FIRST_VERSION..STATE_SCHEMA_VERSION).contains(&version) =>
927 {
928 super::migrations::open_existing(state)
929 }
930 Some(version) if version != 0 && version != STATE_SCHEMA_VERSION => {
931 Err(schema_error(version))
932 }
933 _ => Ok(()),
934 }
935}
936
937pub fn state_schema(state: &Path) -> Result<Option<u32>, String> {
940 let state = canonical_target(state).map_err(|e| e.to_string())?;
941 regular_files(&state)?;
942 let database = state.join(DATABASE);
943 if !database.try_exists().map_err(|e| e.to_string())? {
944 return Ok(None);
945 }
946 let read = || -> Result<u32, rusqlite::Error> {
947 let db = rusqlite::Connection::open_with_flags(
948 &database,
949 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
950 )?;
951 super::wait_when_busy(&db)?;
952 db.pragma_query_value(None, "user_version", |row| row.get(0))
953 };
954 let version = read().map_err(|e| e.to_string())?;
955 if (super::migrations::FIRST_VERSION..STATE_SCHEMA_VERSION).contains(&version) {
956 super::migrations::open_existing(&state)?;
957 return read().map(Some).map_err(|e| e.to_string());
958 }
959 Ok(Some(version))
960}
961
962pub fn read_state_id(state: &Path) -> Result<Option<crate::types::StateId>, String> {
965 let state = canonical_target(state).map_err(|e| e.to_string())?;
966 check_files(&state)?;
967 let database = state.join(DATABASE);
968 if !database.try_exists().map_err(|e| e.to_string())? {
969 return Ok(None);
970 }
971 let read = || -> Result<Option<crate::types::StateId>, Error> {
972 let mut db = rusqlite::Connection::open_with_flags(
973 &database,
974 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
975 )?;
976 super::wait_when_busy(&db)?;
977 let transaction = db.transaction()?;
978 if !schema_initialized(&transaction)? {
979 return Ok(None);
980 }
981 Ok(transaction
982 .query_row("SELECT value FROM state_meta WHERE name='id'", [], |row| {
983 row.get(0)
984 })
985 .optional()?)
986 };
987 read().map_err(|e| e.to_string())
988}
989
990pub(crate) fn regular_files(state: &Path) -> Result<(), String> {
991 regular_files_with_retry(state, || {
992 std::thread::sleep(super::TRANSIENT_ACCESS_BACKOFF);
993 })
994}
995
996pub(super) fn regular_files_with_retry(
999 state: &Path,
1000 mut before_retry: impl FnMut(),
1001) -> Result<(), String> {
1002 for suffix in ["", "-wal", "-shm"] {
1003 let path = state.join(format!("{DATABASE}{suffix}"));
1004 match super::retry_transient_access(|| crate::platform::path_kind(&path), &mut before_retry)
1005 {
1006 Ok(kind) if kind != crate::platform::FileKind::File => {
1007 return Err(format!(
1008 "State files must be regular files: {}",
1009 crate::platform::path_text(&path)
1010 ));
1011 }
1012 Err(error) if error.kind() != std::io::ErrorKind::NotFound => {
1013 return Err(error.to_string());
1014 }
1015 _ => {}
1016 }
1017 }
1018 Ok(())
1019}
1020
1021#[derive(Debug, Clone, Serialize)]
1022#[serde(rename_all = "camelCase")]
1023pub struct LastRequest {
1024 pub run_id: RunId,
1025 pub verdict: RequestStatus,
1026 #[serde(skip_serializing_if = "Option::is_none")]
1027 pub fingerprint: Option<Fingerprint>,
1028}
1029
1030pub async fn read_latest_requests(
1032 state: &Path,
1033 repo: &Path,
1034) -> Result<std::collections::BTreeMap<crate::types::EvalId, LastRequest>, String> {
1035 let state = canonical_target(state).map_err(|e| e.to_string())?;
1036 outside_workspace(repo, &state).map_err(|e| e.to_string())?;
1037 check_files(&state)?;
1038 if !state
1039 .join(DATABASE)
1040 .try_exists()
1041 .map_err(|e| e.to_string())?
1042 {
1043 return Ok(Default::default());
1044 }
1045 let connection = Connection::open_with_flags(
1046 state.join(DATABASE),
1047 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
1048 )
1049 .await
1050 .map_err(|e| e.to_string())?;
1051 let repo = repo.to_path_buf();
1052 let result = connection
1053 .call(move |db| -> Result<_, Error> {
1054 super::wait_when_busy(db)?;
1055 let transaction = db.transaction()?;
1056 if !schema_initialized(&transaction)? {
1057 return Ok(Default::default());
1058 }
1059 let latest = {
1060 let mut statement = transaction.prepare(
1061 "SELECT data,run_id FROM (
1062 SELECT q.eval_id,q.data,q.run_id,
1063 row_number() OVER (PARTITION BY q.eval_id ORDER BY r.rowid DESC) AS rank
1064 FROM requests q JOIN runs r ON r.id=q.run_id
1065 WHERE r.repo=?
1066 ) WHERE rank=1 ORDER BY eval_id",
1067 )?;
1068 let mut run_data = transaction.prepare("SELECT data FROM runs WHERE id=?")?;
1071 let mut readable = std::collections::BTreeMap::<String, bool>::new();
1072 statement
1073 .query_map([crate::platform::path_text(&repo)], |row| {
1074 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
1075 })?
1076 .filter_map(|row| match row {
1077 Err(error) => Some(Err(error)),
1078 Ok((data, run)) => {
1079 let run_readable = match readable.get(&run) {
1080 Some(known) => *known,
1081 None => {
1082 let known = match run_data
1083 .query_row([&run], |row| row.get::<_, String>(0))
1084 {
1085 Ok(run_data) => {
1086 super::unreadable::evidence::<Run>("run", &run_data)
1087 .is_some()
1088 }
1089 Err(error) => return Some(Err(error)),
1090 };
1091 readable.insert(run, known);
1092 known
1093 }
1094 };
1095 run_readable
1096 .then(|| super::unreadable::evidence::<Request>("request", &data))
1097 .flatten()
1098 .map(|request| {
1099 Ok((
1100 request.eval_id.clone(),
1101 LastRequest {
1102 run_id: request.run_id.clone(),
1103 verdict: request.status(),
1104 fingerprint: request.fingerprint,
1105 },
1106 ))
1107 })
1108 }
1109 })
1110 .collect::<Result<_, _>>()?
1111 };
1112 transaction.commit()?;
1113 Ok(latest)
1114 })
1115 .await
1116 .map_err(|e| e.to_string());
1117 super::connection::finish(connection, result).await
1118}