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;
28pub use lifecycle::{ExecutionState, GreenResult, PendingRequest, RequestState, RunState};
29pub use result::{ExecutionResult, Field as ResultField, RuntimeResult};
30pub use snapshots::{
31    ArtifactValidation, Blocker, ChildIdentity, EvalValidation, HumanDefinition, Validation,
32    ValidationSnapshot,
33};
34mod wait_timeout;
35pub use catalog::{CatalogRun, Signoff, read_catalog};
36pub use runs::{RunSummary, read_runs, read_scoped_runs};
37pub(crate) mod reader;
38mod requests;
39pub(crate) use executions::read_profiled_executions;
40pub use executions::{
41    Capacity, Claim, Execution, ExecutionOptions, Origin, Producer, Provenance,
42    read_keyed_executions, read_latest_cached,
43};
44#[cfg(any(test, feature = "test-support"))]
45pub use receipts::OwnedReceipts;
46pub(crate) use receipts::regular_files as check_probe_files;
47pub use receipts::{
48    DATABASE, EARLIER_STATE, LastRequest, QueueReason, Receipts, Request, Run, RunView,
49    StoppedBackend, read_latest_requests, read_run, read_state_id, schema_error, state_schema,
50};
51pub use requests::read_request_repo;
52pub use requests::{
53    RequestView, read_original, read_request, read_request_batch, read_requests, read_requests_in,
54    read_run_requests, read_session_request, read_waiting, read_waiting_originals,
55};
56
57/// Let concurrent readers/writers finish short SQLite transactions without an
58/// immediate busy error, while limiting how long one database operation can block.
59pub(crate) const SQLITE_BUSY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
60
61/// Allow four short backoffs for a pending-delete handle to close, bounding the extra
62/// latency of a real file-access failure to 80ms. This is a retry policy, not a test deadline.
63const TRANSIENT_ACCESS_RETRIES: u32 = 5;
64const TRANSIENT_ACCESS_BACKOFF: std::time::Duration = std::time::Duration::from_millis(20);
65
66/// Retry `attempt` while `transient` says its error can clear on its own, before giving up
67/// with its last error. `transient` decides alone: a non-transient error, or the bound run out,
68/// both stop the loop and return that error immediately, never retried further.
69fn retry<T, E>(
70    mut attempt: impl FnMut() -> Result<T, E>,
71    transient: impl Fn(&E) -> bool,
72    mut before_retry: impl FnMut(),
73) -> Result<T, E> {
74    for remaining in (0..TRANSIENT_ACCESS_RETRIES).rev() {
75        match attempt() {
76            Ok(value) => return Ok(value),
77            Err(error) if remaining > 0 && transient(&error) => {
78                before_retry();
79            }
80            Err(error) => return Err(error),
81        }
82    }
83    unreachable!("the loop above always returns by its last attempt")
84}
85
86/// Retry `attempt` past [`platform::transient_file_access`] (Windows transiently denying a
87/// metadata or open probe while another handle finishes closing a just-deleted WAL sidecar)
88/// before giving up with its last error. Every read-only state open reaches this through
89/// [`receipts::check_files`]/[`check_probe_files`], which every reader calls first, so this is
90/// the one place the retry needs to live.
91pub(crate) fn retry_transient_access<T>(
92    attempt: impl FnMut() -> std::io::Result<T>,
93    before_retry: impl FnMut(),
94) -> std::io::Result<T> {
95    retry(attempt, platform::transient_file_access, before_retry)
96}
97
98#[cfg(test)]
99mod retry_tests {
100    use std::{cell::Cell, io};
101
102    #[test]
103    fn file_probe_returns_success_and_permanent_errors_without_backoff() {
104        let calls = Cell::new(0);
105        let result = super::retry_transient_access(
106            || {
107                calls.set(calls.get() + 1);
108                Ok(7)
109            },
110            || panic!("successful probes need no backoff"),
111        );
112        assert_eq!(result.unwrap(), 7);
113        assert_eq!(calls.get(), 1);
114
115        let calls = Cell::new(0);
116        let result = super::retry_transient_access(
117            || {
118                calls.set(calls.get() + 1);
119                Err::<(), _>(io::Error::other("permanent probe failure"))
120            },
121            || panic!("permanent errors cannot be retried"),
122        );
123        assert_eq!(result.unwrap_err().to_string(), "permanent probe failure");
124        assert_eq!(calls.get(), 1);
125    }
126}
127
128/// Resolve the single state directory without creating it.
129pub fn state_dir(explicit: Option<&Path>) -> Result<PathBuf, String> {
130    let path = match explicit {
131        Some(path) => path.to_path_buf(),
132        None => state_home().map_err(|e| e.to_string())?,
133    };
134    canonical_target(&path).map_err(|e| e.to_string())
135}
136
137/// SQLite `user_version`; schema 9 keeps a Run's definitions in their own row, written once,
138/// and numbers every change of a Run, request or execution row. Versions from schema 6 upgrade
139/// additively.
140pub const STATE_SCHEMA_VERSION: u32 = 9;
141
142#[derive(Debug, thiserror::Error)]
143#[error("cannot resolve state home: set {}", state_home_variables())]
144pub struct StateHomeError;
145
146/// The variables `state_home` reads, in order, for the error that names them.
147fn state_home_variables() -> String {
148    let names: Vec<&str> = std::iter::once("ARTIFACTIZE_STATE_HOME")
149        .chain(platform::STATE_VARIABLES.iter().copied())
150        .chain([platform::HOME_VARIABLE])
151        .collect();
152    let (last, rest) = names.split_last().expect("constant names");
153    format!("{}, or {last}", rest.join(", "))
154}
155
156/// Resolve the default state home without creating directories; empty variables are ignored.
157/// The platform's state variables come after `ARTIFACTIZE_STATE_HOME` and before the home.
158pub fn state_home() -> Result<PathBuf, StateHomeError> {
159    platform::state_directory().ok_or(StateHomeError)
160}
161
162#[cfg(test)]
163#[path = "store/file_tests.rs"]
164mod file_tests;