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);
40 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));
41 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, completed_at TEXT, bytes INTEGER, last_used TEXT, data TEXT NOT NULL);
42 CREATE TABLE state_meta(name TEXT PRIMARY KEY, value TEXT NOT NULL);
43 CREATE INDEX active_request_execution ON requests(execution_id) WHERE status IN ('QUEUED','RUNNING','WAITING_HUMAN');
44 CREATE UNIQUE INDEX active_key ON executions(key) WHERE key IS NOT NULL AND status IN ('RUNNING','WAITING_HUMAN');
45 CREATE INDEX key_history ON executions(key, completed_at);
46 CREATE INDEX definition_history ON executions(eval_def_hash, completed_at);
47 CREATE INDEX running_backend ON executions(backend) WHERE status='RUNNING';";
48
49pub 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). artifactize does not migrate it.";
51
52pub fn schema_error(version: u32) -> String {
54 match version {
55 0 => "Unsupported state schema version: 0.".into(),
56 version if version < STATE_SCHEMA_VERSION => EARLIER_STATE.into(),
57 version => {
58 format!("Unsupported state schema version: {version}; a newer artifactize wrote it.")
59 }
60 }
61}
62
63#[derive(Debug, thiserror::Error)]
64pub enum Error {
65 #[error(transparent)]
66 Sql(#[from] rusqlite::Error),
67 #[error(transparent)]
68 Json(#[from] serde_json::Error),
69 #[error("{0}")]
70 Invalid(String),
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
74#[serde(try_from = "RunWire", into = "RunWire")]
75pub struct Run {
76 pub id: RunId,
77 pub repo_path: PathBuf,
78 pub repository: crate::repository::Identity,
80 pub state_dir: PathBuf,
81 pub state: super::RunState,
82 pub created_at: Timestamp,
83 pub selection: crate::project::selection::Selection,
84 pub profile: Option<crate::project::selection::ProfileSelection>,
85 pub definitions: super::definitions::Definitions,
86 pub recursive: bool,
87 pub force: bool,
88 pub ignore_gates: bool,
89 pub jobs: usize,
90 pub fingerprint_jobs: Option<usize>,
92 pub max_executions: Option<u64>,
93 pub executions_started: u64,
94 pub wait_timeout_ms: Option<Duration>,
95 pub wait_timed_out: bool,
96 pub reuse_only: BTreeSet<ProfileKind>,
98 pub validation: super::Validation,
99 pub stopped_backends: Vec<StoppedBackend>,
101 pub evidence: BTreeMap<crate::types::EvalId, crate::types::RequestStatus>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
107#[serde(rename_all = "camelCase")]
108pub(crate) struct RunWire {
109 pub id: RunId,
110 #[serde(with = "crate::platform::path_serde")]
111 pub repo_path: PathBuf,
112 #[serde(flatten)]
114 pub repository: crate::repository::Identity,
115 #[serde(with = "crate::platform::path_serde")]
116 pub state_dir: PathBuf,
117 pub status: RunStatus,
118 pub created_at: Timestamp,
119 pub completed_at: Option<Timestamp>,
120 pub selection: crate::project::selection::Selection,
121 #[serde(default)]
122 pub profile: Option<crate::project::selection::ProfileSelection>,
123 #[serde(default)]
124 pub definitions: super::definitions::Definitions,
125 #[serde(default)]
126 pub recursive: bool,
127 #[serde(default)]
128 pub force: bool,
129 #[serde(default)]
130 pub ignore_gates: bool,
131 #[serde(default = "default_jobs")]
132 pub jobs: usize,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub fingerprint_jobs: Option<usize>,
136 #[serde(default)]
137 pub max_executions: Option<u64>,
138 #[serde(default)]
139 pub executions_started: u64,
140 #[serde(default, with = "super::wait_timeout")]
141 pub wait_timeout_ms: Option<Duration>,
142 #[serde(default)]
143 pub wait_timed_out: bool,
144 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
146 pub reuse_only: BTreeSet<ProfileKind>,
147 pub validation: super::Validation,
148 pub error: Option<String>,
149 #[serde(default, skip_serializing_if = "Vec::is_empty")]
151 pub stopped_backends: Vec<StoppedBackend>,
152 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
155 pub evidence: BTreeMap<crate::types::EvalId, crate::types::RequestStatus>,
156}
157
158impl TryFrom<RunWire> for Run {
159 type Error = String;
160 fn try_from(wire: RunWire) -> Result<Self, Self::Error> {
161 let state = super::RunState::from_parts(wire.status, wire.completed_at, wire.error)?;
162 Ok(Self {
163 state,
164 id: wire.id,
165 repo_path: wire.repo_path,
166 repository: wire.repository,
167 state_dir: wire.state_dir,
168 created_at: wire.created_at,
169 selection: wire.selection,
170 profile: wire.profile,
171 definitions: wire.definitions,
172 recursive: wire.recursive,
173 force: wire.force,
174 ignore_gates: wire.ignore_gates,
175 jobs: wire.jobs,
176 fingerprint_jobs: wire.fingerprint_jobs,
177 max_executions: wire.max_executions,
178 executions_started: wire.executions_started,
179 wait_timeout_ms: wire.wait_timeout_ms,
180 wait_timed_out: wire.wait_timed_out,
181 reuse_only: wire.reuse_only,
182 validation: wire.validation,
183 stopped_backends: wire.stopped_backends,
184 evidence: wire.evidence,
185 })
186 }
187}
188impl From<Run> for RunWire {
189 fn from(record: Run) -> Self {
190 Self {
191 status: record.status(),
192 completed_at: record.completed_at(),
193 error: record.error().map(str::to_owned),
194 id: record.id,
195 repo_path: record.repo_path,
196 repository: record.repository,
197 state_dir: record.state_dir,
198 created_at: record.created_at,
199 selection: record.selection,
200 profile: record.profile,
201 definitions: record.definitions,
202 recursive: record.recursive,
203 force: record.force,
204 ignore_gates: record.ignore_gates,
205 jobs: record.jobs,
206 fingerprint_jobs: record.fingerprint_jobs,
207 max_executions: record.max_executions,
208 executions_started: record.executions_started,
209 wait_timeout_ms: record.wait_timeout_ms,
210 wait_timed_out: record.wait_timed_out,
211 reuse_only: record.reuse_only,
212 validation: record.validation,
213 stopped_backends: record.stopped_backends,
214 evidence: record.evidence,
215 }
216 }
217}
218impl Run {
219 pub fn status(&self) -> RunStatus {
220 self.state.status()
221 }
222 pub fn completed_at(&self) -> Option<Timestamp> {
223 self.state.completed_at()
224 }
225 pub fn error(&self) -> Option<&str> {
226 self.state.error()
227 }
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
233#[serde(rename_all = "camelCase")]
234pub struct StoppedBackend {
235 pub backend: crate::config::Backend,
236 pub error_code: crate::types::BackendStopCode,
237 pub eval_id: crate::types::EvalId,
238 pub request_id: RequestId,
239 pub error: String,
240}
241
242fn default_jobs() -> usize {
243 crate::project::DEFAULT_JOBS
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
247#[serde(try_from = "RequestWire", into = "RequestWire")]
248pub struct Request {
249 pub state: super::RequestState,
250 pub id: RequestId,
251 pub run_id: RunId,
252 pub eval_id: crate::types::EvalId,
253 pub target: crate::types::ArtifactName,
254 pub title: String,
255 pub profile: crate::config::StoredProfile,
256 pub requested_profile: crate::config::StoredProfile,
257 pub eval_def_hash: crate::types::DefinitionHash,
258 pub options: ExecutionOptions,
260 pub execution_id: Option<crate::types::ExecutionId>,
261 pub provenance: Option<Provenance>,
262 pub usage: Option<Vec<crate::llm::Attempt>>,
264 pub reused_usage: Option<Vec<crate::llm::Attempt>>,
266 pub joined: bool,
269 pub producer: Option<Producer>,
271 pub reviewer: Option<crate::types::ReviewerId>,
273 pub origin: Option<Origin>,
275 pub session_id: Option<SessionId>,
279 pub session: Option<crate::agent::session::SessionRef>,
282 pub human_definition: Option<super::HumanDefinition>,
283 pub payload: crate::config::StoredPayload,
284 pub references: BTreeMap<crate::config::ReferenceName, crate::types::ArtifactName>,
285 pub deps: Vec<crate::types::ArtifactName>,
286 pub force: bool,
287 pub fingerprint: Option<Fingerprint>,
289 pub key: Option<ReuseKey>,
291 pub fingerprints: std::collections::BTreeMap<crate::types::ArtifactName, Fingerprint>,
293 pub created_at: Timestamp,
294 pub started_at: Option<Timestamp>,
295 pub cwd: PathBuf,
296 pub run_dir: Option<PathBuf>,
297 pub argv: Option<Vec<String>>,
298 pub child: Option<super::ChildIdentity>,
299 pub blocked_reason: Option<String>,
300 pub blocked_by: Vec<super::Blocker>,
302 pub queue: Option<QueueReason>,
304}
305
306#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
308#[serde(tag = "kind", rename_all = "camelCase")]
309pub enum QueueReason {
310 Slot { backend: crate::config::Backend },
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
315#[serde(rename_all = "camelCase")]
316pub(crate) struct RequestWire {
317 pub id: RequestId,
318 pub run_id: RunId,
319 pub eval_id: crate::types::EvalId,
320 pub target: crate::types::ArtifactName,
321 pub title: String,
322 pub profile: crate::config::StoredProfile,
323 pub requested_profile: crate::config::StoredProfile,
324 pub eval_def_hash: crate::types::DefinitionHash,
325 #[serde(default)]
327 pub options: ExecutionOptions,
328 pub execution_id: Option<crate::types::ExecutionId>,
329 pub provenance: Option<Provenance>,
330 pub usage: Option<Vec<crate::llm::Attempt>>,
332 #[serde(default, skip_serializing_if = "Option::is_none")]
334 pub reused_usage: Option<Vec<crate::llm::Attempt>>,
335 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
338 pub joined: bool,
339 #[serde(default, skip_serializing_if = "Option::is_none")]
341 pub producer: Option<Producer>,
342 #[serde(default, skip_serializing_if = "Option::is_none")]
344 pub reviewer: Option<crate::types::ReviewerId>,
345 #[serde(default, skip_serializing_if = "Option::is_none")]
347 pub origin: Option<Origin>,
348 #[serde(default, skip_serializing_if = "Option::is_none")]
352 pub session_id: Option<SessionId>,
353 #[serde(default, skip_serializing_if = "Option::is_none")]
356 pub session: Option<crate::agent::session::SessionRef>,
357 #[serde(default, skip_serializing_if = "Option::is_none")]
358 pub human_definition: Option<super::HumanDefinition>,
359 pub payload: crate::config::StoredPayload,
360 pub references: BTreeMap<crate::config::ReferenceName, crate::types::ArtifactName>,
361 pub deps: Vec<crate::types::ArtifactName>,
362 #[serde(default)]
363 pub force: bool,
364 #[serde(default)]
366 pub fingerprint: Option<Fingerprint>,
367 #[serde(default)]
369 pub key: Option<ReuseKey>,
370 #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
372 pub fingerprints: std::collections::BTreeMap<crate::types::ArtifactName, Fingerprint>,
373 pub status: RequestStatus,
374 pub created_at: Timestamp,
375 pub started_at: Option<Timestamp>,
376 pub completed_at: Option<Timestamp>,
377 #[serde(with = "crate::platform::path_serde")]
378 pub cwd: PathBuf,
379 #[serde(default, with = "crate::platform::path_serde::option")]
380 pub run_dir: Option<PathBuf>,
381 pub argv: Option<Vec<String>>,
382 pub child: Option<super::ChildIdentity>,
383 pub result: Option<super::ExecutionResult>,
384 pub error: Option<String>,
385 pub error_code: Option<crate::types::FailureCode>,
386 pub blocked_reason: Option<String>,
387 #[serde(default, skip_serializing_if = "Vec::is_empty")]
389 pub blocked_by: Vec<super::Blocker>,
390 #[serde(default, skip_serializing_if = "Option::is_none")]
392 pub queue: Option<QueueReason>,
393}
394
395impl TryFrom<RequestWire> for Request {
396 type Error = String;
397 fn try_from(wire: RequestWire) -> Result<Self, Self::Error> {
398 let state = super::RequestState::from_parts(
399 wire.status,
400 wire.result,
401 wire.error,
402 wire.error_code,
403 wire.completed_at,
404 )?;
405 Ok(Self {
406 state,
407 id: wire.id,
408 run_id: wire.run_id,
409 eval_id: wire.eval_id,
410 target: wire.target,
411 title: wire.title,
412 profile: wire.profile,
413 requested_profile: wire.requested_profile,
414 eval_def_hash: wire.eval_def_hash,
415 options: wire.options,
416 execution_id: wire.execution_id,
417 provenance: wire.provenance,
418 usage: wire.usage,
419 reused_usage: wire.reused_usage,
420 joined: wire.joined,
421 producer: wire.producer,
422 reviewer: wire.reviewer,
423 origin: wire.origin,
424 session_id: wire.session_id,
425 session: wire.session,
426 human_definition: wire.human_definition,
427 payload: wire.payload,
428 references: wire.references,
429 deps: wire.deps,
430 force: wire.force,
431 fingerprint: wire.fingerprint,
432 key: wire.key,
433 fingerprints: wire.fingerprints,
434 created_at: wire.created_at,
435 started_at: wire.started_at,
436 cwd: wire.cwd,
437 run_dir: wire.run_dir,
438 argv: wire.argv,
439 child: wire.child,
440 blocked_reason: wire.blocked_reason,
441 blocked_by: wire.blocked_by,
442 queue: wire.queue,
443 })
444 }
445}
446impl From<Request> for RequestWire {
447 fn from(record: Request) -> Self {
448 Self {
449 status: record.status(),
450 result: record.result().cloned(),
451 error: record.error().map(str::to_owned),
452 error_code: record.error_code(),
453 completed_at: record.completed_at(),
454 id: record.id,
455 run_id: record.run_id,
456 eval_id: record.eval_id,
457 target: record.target,
458 title: record.title,
459 profile: record.profile,
460 requested_profile: record.requested_profile,
461 eval_def_hash: record.eval_def_hash,
462 options: record.options,
463 execution_id: record.execution_id,
464 provenance: record.provenance,
465 usage: record.usage,
466 reused_usage: record.reused_usage,
467 joined: record.joined,
468 producer: record.producer,
469 reviewer: record.reviewer,
470 origin: record.origin,
471 session_id: record.session_id,
472 session: record.session,
473 human_definition: record.human_definition,
474 payload: record.payload,
475 references: record.references,
476 deps: record.deps,
477 force: record.force,
478 fingerprint: record.fingerprint,
479 key: record.key,
480 fingerprints: record.fingerprints,
481 created_at: record.created_at,
482 started_at: record.started_at,
483 cwd: record.cwd,
484 run_dir: record.run_dir,
485 argv: record.argv,
486 child: record.child,
487 blocked_reason: record.blocked_reason,
488 blocked_by: record.blocked_by,
489 queue: record.queue,
490 }
491 }
492}
493impl Request {
494 pub fn status(&self) -> crate::types::RequestStatus {
495 self.state.status()
496 }
497 pub fn result(&self) -> Option<&super::ExecutionResult> {
498 self.state.result()
499 }
500 pub fn error(&self) -> Option<&str> {
501 self.state.error()
502 }
503 pub fn error_code(&self) -> Option<crate::types::FailureCode> {
504 self.state.error_code()
505 }
506 pub fn completed_at(&self) -> Option<crate::types::Timestamp> {
507 self.state.completed_at()
508 }
509}
510
511#[derive(Debug, Serialize, Deserialize)]
512pub struct RunView {
513 #[serde(flatten)]
514 pub run: Run,
515 pub requests: Vec<Request>,
516 #[serde(default, skip_serializing_if = "Vec::is_empty")]
517 pub unreadable: Vec<super::Unreadable>,
518}
519
520#[derive(Clone)]
521pub struct Receipts {
522 pub(super) connection: NotifyingConnection,
523}
524
525#[derive(Clone)]
529pub(crate) struct NotifyingConnection {
530 inner: Connection,
531 publisher: crate::changes::Publisher,
532}
533impl NotifyingConnection {
534 pub(crate) fn new(inner: Connection, state: &Path) -> Self {
535 Self {
536 inner,
537 publisher: crate::changes::Publisher::new(state),
538 }
539 }
540 pub(crate) async fn call<F, R, E>(&self, function: F) -> Result<R, tokio_rusqlite::Error<E>>
541 where
542 F: FnOnce(&mut rusqlite::Connection) -> Result<R, E> + Send + 'static,
543 R: Send + 'static,
544 E: Send + 'static,
545 {
546 let publisher = self.publisher.clone();
547 self.inner
548 .call(move |db| {
549 let before = db.total_changes();
550 let result = function(db);
551 if db.is_autocommit() && db.total_changes() != before {
552 publisher.notify(crate::changes::Change::StateInvalidated);
553 }
554 result
555 })
556 .await
557 }
558}
559
560impl Receipts {
561 pub async fn open(state: &Path, repo: &Path) -> Result<Self, String> {
562 let state = prepare_directory(state, repo).map_err(|e| e.to_string())?;
563 check_files(&state)?;
564 let connection = Connection::open(state.join(DATABASE))
565 .await
566 .map_err(|e| e.to_string())?;
567 let connection = NotifyingConnection::new(connection, &state);
568 let deadline = tokio::time::Instant::now() + super::SQLITE_BUSY_TIMEOUT;
570 let state_id: crate::types::StateId = crate::agent::uuid()?.parse()?;
571 loop {
572 let state_id = state_id.clone();
573 let initialized = connection
574 .call(move |db| -> Result<(), Error> {
575 db.busy_timeout(super::SQLITE_BUSY_TIMEOUT)?;
576 db.pragma_update(None, "foreign_keys", true)?;
577 let version: u32 =
578 db.pragma_query_value(None, "user_version", |row| row.get(0))?;
579 if version != 0 && version != STATE_SCHEMA_VERSION {
580 return Err(Error::Invalid(schema_error(version)));
581 }
582 db.pragma_update(None, "journal_mode", "WAL")?;
583 let transaction =
584 db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
585 if !schema_initialized(&transaction)? {
588 transaction.execute_batch(SCHEMA)?;
589 transaction.execute(
590 "INSERT INTO state_meta(name,value) VALUES ('id',?)",
591 [&state_id],
592 )?;
593 transaction.pragma_update(None, "user_version", STATE_SCHEMA_VERSION)?;
594 }
595 transaction.commit()?;
596 Ok(())
597 })
598 .await;
599 match initialized {
600 Ok(()) => break,
601 Err(tokio_rusqlite::Error::Error(Error::Sql(rusqlite::Error::SqliteFailure(
602 error,
603 _,
604 )))) if error.code == rusqlite::ErrorCode::DatabaseBusy
605 && tokio::time::Instant::now() < deadline =>
606 {
607 tokio::time::sleep(INITIALIZATION_RETRY_INTERVAL).await;
609 }
610 Err(error) => return Err(error.to_string()),
611 }
612 }
613 Ok(Self { connection })
614 }
615
616 pub async fn state_id(&self) -> Result<crate::types::StateId, String> {
618 self.connection
619 .call(|db| -> Result<crate::types::StateId, Error> {
620 Ok(
621 db.query_row("SELECT value FROM state_meta WHERE name='id'", [], |row| {
622 row.get(0)
623 })?,
624 )
625 })
626 .await
627 .map_err(|e| e.to_string())
628 }
629
630 pub async fn create_run(&self, run: &Run, requests: &[Request]) -> Result<(), String> {
631 for request in requests {
632 if request.run_id != run.id {
633 return Err("Request belongs to another Run.".into());
634 }
635 }
636 let run = run.clone();
637 let requests = requests.to_vec();
638 self.connection
639 .call(move |db| -> Result<(), Error> {
640 let transaction = db.transaction()?;
641 transaction.execute(
642 "INSERT INTO runs(id,repo,status,data) VALUES (?,?,?,?)",
643 params![
644 run.id,
645 crate::platform::path_text(&run.repo_path),
646 run.status(),
647 serde_json::to_string(&run)?
648 ],
649 )?;
650 for (ordinal, request) in requests.iter().enumerate() {
651 transaction.execute(
652 "INSERT INTO requests(id,run_id,eval_id,ordinal,status,data) VALUES (?,?,?,?,?,?)",
653 params![
654 request.id,
655 run.id,
656 request.eval_id,
657 ordinal as i64,
658 request.status(),
659 serde_json::to_string(request)?
660 ],
661 )?;
662 }
663 transaction.commit()?;
664 Ok(())
665 })
666 .await
667 .map_err(|e| e.to_string())
668 }
669
670 pub async fn save_request(&self, request: &Request) -> Result<(), String> {
671 let request = request.clone();
672 self.connection
673 .call(move |db| update_request(db, &request))
674 .await
675 .map_err(|e| e.to_string())
676 }
677
678 pub async fn save_run(&self, run: &Run) -> Result<(), String> {
679 let run = run.clone();
680 self.connection
681 .call(move |db| -> Result<(), Error> {
682 db.execute(
683 "UPDATE runs SET status=?,data=? WHERE id=?",
684 params![run.status(), serde_json::to_string(&run)?, run.id],
685 )?;
686 Ok(())
687 })
688 .await
689 .map_err(|e| e.to_string())
690 }
691
692 pub async fn finish(&self, run: &Run, requests: &[Request]) -> Result<(), String> {
693 let run = run.clone();
694 let requests = requests.to_vec();
695 self.connection
696 .call(move |db| -> Result<(), Error> {
697 let transaction = db.transaction()?;
698 for request in requests {
699 if request.status() == crate::types::RequestStatus::WaitingHuman {
701 continue;
702 }
703 update_request(&transaction, &request)?;
704 }
705 transaction.execute(
706 "UPDATE runs SET status=?,data=? WHERE id=?",
707 params![run.status(), serde_json::to_string(&run)?, run.id],
708 )?;
709 transaction.commit()?;
710 Ok(())
711 })
712 .await
713 .map_err(|e| e.to_string())
714 }
715}
716
717pub(super) fn update_request(db: &rusqlite::Connection, request: &Request) -> Result<(), Error> {
718 if db.execute(
719 "UPDATE requests SET status=?,data=?,execution_id=? WHERE id=?",
720 params![
721 request.status(),
722 serde_json::to_string(request)?,
723 request.execution_id,
724 request.id
725 ],
726 )? != 1
727 {
728 return Err(Error::Invalid("Review request not found.".into()));
729 }
730 Ok(())
731}
732
733pub(super) fn schema_initialized(transaction: &rusqlite::Transaction<'_>) -> Result<bool, Error> {
734 let version: u32 = transaction.pragma_query_value(None, "user_version", |row| row.get(0))?;
735 if version == 0
737 && !transaction.query_row("SELECT EXISTS(SELECT 1 FROM sqlite_master)", [], |row| {
738 row.get::<_, bool>(0)
739 })?
740 {
741 return Ok(false);
742 }
743 if version != STATE_SCHEMA_VERSION {
744 return Err(Error::Invalid(schema_error(version)));
745 }
746 Ok(true)
747}
748
749pub(super) fn check_files(state: &Path) -> Result<(), String> {
751 match state_schema(state)? {
752 Some(version) if version != 0 && version != STATE_SCHEMA_VERSION => {
753 Err(schema_error(version))
754 }
755 _ => Ok(()),
756 }
757}
758
759pub fn state_schema(state: &Path) -> Result<Option<u32>, String> {
762 let state = canonical_target(state).map_err(|e| e.to_string())?;
763 regular_files(&state)?;
764 let database = state.join(DATABASE);
765 if !database.try_exists().map_err(|e| e.to_string())? {
766 return Ok(None);
767 }
768 let read = || -> Result<u32, rusqlite::Error> {
769 let db = rusqlite::Connection::open_with_flags(
770 &database,
771 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
772 )?;
773 db.busy_timeout(super::SQLITE_BUSY_TIMEOUT)?;
774 db.pragma_query_value(None, "user_version", |row| row.get(0))
775 };
776 read().map(Some).map_err(|e| e.to_string())
777}
778
779pub fn read_state_id(state: &Path) -> Result<Option<crate::types::StateId>, String> {
782 let state = canonical_target(state).map_err(|e| e.to_string())?;
783 check_files(&state)?;
784 let database = state.join(DATABASE);
785 if !database.try_exists().map_err(|e| e.to_string())? {
786 return Ok(None);
787 }
788 let read = || -> Result<Option<crate::types::StateId>, Error> {
789 let mut db = rusqlite::Connection::open_with_flags(
790 &database,
791 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
792 )?;
793 db.busy_timeout(super::SQLITE_BUSY_TIMEOUT)?;
794 let transaction = db.transaction()?;
795 if !schema_initialized(&transaction)? {
796 return Ok(None);
797 }
798 Ok(transaction
799 .query_row("SELECT value FROM state_meta WHERE name='id'", [], |row| {
800 row.get(0)
801 })
802 .optional()?)
803 };
804 read().map_err(|e| e.to_string())
805}
806
807pub(crate) fn regular_files(state: &Path) -> Result<(), String> {
808 regular_files_with_retry(state, || {
809 std::thread::sleep(super::TRANSIENT_ACCESS_BACKOFF);
810 })
811}
812
813pub(super) fn regular_files_with_retry(
816 state: &Path,
817 mut before_retry: impl FnMut(),
818) -> Result<(), String> {
819 for suffix in ["", "-wal", "-shm"] {
820 let path = state.join(format!("{DATABASE}{suffix}"));
821 match super::retry_transient_access(|| crate::platform::path_kind(&path), &mut before_retry)
822 {
823 Ok(kind) if kind != crate::platform::FileKind::File => {
824 return Err(format!(
825 "State files must be regular files: {}",
826 crate::platform::path_text(&path)
827 ));
828 }
829 Err(error) if error.kind() != std::io::ErrorKind::NotFound => {
830 return Err(error.to_string());
831 }
832 _ => {}
833 }
834 }
835 Ok(())
836}
837
838#[derive(Debug, Serialize)]
839#[serde(rename_all = "camelCase")]
840pub struct LastRequest {
841 pub run_id: RunId,
842 pub verdict: RequestStatus,
843 #[serde(skip_serializing_if = "Option::is_none")]
844 pub fingerprint: Option<Fingerprint>,
845}
846
847pub async fn read_latest_requests(
849 state: &Path,
850 repo: &Path,
851) -> Result<std::collections::BTreeMap<crate::types::EvalId, LastRequest>, String> {
852 let state = canonical_target(state).map_err(|e| e.to_string())?;
853 outside_workspace(repo, &state).map_err(|e| e.to_string())?;
854 check_files(&state)?;
855 if !state
856 .join(DATABASE)
857 .try_exists()
858 .map_err(|e| e.to_string())?
859 {
860 return Ok(Default::default());
861 }
862 let connection = Connection::open_with_flags(
863 state.join(DATABASE),
864 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
865 )
866 .await
867 .map_err(|e| e.to_string())?;
868 let repo = repo.to_path_buf();
869 connection
870 .call(move |db| -> Result<_, Error> {
871 db.busy_timeout(super::SQLITE_BUSY_TIMEOUT)?;
872 let transaction = db.transaction()?;
873 if !schema_initialized(&transaction)? {
874 return Ok(Default::default());
875 }
876 let latest = {
877 let mut statement = transaction.prepare(
878 "SELECT data,run_data FROM (
879 SELECT q.eval_id,q.data,r.data AS run_data,
880 row_number() OVER (PARTITION BY q.eval_id ORDER BY r.rowid DESC) AS rank
881 FROM requests q JOIN runs r ON r.id=q.run_id
882 WHERE r.repo=?
883 ) WHERE rank=1 ORDER BY eval_id",
884 )?;
885 statement
886 .query_map([crate::platform::path_text(&repo)], |row| {
887 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
888 })?
889 .filter_map(|row| match row {
890 Err(error) => Some(Err(error)),
891 Ok((data, run_data)) => {
892 super::unreadable::evidence::<Run>("run", &run_data)
893 .and_then(|_| {
894 super::unreadable::evidence::<Request>("request", &data)
895 })
896 .map(|request| {
897 Ok((
898 request.eval_id.clone(),
899 LastRequest {
900 run_id: request.run_id.clone(),
901 verdict: request.status(),
902 fingerprint: request.fingerprint,
903 },
904 ))
905 })
906 }
907 })
908 .collect::<Result<_, _>>()?
909 };
910 transaction.commit()?;
911 Ok(latest)
912 })
913 .await
914 .map_err(|e| e.to_string())
915}
916
917pub async fn read_run(state: &Path, id: &str) -> Result<RunView, String> {
919 let state = canonical_target(state).map_err(|e| e.to_string())?;
920 check_files(&state)?;
921 let connection = Connection::open_with_flags(
922 state.join(DATABASE),
923 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
924 )
925 .await
926 .map_err(|e| e.to_string())?;
927 let id: RunId = id.parse()?;
928 connection
929 .call(move |db| -> Result<RunView, Error> {
930 db.busy_timeout(super::SQLITE_BUSY_TIMEOUT)?;
931 let transaction = db.transaction()?;
932 if !schema_initialized(&transaction)? {
933 return Err(Error::Invalid("Run not found.".into()));
934 }
935 let saved: Option<(String, String)> = transaction
936 .query_row("SELECT repo,data FROM runs WHERE id=?", [&id], |row| {
937 Ok((row.get(0)?, row.get(1)?))
938 })
939 .optional()?;
940 let (repo, data) = saved.ok_or_else(|| Error::Invalid("Run not found.".into()))?;
941 outside_workspace(Path::new(&repo), &state)
942 .map_err(|e| Error::Invalid(e.to_string()))?;
943 let run = super::unreadable::decode("run", &id, &data)
944 .map_err(|error| Error::Invalid(error.to_string()))?;
945 let requests = {
946 let mut statement = transaction
947 .prepare("SELECT data FROM requests WHERE run_id=? ORDER BY ordinal")?;
948 statement
949 .query_map([&id], |row| row.get::<_, String>(0))?
950 .filter_map(|row| match row {
951 Ok(data) => super::unreadable::evidence("request", &data).map(Ok),
952 Err(error) => Some(Err(Error::Sql(error))),
953 })
954 .collect::<Result<Vec<Request>, Error>>()?
955 };
956 let unreadable = super::unreadable::in_run(&transaction, &id)?;
957 transaction.commit()?;
958 Ok(RunView {
959 run,
960 requests,
961 unreadable,
962 })
963 })
964 .await
965 .map_err(|e| e.to_string())
966}