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