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