1use std::path::Path;
6
7use rusqlite::{OpenFlags, OptionalExtension, params};
8use serde::Serialize;
9use tokio_rusqlite::Connection;
10
11use super::{
12 DATABASE, Execution,
13 receipts::{Error, NotifyingConnection, schema_initialized},
14};
15
16pub const MAX_ENTRIES: i64 = 10_000;
18pub const MAX_BYTES: i64 = 1024 * 1024 * 1024;
20pub const MAX_ENTRY_BYTES: usize = 16 * 1024 * 1024;
22
23pub(super) const RECORD: &str =
25 "e.completed_at IS NOT NULL AND e.status IN ('GREEN','RED') AND e.key IS NOT NULL";
26
27pub(super) const LATEST: &str = "e.completed_at DESC, e.rowid DESC";
29
30#[derive(Debug, Serialize)]
32#[serde(rename_all = "camelCase")]
33pub struct Entry {
34 pub key: crate::types::ReuseKey,
35 pub eval_def_hash: crate::types::DefinitionHash,
36 pub execution_id: crate::types::ExecutionId,
37 pub verdict: crate::types::ExecutionStatus,
38 #[serde(with = "crate::platform::path_serde")]
39 pub repo_path: std::path::PathBuf,
40 pub eval_id: crate::types::EvalId,
41 pub fingerprint: Option<crate::types::Fingerprint>,
43 pub completed_at: Option<crate::types::Timestamp>,
44 pub producer: Option<crate::types::ProducerId>,
46 pub variant: Option<crate::config::ProfileVariantName>,
48 pub origin: Option<crate::types::StoreUrl>,
50 pub records: i64,
52 pub bytes: i64,
53 pub last_used: crate::types::Timestamp,
54}
55
56async fn open(state: &Path, writable: bool) -> Result<Option<NotifyingConnection>, String> {
57 let state = crate::workspace::canonical_target(state).map_err(|e| e.to_string())?;
58 super::receipts::check_files(&state)?;
59 let path = state.join(DATABASE);
60 if !path.try_exists().map_err(|e| e.to_string())? {
61 return Ok(None);
62 }
63 let connection = Connection::open_with_flags(
64 path,
65 if writable {
66 OpenFlags::SQLITE_OPEN_READ_WRITE
67 } else {
68 OpenFlags::SQLITE_OPEN_READ_ONLY
69 },
70 )
71 .await
72 .map_err(|e| e.to_string())?;
73 let connection = NotifyingConnection::new(connection, &state);
74 let initialized = connection
75 .call(|db| -> Result<bool, Error> {
76 db.busy_timeout(super::SQLITE_BUSY_TIMEOUT)?;
77 let transaction = db.transaction()?;
78 schema_initialized(&transaction)
79 })
80 .await
81 .map_err(|e| e.to_string());
82 match initialized {
83 Ok(true) => Ok(Some(connection)),
84 result => connection.finish(result.map(|_| None)).await,
85 }
86}
87
88pub async fn list(state: &Path, history: bool) -> Result<Vec<Entry>, String> {
90 let Some(connection) = open(state, false).await? else {
91 return Ok(Vec::new());
92 };
93 let result = connection
94 .call(move |db| -> Result<_, Error> {
95 let mut statement = db.prepare(&format!(
96 "SELECT * FROM (SELECT e.key,e.eval_def_hash,e.id,e.status,json_extract(e.data,'$.provenance.repoPath'),json_extract(e.data,'$.provenance.evalId'),json_extract(e.data,'$.fingerprint'),json_extract(e.data,'$.completedAt'),json_extract(e.data,'$.origin.store'),count(*) OVER (PARTITION BY e.key),e.bytes,e.last_used,row_number() OVER (PARTITION BY e.key ORDER BY {LATEST}) AS rank,e.data FROM executions e WHERE {RECORD}) WHERE ?1 OR rank=1 ORDER BY 1,rank"
97 ))?;
98 Ok(statement
99 .query_map([history], |row| {
100 let id: crate::types::ExecutionId = row.get(2)?;
101 let data: String = row.get(13)?;
102 let Some(execution) =
103 super::unreadable::evidence::<Execution>("execution", &data)
104 else {
105 return Ok(None);
106 };
107 Ok(Some(Entry {
108 key: row.get(0)?,
109 eval_def_hash: row.get(1)?,
110 execution_id: id,
111 verdict: row.get(3)?,
112 repo_path: row.get::<_, String>(4)?.into(),
113 eval_id: row.get(5)?,
114 fingerprint: row.get(6)?,
115 completed_at: row.get(7)?,
116 producer: execution.producer.map(|producer| producer.name),
117 variant: execution.options.variant,
118 origin: row.get(8)?,
119 records: row.get(9)?,
120 bytes: row.get(10)?,
121 last_used: row.get(11)?,
122 }))
123 })?
124 .filter_map(|row| row.transpose())
125 .collect::<Result<_, _>>()?)
126 })
127 .await
128 .map_err(|e| e.to_string());
129 connection.finish(result).await
130}
131
132pub async fn show(state: &Path, key: &str, history: bool) -> Result<Vec<Execution>, String> {
134 let Some(connection) = open(state, false).await? else {
135 return Ok(Vec::new());
136 };
137 let Ok(key) = key.parse::<crate::types::ReuseKey>() else {
138 return connection.finish(Ok(Vec::new())).await;
139 };
140 let result = connection
141 .call(move |db| -> Result<_, Error> {
142 let mut statement = db.prepare(&format!(
143 "SELECT e.data FROM executions e WHERE e.key=? AND {RECORD} ORDER BY {LATEST} LIMIT ?"
144 ))?;
145 statement
146 .query_map(params![key, if history { -1 } else { 1 }], |row| {
147 row.get::<_, String>(0)
148 })?
149 .filter_map(|row| match row {
150 Ok(data) => super::unreadable::evidence("execution", &data).map(Ok),
151 Err(error) => Some(Err(Error::Sql(error))),
152 })
153 .collect()
154 })
155 .await
156 .map_err(|e| e.to_string());
157 connection.finish(result).await
158}
159
160pub async fn local(
162 state: &Path,
163 after: Option<crate::types::ReuseKey>,
164 limit: usize,
165) -> Result<Vec<Execution>, String> {
166 let Some(connection) = open(state, false).await? else {
167 return Ok(Vec::new());
168 };
169 let result = connection
170 .call(move |db| -> Result<_, Error> {
171 let mut statement = db.prepare(&format!(
172 "SELECT data FROM (SELECT e.key,e.data,row_number() OVER (PARTITION BY e.key ORDER BY {LATEST}) AS rank FROM executions e WHERE {RECORD} AND json_extract(e.data,'$.origin') IS NULL AND e.key>?) WHERE rank=1 ORDER BY key LIMIT ?"
173 ))?;
174 statement
175 .query_map(
176 params![
177 after.as_ref().map_or("", crate::types::ReuseKey::as_str),
178 limit as i64
179 ],
180 |row| row.get::<_, String>(0),
181 )?
182 .filter_map(|row| match row {
183 Ok(data) => super::unreadable::evidence("execution", &data).map(Ok),
184 Err(error) => Some(Err(Error::Sql(error))),
185 })
186 .collect()
187 })
188 .await
189 .map_err(|e| e.to_string());
190 connection.finish(result).await
191}
192
193pub async fn remove(state: &Path, key: &str) -> Result<bool, String> {
195 let Some(connection) = open(state, true).await? else {
196 return Ok(false);
197 };
198 let Ok(key) = key.parse::<crate::types::ReuseKey>() else {
199 return connection.finish(Ok(false)).await;
200 };
201 let result = connection
202 .call(move |db| -> Result<bool, Error> {
203 let transaction =
204 db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
205 let in_use: bool = transaction.query_row(
206 "SELECT EXISTS(SELECT 1 FROM executions WHERE key=?1 AND status IN ('RUNNING','WAITING_HUMAN')) OR EXISTS(SELECT 1 FROM requests q JOIN executions e ON e.id=q.execution_id WHERE e.key=?1 AND e.completed_at IS NOT NULL AND q.status IN ('QUEUED','RUNNING','WAITING_HUMAN'))",
207 [&key],
208 |row| row.get(0),
209 )?;
210 if in_use {
211 return Err(Error::Invalid(format!(
212 "Key {key} is in use by an active execution or waiter."
213 )));
214 }
215 let removed = transaction.execute(
216 "UPDATE executions SET completed_at=NULL,bytes=NULL,last_used=NULL WHERE key=? AND completed_at IS NOT NULL",
217 [&key],
218 )?;
219 transaction.commit()?;
220 Ok(removed != 0)
221 })
222 .await
223 .map_err(|e| e.to_string());
224 connection.finish(result).await
225}
226
227pub(super) fn columns(
231 execution: &Execution,
232 bytes: usize,
233) -> Result<Option<(crate::types::Timestamp, i64)>, Error> {
234 let (Some(_), Some(_), Some(completed_at)) = (
235 &execution.key,
236 execution.verdict(),
237 &execution.completed_at(),
238 ) else {
239 return Ok(None);
240 };
241 if bytes > MAX_ENTRY_BYTES {
242 return Ok(None);
243 }
244 Ok(Some((*completed_at, bytes as i64)))
245}
246
247pub(super) fn touch(
249 db: &rusqlite::Connection,
250 execution_id: &crate::types::ExecutionId,
251 at: crate::types::Timestamp,
252) -> Result<(), Error> {
253 db.execute(
254 "UPDATE executions SET last_used=? WHERE id=? AND completed_at IS NOT NULL",
255 params![at, execution_id],
256 )?;
257 Ok(())
258}
259
260pub(super) fn collect_after(db: &mut rusqlite::Connection) {
262 if let Err(error) = collect(db) {
263 use std::io::Write;
264 let _ = writeln!(std::io::stderr().lock(), "Cache GC failed: {error}");
265 }
266}
267
268fn collect(db: &mut rusqlite::Connection) -> Result<(), Error> {
272 loop {
273 let transaction = db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
275 let (entries, bytes): (i64, i64) = transaction.query_row(
276 "SELECT count(*),coalesce(sum(bytes),0) FROM executions WHERE completed_at IS NOT NULL",
277 [],
278 |row| Ok((row.get(0)?, row.get(1)?)),
279 )?;
280 let over_capacity = entries > MAX_ENTRIES || bytes > MAX_BYTES;
281 let candidate: Option<crate::types::ExecutionId> = transaction
282 .query_row(
283 "SELECT e.id FROM executions e WHERE e.completed_at IS NOT NULL AND (? OR e.bytes>?)
284 AND NOT EXISTS(SELECT 1 FROM executions a WHERE a.key=e.key AND a.status IN ('RUNNING','WAITING_HUMAN'))
285 AND NOT EXISTS(SELECT 1 FROM requests q WHERE q.execution_id=e.id AND q.status IN ('QUEUED','RUNNING','WAITING_HUMAN'))
286 ORDER BY e.last_used,e.key,e.completed_at LIMIT 1",
287 params![over_capacity, MAX_ENTRY_BYTES as i64],
288 |row| row.get(0),
289 )
290 .optional()?;
291 let Some(execution_id) = candidate else {
292 return Ok(transaction.commit()?);
293 };
294 transaction.execute(
295 "UPDATE executions SET completed_at=NULL,bytes=NULL,last_used=NULL WHERE id=?",
296 [execution_id],
297 )?;
298 transaction.commit()?;
299 }
300}