artifactize_core/runs/
watch.rs1use super::{Context, Finding, RunObservation, RunResultPage};
3use crate::{
4 store,
5 types::{RequestId, RequestStatus, RunId},
6};
7use serde::Serialize;
8use std::{collections::BTreeMap, future::Future, time::Duration};
9use tokio_util::sync::CancellationToken;
10
11#[derive(Debug, Serialize)]
12#[serde(tag = "kind", content = "data", rename_all = "snake_case")]
13pub enum RunEvent {
14 Update(RunObservation),
16 EvalResult(Finding),
18 Outcome(RunResultPage),
20}
21
22pub async fn watch<F, R>(
26 context: &Context,
27 id: &RunId,
28 bound: Option<Duration>,
29 cancellation: CancellationToken,
30 mut emit: F,
31) -> Result<RunObservation, String>
32where
33 F: FnMut(RunEvent) -> R,
34 R: Future<Output = Result<(), String>>,
35{
36 let mut subscription = None;
37 let mut last = None;
38 let observing = async {
39 let changes = subscription.insert(crate::changes::Subscription::new(&context.state).await);
40 let mut seen: BTreeMap<RequestId, RequestStatus> = BTreeMap::new();
41 let mut previous = None;
42 loop {
43 let current = super::status(context, id).await?;
46 let signature = (current.state, current.counts.clone());
48 if previous.as_ref() != Some(&signature) {
49 emit(RunEvent::Update(current.clone())).await?;
50 previous = Some(signature);
51 }
52 for request in store::read_new_results(&context.state, id, seen.clone()).await? {
53 if (request.result().is_some() || request.error().is_some())
54 && seen.get(&request.id) != Some(&request.status())
55 {
56 emit(RunEvent::EvalResult(super::result::finding(&request))).await?;
57 seen.insert(request.id.clone(), request.status());
58 }
59 }
60 let ended = current.state.is_terminal();
61 last = Some(current);
62 if ended {
63 emit(RunEvent::Outcome(super::result(context, id, None).await?)).await?;
64 return Ok::<(), String>(());
65 }
66 changes.next_reconciled().await;
67 }
68 };
69 let result = {
70 tokio::pin!(observing);
71 tokio::select! {
72 result = &mut observing => result,
73 _ = cancellation.cancelled() => Err("Run observation was cancelled.".into()),
74 _ = async { match bound { Some(bound) => tokio::time::sleep(bound).await, None => std::future::pending().await } } => Ok(()),
75 }
76 };
77 let closed = match subscription {
78 Some(subscription) => subscription.close().await,
79 None => Ok(()),
80 };
81 result?;
82 closed?;
83 last.ok_or_else(|| "State unavailable within the observation bound.".into())
84}