1use crate::types::{ExecutionStatus, Fingerprint, RequestId, ReuseKey, RunId};
2use std::{collections::BTreeMap, path::PathBuf};
3
4use rusqlite::{OptionalExtension, params};
5use serde::{Deserialize, Serialize};
6
7use super::{
8 Receipts, Request, history,
9 receipts::{Error, schema_initialized, update_request},
10};
11use crate::{config::Profile, process, runtime::Verdict};
12
13pub enum Claim {
14 Owned,
15 BudgetExhausted,
16 Full,
18 Reuse(Box<Execution>),
19 Wait(crate::types::ExecutionId),
20 WaitHuman(crate::types::ExecutionId),
21}
22
23pub struct Capacity {
25 pub limit: u32,
26 pub stopped: Box<dyn FnOnce() -> bool + Send>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub struct Provenance {
35 #[serde(with = "crate::platform::path_serde")]
36 pub repo_path: PathBuf,
37 pub run_id: RunId,
38 pub request_id: RequestId,
39 pub eval_id: crate::types::EvalId,
40 pub eval_def_hash: crate::types::DefinitionHash,
41 pub completed_at: Option<crate::types::Timestamp>,
42 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
45 pub execution_paths: crate::tools::pins::Pins,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(try_from = "ExecutionWire", into = "ExecutionWire")]
52pub struct Execution {
53 pub state: super::ExecutionState,
54 pub id: crate::types::ExecutionId,
55 pub key: Option<ReuseKey>,
57 pub fingerprint: Option<Fingerprint>,
59 pub fingerprints: BTreeMap<crate::types::ArtifactName, Fingerprint>,
61 pub artifact_kinds: BTreeMap<crate::types::ArtifactName, crate::config::ArtifactKind>,
63 pub eval_def_hash: crate::types::DefinitionHash,
64 pub owner: Option<process::ChildIdentity>,
67 pub profile: crate::config::StoredProfile,
68 pub options: ExecutionOptions,
70 pub usage: Option<Vec<crate::llm::Attempt>>,
71 pub provenance: Provenance,
72 pub started_at: crate::types::Timestamp,
73 pub producer: Option<Producer>,
74 pub reviewer: Option<crate::types::ReviewerId>,
76 pub origin: Option<Origin>,
78 pub manifest: Option<crate::cache::Manifest>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub(crate) struct ExecutionWire {
84 pub id: crate::types::ExecutionId,
85 #[serde(default)]
87 pub key: Option<ReuseKey>,
88 pub fingerprint: Option<Fingerprint>,
90 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
92 pub fingerprints: BTreeMap<crate::types::ArtifactName, Fingerprint>,
93 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
95 pub artifact_kinds: BTreeMap<crate::types::ArtifactName, crate::config::ArtifactKind>,
96 pub eval_def_hash: crate::types::DefinitionHash,
97 pub owner_pid: u32,
98 pub owner_start_time: u64,
99 pub status: ExecutionStatus,
100 pub result: Option<super::ExecutionResult>,
101 pub error: Option<String>,
102 pub error_code: Option<crate::types::FailureCode>,
103 pub profile: crate::config::StoredProfile,
104 #[serde(default)]
106 pub options: ExecutionOptions,
107 pub usage: Option<Vec<crate::llm::Attempt>>,
108 pub provenance: Provenance,
109 pub started_at: crate::types::Timestamp,
110 pub completed_at: Option<crate::types::Timestamp>,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub producer: Option<Producer>,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub reviewer: Option<crate::types::ReviewerId>,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub origin: Option<Origin>,
119 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub manifest: Option<crate::cache::Manifest>,
121}
122
123impl TryFrom<ExecutionWire> for Execution {
124 type Error = String;
125 fn try_from(wire: ExecutionWire) -> Result<Self, Self::Error> {
126 let state = super::ExecutionState::from_parts(
127 wire.status,
128 wire.result,
129 wire.error,
130 wire.error_code,
131 wire.completed_at,
132 )?;
133 Ok(Self {
134 state,
135 id: wire.id,
136 key: wire.key,
137 fingerprint: wire.fingerprint,
138 fingerprints: wire.fingerprints,
139 artifact_kinds: wire.artifact_kinds,
140 eval_def_hash: wire.eval_def_hash,
141 owner: std::num::NonZeroU32::new(wire.owner_pid).map(|pid| process::ChildIdentity {
142 pid: process::ProcessId::from(pid),
143 start_time: wire.owner_start_time,
144 }),
145 profile: wire.profile,
146 options: wire.options,
147 usage: wire.usage,
148 provenance: wire.provenance,
149 started_at: wire.started_at,
150 producer: wire.producer,
151 reviewer: wire.reviewer,
152 origin: wire.origin,
153 manifest: wire.manifest,
154 })
155 }
156}
157impl From<Execution> for ExecutionWire {
158 fn from(record: Execution) -> Self {
159 let owner_pid = record.owner_pid();
160 let owner_start_time = record.owner_start_time();
161 Self {
162 status: record.status(),
163 result: record.result().cloned(),
164 error: record.error().map(str::to_owned),
165 error_code: record.error_code(),
166 completed_at: record.completed_at(),
167 id: record.id,
168 key: record.key,
169 fingerprint: record.fingerprint,
170 fingerprints: record.fingerprints,
171 artifact_kinds: record.artifact_kinds,
172 eval_def_hash: record.eval_def_hash,
173 owner_pid,
174 owner_start_time,
175 profile: record.profile,
176 options: record.options,
177 usage: record.usage,
178 provenance: record.provenance,
179 started_at: record.started_at,
180 producer: record.producer,
181 reviewer: record.reviewer,
182 origin: record.origin,
183 manifest: record.manifest,
184 }
185 }
186}
187impl Execution {
188 pub fn status(&self) -> crate::types::ExecutionStatus {
189 self.state.status()
190 }
191 pub fn result(&self) -> Option<&super::ExecutionResult> {
192 self.state.result()
193 }
194 pub fn error(&self) -> Option<&str> {
195 self.state.error()
196 }
197 pub fn error_code(&self) -> Option<crate::types::FailureCode> {
198 self.state.error_code()
199 }
200 pub fn completed_at(&self) -> Option<crate::types::Timestamp> {
201 self.state.completed_at()
202 }
203 pub fn owner_pid(&self) -> u32 {
207 self.owner.map_or(0, |owner| owner.pid.get())
208 }
209 pub fn owner_start_time(&self) -> u64 {
211 self.owner.map_or(0, |owner| owner.start_time)
212 }
213}
214
215#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
218#[serde(rename_all = "camelCase")]
219pub struct ExecutionOptions {
220 #[serde(default, skip_serializing_if = "Option::is_none")]
221 pub backend: Option<crate::config::Backend>,
222 #[serde(default, skip_serializing_if = "Option::is_none")]
223 pub model: Option<crate::config::ModelId>,
224 #[serde(default, skip_serializing_if = "Option::is_none")]
225 pub reasoning: Option<crate::config::Reasoning>,
226 #[serde(default, skip_serializing_if = "Option::is_none")]
227 #[serde(with = "crate::config::validation::milliseconds")]
228 pub timeout_ms: Option<std::time::Duration>,
229 #[serde(default, skip_serializing_if = "Option::is_none")]
230 pub max_tool_calls: Option<u64>,
231 #[serde(default, skip_serializing_if = "Option::is_none")]
232 pub max_tokens: Option<u64>,
233 #[serde(default, skip_serializing_if = "Option::is_none")]
235 pub variant: Option<crate::config::ProfileVariantName>,
236}
237
238impl ExecutionOptions {
239 pub fn new(profile: &Profile, variant: Option<&crate::config::ProfileVariantName>) -> Self {
241 let mut options = Self {
242 variant: variant.cloned(),
243 ..Self::default()
244 };
245 match profile {
246 Profile::Agent {
247 backend,
248 model,
249 reasoning,
250 timeout_ms,
251 max_tool_calls,
252 max_tokens,
253 } => {
254 options.backend = Some(*backend);
255 options.model = Some(model.clone());
256 options.reasoning = *reasoning;
257 options.timeout_ms = *timeout_ms;
258 options.max_tool_calls = *max_tool_calls;
259 options.max_tokens = *max_tokens;
260 }
261 Profile::Runtime { timeout_ms, .. } => options.timeout_ms = *timeout_ms,
262 Profile::Human {} | Profile::Dependency { .. } => {}
263 }
264 options
265 }
266}
267
268#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
270pub struct Producer {
271 pub name: crate::types::ProducerId,
273 pub version: String,
275 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub session: Option<crate::agent::session::SessionRef>,
279}
280
281const UNKNOWN_PRODUCER: &str = "unknown@unknown";
283
284impl Producer {
285 pub fn current() -> Self {
286 let user = crate::platform::user_name();
287 let host = crate::platform::host_name();
288 let name = format!(
289 "{}@{}",
290 user.as_deref().unwrap_or("unknown"),
291 host.as_deref()
292 .map(str::trim)
293 .filter(|host| !host.is_empty())
294 .unwrap_or("unknown")
295 );
296 let mut printable: String = name.chars().filter(|c| !c.is_control()).collect();
298 while printable.len() > crate::types::MAX_ID_BYTES {
299 printable.pop();
300 }
301 Self {
302 name: printable
303 .parse()
304 .unwrap_or_else(|_| UNKNOWN_PRODUCER.parse().expect("a valid producer name")),
305 version: env!("CARGO_PKG_VERSION").into(),
306 session: None,
307 }
308 }
309}
310
311#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
313#[serde(rename_all = "camelCase")]
314pub struct Origin {
315 pub store: crate::types::StoreUrl,
316 pub publisher: crate::types::TokenName,
317 pub published_at: crate::types::Timestamp,
318}
319
320impl Execution {
321 fn backend(&self) -> Option<&str> {
324 self.options
325 .backend
326 .as_ref()
327 .map(|backend| backend.as_str())
328 .filter(|_| self.origin.is_none())
329 }
330
331 pub fn completed_after(&self, other: &Execution) -> bool {
333 self.completed_at() > other.completed_at()
334 }
335
336 pub fn verdict(&self) -> Option<Verdict> {
337 match self.status() {
338 ExecutionStatus::Green => Some(Verdict::Green),
339 ExecutionStatus::Red => Some(Verdict::Red),
340 _ => None,
341 }
342 }
343}
344
345pub(super) fn lookup(
347 db: &rusqlite::Connection,
348 key: &ReuseKey,
349) -> Result<Option<Execution>, Error> {
350 let data: Option<String> = db
351 .query_row(
352 &format!(
353 "SELECT e.data FROM executions e WHERE e.key=? AND {} ORDER BY {} LIMIT 1",
354 history::RECORD,
355 history::LATEST
356 ),
357 [key],
358 |row| row.get(0),
359 )
360 .optional()?;
361 Ok(data.and_then(|data| super::unreadable::evidence("execution", &data)))
362}
363
364fn owner_died(
366 db: &rusqlite::Connection,
367 id: &crate::types::ExecutionId,
368 at: crate::types::Timestamp,
369) -> Result<(), Error> {
370 db.execute(
371 "UPDATE executions SET status='ERROR',data=json_set(data,'$.status','ERROR','$.error','Execution owner died.','$.errorCode','OWNER_DIED','$.result',NULL,'$.completedAt',?1,'$.provenance.completedAt',?1) WHERE id=?2 AND status='RUNNING'",
372 params![at, id],
373 )?;
374 Ok(())
375}
376
377fn held_slots(
380 db: &rusqlite::Connection,
381 backend: &str,
382 at: crate::types::Timestamp,
383) -> Result<u32, Error> {
384 let owners = {
385 let mut statement = db.prepare(
386 "SELECT id,owner_pid,owner_start_time FROM executions WHERE backend=? AND status='RUNNING'",
387 )?;
388 statement
389 .query_map([backend], |row| {
390 Ok((
391 row.get::<_, crate::types::ExecutionId>(0)?,
392 process::ChildIdentity {
393 pid: row.get(1)?,
394 start_time: row.get::<_, i64>(2)? as u64,
395 },
396 ))
397 })?
398 .collect::<Result<Vec<_>, _>>()?
399 };
400 let mut held = 0;
401 for (id, owner) in owners {
402 if process::is_alive(Some(owner)).map_err(|e| Error::Invalid(e.to_string()))? {
403 held += 1;
404 } else {
405 owner_died(db, &id, at)?;
406 }
407 }
408 Ok(held)
409}
410
411fn retire_unreadable(
414 db: &rusqlite::Connection,
415 key: Option<&ReuseKey>,
416 backend: Option<&str>,
417) -> Result<(), Error> {
418 let records = {
419 let mut statement = db.prepare("SELECT id,data FROM executions WHERE status IN ('RUNNING','WAITING_HUMAN') AND ((?1 IS NOT NULL AND key=?1) OR (?2 IS NOT NULL AND backend=?2))")?;
420 statement
421 .query_map(params![key, backend], |row| {
422 Ok((
423 row.get::<_, crate::types::ExecutionId>(0)?,
424 row.get::<_, String>(1)?,
425 ))
426 })?
427 .collect::<Result<Vec<_>, _>>()?
428 };
429 for (id, data) in records {
430 if let Err(error) = super::unreadable::decode::<Execution>("execution", id.as_str(), &data)
431 {
432 use std::io::Write;
433 let _ = writeln!(std::io::stderr().lock(), "{error}");
434 db.execute(
435 "UPDATE executions SET status='ERROR',backend=NULL WHERE id=?",
436 [id],
437 )?;
438 }
439 }
440 Ok(())
441}
442
443fn active_owner(
444 db: &rusqlite::Connection,
445 key: &ReuseKey,
446) -> Result<
447 Option<(
448 crate::types::ExecutionId,
449 process::ChildIdentity,
450 ExecutionStatus,
451 )>,
452 Error,
453> {
454 let row = db
455 .query_row(
456 "SELECT id,owner_pid,owner_start_time,status,data FROM executions WHERE key=? AND status IN ('RUNNING','WAITING_HUMAN')",
457 [key],
458 |row| {
459 Ok((
460 row.get::<_, crate::types::ExecutionId>(0)?,
461 process::ChildIdentity {
462 pid: row.get(1)?,
463 start_time: row.get::<_, i64>(2)? as u64,
464 },
465 row.get::<_, ExecutionStatus>(3)?,
466 row.get::<_, String>(4)?,
467 ))
468 },
469 )
470 .optional()?;
471 Ok(row.and_then(|(id, owner, status, data)| {
472 super::unreadable::evidence::<Execution>("execution", &data).map(|_| (id, owner, status))
473 }))
474}
475
476fn available_to_waiter(
477 db: &rusqlite::Connection,
478 key: &ReuseKey,
479 waiting_for: Option<&crate::types::ExecutionId>,
480) -> Result<Option<Claim>, Error> {
481 if let Some(id) = waiting_for {
482 let data: Option<String> = db
483 .query_row(
484 "SELECT data FROM executions WHERE id=? AND key=? AND status IN ('GREEN','RED')",
485 params![id, key],
486 |row| row.get(0),
487 )
488 .optional()?;
489 if let Some(data) = data
490 && let Some(execution) = super::unreadable::evidence("execution", &data)
491 {
492 return Ok(Some(Claim::Reuse(Box::new(execution))));
493 }
494 }
495 available(db, key)
496}
497
498fn available(db: &rusqlite::Connection, key: &ReuseKey) -> Result<Option<Claim>, Error> {
499 if let Some(execution) = lookup(db, key)? {
500 return Ok(Some(Claim::Reuse(Box::new(execution))));
501 }
502 if let Some((id, owner, status)) = active_owner(db, key)? {
503 if status == ExecutionStatus::WaitingHuman {
504 return Ok(Some(Claim::WaitHuman(id)));
505 }
506 if process::is_alive(Some(owner)).map_err(|e| Error::Invalid(e.to_string()))? {
507 return Ok(Some(Claim::Wait(id)));
508 }
509 }
510 Ok(None)
511}
512
513async fn open_read_only(
514 state: &std::path::Path,
515) -> Result<Option<tokio_rusqlite::Connection>, String> {
516 let state = crate::workspace::canonical_target(state).map_err(|e| e.to_string())?;
517 super::receipts::check_files(&state)?;
518 if !state
519 .join(super::DATABASE)
520 .try_exists()
521 .map_err(|e| e.to_string())?
522 {
523 return Ok(None);
524 }
525 tokio_rusqlite::Connection::open_with_flags(
526 state.join(super::DATABASE),
527 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
528 )
529 .await
530 .map(Some)
531 .map_err(|e| e.to_string())
532}
533
534pub async fn read_keyed_executions(
537 state: &std::path::Path,
538 keys: &[ReuseKey],
539) -> Result<std::collections::BTreeMap<ReuseKey, Claim>, String> {
540 if keys.is_empty() {
541 return Ok(Default::default());
542 }
543 let Some(connection) = open_read_only(state).await? else {
544 return Ok(Default::default());
545 };
546 let keys = keys.to_vec();
547 let result = connection
548 .call(move |db| -> Result<_, Error> {
549 db.busy_timeout(super::SQLITE_BUSY_TIMEOUT)?;
550 let transaction = db.transaction()?;
551 if !schema_initialized(&transaction)? {
552 return Ok(Default::default());
553 }
554 let mut entries = std::collections::BTreeMap::new();
555 for key in keys {
556 if let Some(claim) = available(&transaction, &key)? {
557 entries.insert(key, claim);
558 }
559 }
560 transaction.commit()?;
561 Ok(entries)
562 })
563 .await
564 .map_err(|e| e.to_string());
565 super::connection::finish(connection, result).await
566}
567
568pub async fn read_latest_cached(
571 state: &std::path::Path,
572 keys: &[(crate::types::EvalId, crate::types::DefinitionHash)],
573) -> Result<
574 std::collections::BTreeMap<(crate::types::EvalId, crate::types::DefinitionHash), Execution>,
575 String,
576> {
577 if keys.is_empty() {
578 return Ok(Default::default());
579 }
580 let Some(connection) = open_read_only(state).await? else {
581 return Ok(Default::default());
582 };
583 let keys = keys.to_vec();
584 let result = connection
585 .call(move |db| -> Result<_, Error> {
586 db.busy_timeout(super::SQLITE_BUSY_TIMEOUT)?;
587 let transaction = db.transaction()?;
588 if !schema_initialized(&transaction)? {
589 return Ok(Default::default());
590 }
591 let mut latest = std::collections::BTreeMap::new();
592 {
593 let mut statement = transaction.prepare(&format!(
594 "SELECT e.data FROM executions e WHERE e.eval_def_hash=? AND json_extract(e.data,'$.provenance.evalId')=? AND {} ORDER BY {} LIMIT 1",
595 history::RECORD,
596 history::LATEST
597 ))?;
598 for (eval_id, eval_def_hash) in keys {
599 let data: Option<String> = statement
600 .query_row(params![eval_def_hash, eval_id], |row| row.get(0))
601 .optional()?;
602 if let Some(data) = data
603 && let Some(execution) = super::unreadable::evidence("execution", &data)
604 {
605 latest.insert((eval_id, eval_def_hash), execution);
606 }
607 }
608 }
609 transaction.commit()?;
610 Ok(latest)
611 })
612 .await
613 .map_err(|e| e.to_string());
614 super::connection::finish(connection, result).await
615}
616
617impl Receipts {
618 pub async fn cached_execution(&self, key: &str) -> Result<Option<Execution>, String> {
620 let key: ReuseKey = key.parse()?;
621 self.connection
622 .call(move |db| -> Result<_, Error> {
623 let execution = lookup(db, &key)?;
624 if let Some(execution) = &execution {
625 history::touch(db, &execution.id, crate::broker::now())?;
626 }
627 Ok(execution)
628 })
629 .await
630 .map_err(|e| e.to_string())
631 }
632
633 pub async fn published_execution(
636 &self,
637 execution_id: &str,
638 ) -> Result<Option<Execution>, String> {
639 let execution_id: crate::types::ExecutionId = execution_id.parse()?;
640 self.connection
641 .call(move |db| -> Result<_, Error> {
642 let data: Option<String> = db
643 .query_row(
644 &format!(
645 "SELECT e.data FROM executions e LEFT JOIN runs r ON r.id=json_extract(e.data,'$.provenance.runId') WHERE e.id=? AND {} AND (r.id IS NULL OR r.status!='RUNNING' OR coalesce(json_extract(r.data,'$.modes.speculate'),0)=0 OR json_extract(r.data,'$.control.activity')='publishing')",
646 history::RECORD
647 ),
648 [execution_id],
649 |row| row.get(0),
650 )
651 .optional()?;
652 Ok(data
653 .and_then(|data| super::unreadable::evidence::<Execution>("execution", &data))
654 .filter(|execution| execution.origin.is_none()))
655 })
656 .await
657 .map_err(|e| e.to_string())
658 }
659
660 pub async fn claim_execution(
667 &self,
668 execution: &Execution,
669 keyed: bool,
670 waiting_for: Option<&crate::types::ExecutionId>,
671 allow_start: bool,
672 capacity: Option<Capacity>,
673 ) -> Result<Claim, String> {
674 let execution = execution.clone();
675 let waiting_for = waiting_for.cloned();
676 self.connection
677 .call(move |db| -> Result<Claim, Error> {
678 let key = execution.key.as_ref().filter(|_| keyed);
679 if let Some(key) = key {
680 let transaction = db.transaction()?;
681 if let Some(claim) =
682 available_to_waiter(&transaction, key, waiting_for.as_ref())?
683 {
684 return Ok(claim);
685 }
686 }
687 if !allow_start && key.is_none() {
688 return Ok(Claim::BudgetExhausted);
689 }
690 let capacity = capacity.zip(execution.backend());
691 if key.is_none() && capacity.is_none() {
692 return Ok(Claim::Owned);
693 }
694 let transaction =
695 db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
696 if let Some(key) = key
697 && let Some(claim) =
698 available_to_waiter(&transaction, key, waiting_for.as_ref())?
699 {
700 return Ok(claim);
701 }
702 if !allow_start {
706 return Ok(Claim::BudgetExhausted);
707 }
708 retire_unreadable(&transaction, key, capacity.as_ref().map(|(_, backend)| *backend))?;
709 if let Some((capacity, backend)) = capacity {
710 if (capacity.stopped)() {
711 return Ok(Claim::BudgetExhausted);
712 }
713 if held_slots(&transaction, backend, execution.started_at)? >= capacity.limit {
714 transaction.commit()?;
715 return Ok(Claim::Full);
716 }
717 }
718 if let Some(key) = key
719 && let Some((id, _, _)) = active_owner(&transaction, key)?
720 {
721 owner_died(&transaction, &id, execution.started_at)?;
722 }
723 let inserted = transaction.execute(
724 "INSERT INTO executions(id,key,eval_def_hash,status,owner_pid,owner_start_time,backend,data) VALUES (?,?,?,?,?,?,?,?) ON CONFLICT(key) WHERE key IS NOT NULL AND status IN ('RUNNING','WAITING_HUMAN') DO NOTHING",
725 params![
726 execution.id,
727 key,
728 execution.eval_def_hash,
729 execution.status(),
730 execution.owner_pid(),
731 execution.owner_start_time() as i64,
732 execution.backend(),
733 serde_json::to_string(&execution)?
734 ],
735 )?;
736 let claim = match key {
737 Some(key) if inserted == 0 => Claim::Wait(
738 active_owner(&transaction, key)?
739 .expect("conflicting active key")
740 .0,
741 ),
742 _ => Claim::Owned,
743 };
744 transaction.commit()?;
745 Ok(claim)
746 })
747 .await
748 .map_err(|e| e.to_string())
749 }
750
751 pub async fn reuse_execution(&self, request: &Request) -> Result<(), String> {
752 let request = request.clone();
753 self.connection
754 .call(move |db| -> Result<(), Error> {
755 let transaction = db.transaction()?;
756 update_request(&transaction, &request)?;
757 if let (Some(execution), Some(at)) = (&request.execution_id, request.completed_at())
758 {
759 history::touch(&transaction, execution, at)?;
760 }
761 transaction.commit()?;
762 Ok(())
763 })
764 .await
765 .map_err(|e| e.to_string())
766 }
767
768 pub async fn complete_execution(
769 &self,
770 execution: &Execution,
771 request: &Request,
772 ) -> Result<(), String> {
773 let execution = execution.clone();
774 let request = request.clone();
775 self.connection
776 .call(move |db| -> Result<(), Error> {
777 let transaction =
778 db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
779 let published = settle(&transaction, &execution, &request)?;
780 transaction.commit()?;
781 if published {
782 history::collect_after(db);
783 }
784 Ok(())
785 })
786 .await
787 .map_err(|e| e.to_string())
788 }
789
790 pub async fn mirror_execution(
795 &self,
796 execution: &Execution,
797 ) -> Result<Option<Execution>, String> {
798 let execution = execution.clone();
799 self.connection
800 .call(move |db| -> Result<Option<Execution>, Error> {
801 let (Some(key), Some(_), Some(_)) =
802 (&execution.key, &execution.origin, execution.verdict())
803 else {
804 return Err(Error::Invalid(
805 "Only completed remote GREEN/RED results can be mirrored.".into(),
806 ));
807 };
808 let data = serde_json::to_string(&execution)?;
809 if data.len() > history::MAX_ENTRY_BYTES {
810 return Err(Error::Invalid(
811 "Remote result exceeds the cache entry limit.".into(),
812 ));
813 }
814 let transaction =
815 db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
816 if lookup(&transaction, key)?
817 .is_some_and(|local| !execution.completed_after(&local))
818 {
819 transaction.commit()?;
820 return Ok(None);
821 }
822 transaction.execute(
824 "INSERT INTO executions(id,key,eval_def_hash,status,owner_pid,owner_start_time,data) VALUES (?,?,?,?,?,?,?) ON CONFLICT(id) DO NOTHING",
825 params![
826 execution.id,
827 key,
828 execution.eval_def_hash,
829 execution.status(),
830 execution.owner_pid(),
831 execution.owner_start_time() as i64,
832 data
833 ],
834 )?;
835 let data: String = transaction
836 .query_row(
837 "SELECT data FROM executions WHERE id=? AND key=? AND status=?",
838 params![execution.id, key, execution.status()],
839 |row| row.get(0),
840 )
841 .optional()?
842 .ok_or_else(|| {
843 Error::Invalid(
844 "Mirrored execution ID conflicts with another execution.".into(),
845 )
846 })?;
847 let Some(mirrored) = super::unreadable::evidence::<Execution>("execution", &data) else {
848 transaction.commit()?;
849 return Ok(None);
850 };
851 if let Some((completed_at, bytes)) = history::columns(&mirrored, data.len())? {
852 transaction.execute(
853 "UPDATE executions SET completed_at=?,bytes=?,last_used=? WHERE id=? AND completed_at IS NULL",
854 params![completed_at, bytes, crate::broker::now(), mirrored.id],
855 )?;
856 }
857 super::human::settle_waiting(&transaction, &mirrored)?;
858 transaction.commit()?;
859 history::collect_after(db);
860 Ok(Some(mirrored))
861 })
862 .await
863 .map_err(|e| e.to_string())
864 }
865}
866
867pub(super) fn settle(
871 db: &rusqlite::Connection,
872 execution: &Execution,
873 request: &Request,
874) -> Result<bool, Error> {
875 if request.status() != execution.status().into()
876 || request.execution_id.as_ref() != Some(&execution.id)
877 || request.result() != execution.result()
878 || request.error() != execution.error()
879 || request.error_code() != execution.error_code()
880 {
881 return Err(Error::Invalid(
882 "Execution and request settlement disagree.".into(),
883 ));
884 }
885 let data = serde_json::to_string(execution)?;
886 let record = history::columns(execution, data.len())?;
887 let last_used = record.as_ref().and(execution.completed_at());
888 let (completed_at, bytes) = record.unzip();
889 let claimed = execution.key.is_some() && !request.force;
890 if claimed || request.human_definition.is_some() {
891 if db.execute(
892 "UPDATE executions SET key=?,status=?,completed_at=?,bytes=?,last_used=?,data=? WHERE id=? AND owner_pid=? AND owner_start_time=? AND status IN ('RUNNING','WAITING_HUMAN')",
893 params![
894 execution.key,
895 execution.status(),
896 completed_at,
897 bytes,
898 last_used,
899 data,
900 execution.id,
901 execution.owner_pid(),
902 execution.owner_start_time() as i64
903 ],
904 )? != 1
905 {
906 return Err(Error::Invalid("Active execution not found.".into()));
907 }
908 } else {
909 db.execute(
910 "INSERT INTO executions(id,key,eval_def_hash,status,owner_pid,owner_start_time,backend,completed_at,bytes,last_used,data) VALUES (?,?,?,?,?,?,?,?,?,?,?)
911 ON CONFLICT(id) DO UPDATE SET key=excluded.key,status=excluded.status,completed_at=excluded.completed_at,bytes=excluded.bytes,last_used=excluded.last_used,data=excluded.data",
912 params![
913 execution.id,
914 execution.key,
915 execution.eval_def_hash,
916 execution.status(),
917 execution.owner_pid(),
918 execution.owner_start_time() as i64,
919 execution.backend(),
920 completed_at,
921 bytes,
922 last_used,
923 data
924 ],
925 )?;
926 }
927 update_request(db, request)?;
928 Ok(record.is_some())
929}