Skip to main content

artifactize_core/
store.rs

1//! The state database and storage maintenance.
2
3use std::path::{Path, PathBuf};
4
5use crate::{platform, workspace::canonical_target};
6
7mod blocked;
8pub mod definitions;
9mod executions;
10pub(crate) mod history;
11mod human;
12pub use human::HumanClaim;
13mod catalog;
14pub(crate) mod connection;
15pub mod prune;
16pub(crate) mod receipts;
17mod result;
18mod run_record;
19mod runs;
20mod snapshots;
21mod touched;
22mod unreadable;
23mod validation;
24pub use unreadable::{Unreadable, read_unreadable};
25mod lifecycle;
26pub(crate) mod migrations;
27pub(crate) mod run_control;
28mod run_read;
29pub use lifecycle::{ExecutionState, GreenResult, PendingRequest, RequestState, RunState};
30pub use result::{ExecutionResult, Field as ResultField, RuntimeResult};
31pub use run_control::read_run_repo;
32pub use run_read::read_run;
33pub(crate) use run_read::{read_new_results, read_run_record, read_run_requests_only};
34pub use snapshots::{
35    ArtifactValidation, Blocker, ChildIdentity, EvalValidation, HumanDefinition, Validation,
36    ValidationSnapshot,
37};
38mod wait_timeout;
39pub use catalog::{CatalogRun, Signoff, read_catalog};
40pub use runs::{RunSummary, read_runs, read_scoped_runs};
41pub(crate) mod reader;
42mod removed;
43pub use removed::retire_removed_signoffs;
44mod requests;
45pub(crate) use executions::read_profiled_executions;
46pub use executions::{
47    Capacity, Claim, Execution, ExecutionOptions, Origin, Producer, Provenance,
48    read_keyed_executions, read_latest_cached,
49};
50#[cfg(any(test, feature = "test-support"))]
51pub use receipts::OwnedReceipts;
52pub(crate) use receipts::regular_files as check_probe_files;
53pub use receipts::{
54    DATABASE, EARLIER_STATE, LastRequest, QueueReason, Receipts, Request, Run, RunView,
55    StoppedBackend, read_latest_requests, read_state_id, schema_error, state_schema,
56};
57pub use requests::read_request_repo;
58pub use requests::{
59    RequestView, Shown, read_original, read_original_as, read_request, read_request_as,
60    read_request_batch, read_requests, read_requests_in, read_run_requests, read_session_request,
61    read_waiting, read_waiting_originals,
62};
63
64/// How long one database operation waits for the state other processes are writing before it
65/// fails. Several Runs, the monitor and MCP servers share one state; a Run's write waits behind
66/// theirs rather than failing the Run, while a lock that never frees still ends in an error.
67pub(crate) const SQLITE_BUSY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
68
69/// How often a busy operation tries again. SQLite's own busy timeout backs off to 100 ms
70/// between tries, so under steady writes from other connections, which try every 1–2 ms when
71/// they start waiting, a long waiter rarely finds the lock free and runs out its time while
72/// the others commit (#372). Trying at this pace keeps every waiter in the race.
73const SQLITE_BUSY_POLL: std::time::Duration = std::time::Duration::from_millis(1);
74
75/// Make `db` wait for a busy database, trying every [`SQLITE_BUSY_POLL`] for up to
76/// [`SQLITE_BUSY_TIMEOUT`].
77pub(crate) fn wait_when_busy(db: &rusqlite::Connection) -> rusqlite::Result<()> {
78    db.busy_handler(Some(busy))
79}
80
81/// SQLite's busy handler: `tries` counts the earlier calls for this one wait, which runs on
82/// the calling thread, so the wait's start is kept per thread.
83fn busy(tries: i32) -> bool {
84    thread_local! {
85        static SINCE: std::cell::Cell<std::time::Instant> = std::cell::Cell::new(std::time::Instant::now());
86    }
87    let now = std::time::Instant::now();
88    if tries == 0 {
89        SINCE.set(now);
90    }
91    if now.duration_since(SINCE.get()) >= SQLITE_BUSY_TIMEOUT {
92        return false;
93    }
94    std::thread::sleep(SQLITE_BUSY_POLL);
95    true
96}
97
98/// Allow four short backoffs for a pending-delete handle to close, bounding the extra
99/// latency of a real file-access failure to 80ms. This is a retry policy, not a test deadline.
100const TRANSIENT_ACCESS_RETRIES: u32 = 5;
101const TRANSIENT_ACCESS_BACKOFF: std::time::Duration = std::time::Duration::from_millis(20);
102
103/// Retry `attempt` while `transient` says its error can clear on its own, before giving up
104/// with its last error. `transient` decides alone: a non-transient error, or the bound run out,
105/// both stop the loop and return that error immediately, never retried further.
106fn retry<T, E>(
107    mut attempt: impl FnMut() -> Result<T, E>,
108    transient: impl Fn(&E) -> bool,
109    mut before_retry: impl FnMut(),
110) -> Result<T, E> {
111    for remaining in (0..TRANSIENT_ACCESS_RETRIES).rev() {
112        match attempt() {
113            Ok(value) => return Ok(value),
114            Err(error) if remaining > 0 && transient(&error) => {
115                before_retry();
116            }
117            Err(error) => return Err(error),
118        }
119    }
120    unreachable!("the loop above always returns by its last attempt")
121}
122
123/// Retry `attempt` past [`platform::transient_file_access`] (Windows transiently denying a
124/// metadata or open probe while another handle finishes closing a just-deleted WAL sidecar)
125/// before giving up with its last error. Every read-only state open reaches this through
126/// [`receipts::check_files`]/[`check_probe_files`], which every reader calls first, so this is
127/// the one place the retry needs to live.
128pub(crate) fn retry_transient_access<T>(
129    attempt: impl FnMut() -> std::io::Result<T>,
130    before_retry: impl FnMut(),
131) -> std::io::Result<T> {
132    retry(attempt, platform::transient_file_access, before_retry)
133}
134
135#[cfg(test)]
136mod retry_tests {
137    use std::{cell::Cell, io};
138
139    #[test]
140    fn file_probe_returns_success_and_permanent_errors_without_backoff() {
141        let calls = Cell::new(0);
142        let result = super::retry_transient_access(
143            || {
144                calls.set(calls.get() + 1);
145                Ok(7)
146            },
147            || panic!("successful probes need no backoff"),
148        );
149        assert_eq!(result.unwrap(), 7);
150        assert_eq!(calls.get(), 1);
151
152        let calls = Cell::new(0);
153        let result = super::retry_transient_access(
154            || {
155                calls.set(calls.get() + 1);
156                Err::<(), _>(io::Error::other("permanent probe failure"))
157            },
158            || panic!("permanent errors cannot be retried"),
159        );
160        assert_eq!(result.unwrap_err().to_string(), "permanent probe failure");
161        assert_eq!(calls.get(), 1);
162    }
163}
164
165/// Resolve the single state directory without creating it.
166pub fn state_dir(explicit: Option<&Path>) -> Result<PathBuf, String> {
167    let path = match explicit {
168        Some(path) => path.to_path_buf(),
169        None => state_home().map_err(|e| e.to_string())?,
170    };
171    canonical_target(&path).map_err(|e| e.to_string())
172}
173
174/// SQLite `user_version`; schema 9 keeps a Run's definitions in their own row, written once,
175/// and numbers every change of a Run, request or execution row; schema 10 adds the open
176/// WAITING_HUMAN Run and names a Run that ended with only sign-offs left YELLOW. Versions from
177/// schema 6 upgrade additively.
178pub const STATE_SCHEMA_VERSION: u32 = 10;
179
180#[derive(Debug, thiserror::Error)]
181#[error("cannot resolve state home: set {}", state_home_variables())]
182pub struct StateHomeError;
183
184/// The variables `state_home` reads, in order, for the error that names them.
185fn state_home_variables() -> String {
186    let names: Vec<&str> = std::iter::once("ARTIFACTIZE_STATE_HOME")
187        .chain(platform::STATE_VARIABLES.iter().copied())
188        .chain([platform::HOME_VARIABLE])
189        .collect();
190    let (last, rest) = names.split_last().expect("constant names");
191    format!("{}, or {last}", rest.join(", "))
192}
193
194/// Resolve the default state home without creating directories; empty variables are ignored.
195/// The platform's state variables come after `ARTIFACTIZE_STATE_HOME` and before the home.
196pub fn state_home() -> Result<PathBuf, StateHomeError> {
197    platform::state_directory().ok_or(StateHomeError)
198}
199
200#[cfg(test)]
201#[path = "store/file_tests.rs"]
202mod file_tests;