Skip to main content

artifactize_core/store/
human.rs

1use rusqlite::{OptionalExtension, params};
2use serde::{Deserialize, Serialize};
3
4use super::{
5    Execution, Receipts, Request, executions,
6    receipts::{Error, update_request},
7};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub struct HumanClaim {
12    pub request_id: crate::types::RequestId,
13    pub reviewer: crate::types::ReviewerId,
14    pub claimed_at: crate::types::Timestamp,
15}
16
17fn waiting(db: &rusqlite::Connection, id: &str) -> Result<(Request, Execution), Error> {
18    let data: Option<String> = db
19        .query_row(
20            "SELECT e.data FROM requests q JOIN executions e ON e.id=q.execution_id WHERE q.id=? AND q.status IN ('WAITING_HUMAN','DEFERRED') AND e.status='WAITING_HUMAN'",
21            [id],
22            |row| row.get(0),
23        )
24        .optional()?;
25    let data =
26        data.ok_or_else(|| Error::Invalid("Request is not waiting for a Human review.".into()))?;
27    let execution: Execution = super::unreadable::evidence("execution", &data)
28        .ok_or_else(|| Error::Invalid(format!("Request {id} has an unreadable execution.")))?;
29    let data: String = db.query_row(
30        "SELECT data FROM requests WHERE id=? AND status IN ('WAITING_HUMAN','DEFERRED')",
31        [&execution.provenance.request_id],
32        |row| row.get(0),
33    )?;
34    let request: Request =
35        super::unreadable::decode("request", execution.provenance.request_id.as_str(), &data)
36            .map_err(|error| Error::Invalid(error.to_string()))?;
37    if request.profile.kind() != crate::config::ProfileKind::Human
38        || request.human_definition.is_none()
39    {
40        return Err(Error::Invalid(
41            "Request has no recorded Human definition.".into(),
42        ));
43    }
44    Ok((request, execution))
45}
46
47fn claim(db: &rusqlite::Connection, request: &str) -> Result<HumanClaim, Error> {
48    Ok(db.query_row(
49        "SELECT id,claimed_by,claimed_at FROM requests WHERE id=? AND claimed_by IS NOT NULL",
50        [request],
51        |row| {
52            Ok(HumanClaim {
53                request_id: row.get(0)?,
54                reviewer: row.get(1)?,
55                claimed_at: row.get(2)?,
56            })
57        },
58    )?)
59}
60
61/// Clear a request's Human claim.
62fn unclaim(db: &rusqlite::Connection, request: &str) -> Result<(), Error> {
63    db.execute(
64        "UPDATE requests SET claimed_by=NULL,claimed_at=NULL WHERE id=?",
65        [request],
66    )?;
67    Ok(())
68}
69
70fn claimant(
71    db: &rusqlite::Connection,
72    request: &str,
73    reviewer: &crate::types::ReviewerId,
74) -> Result<(), Error> {
75    let owner: Option<crate::types::ReviewerId> = db
76        .query_row(
77            "SELECT claimed_by FROM requests WHERE id=?",
78            [request],
79            |row| row.get(0),
80        )
81        .optional()?
82        .flatten();
83    if owner.as_ref() != Some(reviewer) {
84        return Err(Error::Invalid(
85            "Only the Human claimant may perform this action.".into(),
86        ));
87    }
88    Ok(())
89}
90
91impl Receipts {
92    pub(crate) async fn settled_human_requests(
93        &self,
94        ids: Vec<crate::types::RequestId>,
95    ) -> Result<Vec<Request>, String> {
96        self.connection
97            .call(move |db| -> Result<_, Error> {
98                let mut statement = db.prepare(
99                    "SELECT data FROM requests WHERE id IN (SELECT value FROM json_each(?)) AND status NOT IN ('WAITING_HUMAN','DEFERRED')",
100                )?;
101                let rows = statement.query_map([serde_json::to_string(&ids)?], |row| {
102                    row.get::<_, String>(0)
103                })?;
104                rows.filter_map(|row| match row {
105                    Ok(data) => super::unreadable::evidence("request", &data).map(Ok),
106                    Err(error) => Some(Err(Error::Sql(error))),
107                })
108                .collect()
109            })
110            .await
111            .map_err(|e| e.to_string())
112    }
113
114    /// Whether any of these sign-offs, waiting when the caller last looked, still has a person
115    /// on it: a reviewer holds a claim, or it was submitted since. A claim is held on the request
116    /// that issued the sign-off, so a request that joined it finds it through their shared
117    /// execution. A submission also releases the claim, so it counts until the caller has taken
118    /// its result.
119    pub(crate) async fn signoff_attended(
120        &self,
121        ids: Vec<crate::types::RequestId>,
122    ) -> Result<bool, String> {
123        if ids.is_empty() {
124            return Ok(false);
125        }
126        self.connection
127            .call(move |db| -> Result<_, Error> {
128                Ok(db.query_row(
129                    "SELECT EXISTS(SELECT 1 FROM requests q JOIN requests c ON c.execution_id=q.execution_id WHERE q.id IN (SELECT value FROM json_each(?1)) AND q.status IN ('WAITING_HUMAN','DEFERRED') AND c.claimed_by IS NOT NULL) OR EXISTS(SELECT 1 FROM requests WHERE id IN (SELECT value FROM json_each(?1)) AND status NOT IN ('WAITING_HUMAN','DEFERRED'))",
130                    [serde_json::to_string(&ids)?],
131                    |row| row.get(0),
132                )?)
133            })
134            .await
135            .map_err(|e| e.to_string())
136    }
137
138    pub(crate) async fn wait_for_human(
139        &self,
140        execution: &Execution,
141        request: &Request,
142    ) -> Result<(), String> {
143        let execution = execution.clone();
144        let request = request.clone();
145        self.connection
146            .call(move |db| -> Result<(), Error> {
147                let transaction =
148                    db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
149                let data = serde_json::to_string(&execution)?;
150                if execution.key.is_some() && !request.force {
151                    if transaction.execute(
152                        "UPDATE executions SET status='WAITING_HUMAN',data=? WHERE id=? AND owner_pid=? AND owner_start_time=? AND status='RUNNING'",
153                        params![
154                            data,
155                            execution.id,
156                            execution.owner_pid(),
157                            execution.owner_start_time() as i64
158                        ],
159                    )? != 1
160                    {
161                        return Err(Error::Invalid("Active execution not found.".into()));
162                    }
163                } else {
164                    // Unclaimed: a forced or unkeyed wait never holds its key's active slot.
165                    transaction.execute(
166                        "INSERT INTO executions(id,key,eval_def_hash,status,owner_pid,owner_start_time,data) VALUES (?,NULL,?,'WAITING_HUMAN',?,?,?)",
167                        params![
168                            execution.id,
169                            execution.eval_def_hash,
170                            execution.owner_pid(),
171                            execution.owner_start_time() as i64,
172                            data
173                        ],
174                    )?;
175                }
176                update_request(&transaction, &request)?;
177                supersede_deferred(&transaction, &request)?;
178                transaction.commit()?;
179                Ok(())
180            })
181            .await
182            .map_err(|e| e.to_string())
183    }
184
185    pub(crate) async fn follow_human(&self, request: &Request) -> Result<Request, String> {
186        let mut request = request.clone();
187        self.connection
188            .call(move |db| -> Result<_, Error> {
189                let transaction =
190                    db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
191                let data: String = transaction.query_row(
192                    "SELECT data FROM executions WHERE id=? AND key=?",
193                    params![request.execution_id, request.key],
194                    |row| row.get(0),
195                )?;
196                let Some(execution) = super::unreadable::evidence::<Execution>("execution", &data)
197                else {
198                    request.execution_id = None;
199                    request.state =
200                        super::RequestState::pending(crate::types::RequestStatus::Queued, None)
201                            .expect("queued state");
202                    update_request(&transaction, &request)?;
203                    transaction.commit()?;
204                    return Ok(request);
205                };
206                if execution.status() != crate::types::ExecutionStatus::WaitingHuman {
207                    receive(&mut request, &execution);
208                }
209                update_request(&transaction, &request)?;
210                if request.status().is_human_wait() {
211                    supersede_deferred(&transaction, &request)?;
212                }
213                transaction.commit()?;
214                Ok(request)
215            })
216            .await
217            .map_err(|e| e.to_string())
218    }
219
220    pub(crate) async fn claim_human(
221        &self,
222        id: &crate::types::RequestId,
223        reviewer: &crate::types::ReviewerId,
224    ) -> Result<HumanClaim, String> {
225        let id = id.to_owned();
226        let reviewer = reviewer.to_owned();
227        self.connection
228            .call(move |db| -> Result<_, Error> {
229                let transaction =
230                    db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
231                let (request, _) = waiting(&transaction, &id)?;
232                transaction.execute(
233                    "UPDATE requests SET claimed_by=?,claimed_at=? WHERE id=? AND claimed_by IS NULL",
234                    params![reviewer, crate::broker::now(), request.id],
235                )?;
236                claimant(&transaction, &request.id, &reviewer)?;
237                let claim = claim(&transaction, &request.id)?;
238                transaction.commit()?;
239                Ok(claim)
240            })
241            .await
242            .map_err(|e| e.to_string())
243    }
244
245    /// Only the claimant releases, and only while the original request still waits.
246    pub(crate) async fn release_human(
247        &self,
248        id: &crate::types::RequestId,
249        reviewer: &crate::types::ReviewerId,
250    ) -> Result<HumanClaim, String> {
251        let id = id.to_owned();
252        let reviewer = reviewer.to_owned();
253        self.connection
254            .call(move |db| -> Result<_, Error> {
255                let transaction =
256                    db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
257                let (request, _) = waiting(&transaction, &id)?;
258                claimant(&transaction, &request.id, &reviewer)?;
259                let claim = claim(&transaction, &request.id)?;
260                unclaim(&transaction, &request.id)?;
261                transaction.commit()?;
262                Ok(claim)
263            })
264            .await
265            .map_err(|e| e.to_string())
266    }
267
268    /// The original waiting request that a request or follower forwards Human actions to.
269    pub(crate) async fn waiting_human(
270        &self,
271        id: &crate::types::RequestId,
272    ) -> Result<Request, String> {
273        let id = id.to_owned();
274        self.connection
275            .call(move |db| -> Result<_, Error> {
276                let transaction = db.transaction()?;
277                let (request, _) = waiting(&transaction, &id)?;
278                transaction.commit()?;
279                Ok(request)
280            })
281            .await
282            .map_err(|e| e.to_string())
283    }
284
285    pub(crate) async fn human_request(
286        &self,
287        id: &crate::types::RequestId,
288        reviewer: &crate::types::ReviewerId,
289    ) -> Result<(Request, Execution), String> {
290        let id = id.to_owned();
291        let reviewer = reviewer.to_owned();
292        self.connection
293            .call(move |db| -> Result<_, Error> {
294                let transaction = db.transaction()?;
295                let (request, execution) = waiting(&transaction, &id)?;
296                claimant(&transaction, &request.id, &reviewer)?;
297                transaction.commit()?;
298                Ok((request, execution))
299            })
300            .await
301            .map_err(|e| e.to_string())
302    }
303
304    pub(crate) async fn settle_human(
305        &self,
306        request: &Request,
307        reviewer: &crate::types::ReviewerId,
308    ) -> Result<Request, String> {
309        let mut request = request.clone();
310        let reviewer = reviewer.to_owned();
311        self.connection
312            .call(move |db| -> Result<_, Error> {
313                let transaction =
314                    db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
315                let (current, mut execution) = waiting(&transaction, &request.id)?;
316                claimant(&transaction, &current.id, &reviewer)?;
317                request.state = match request.state {
318                    super::RequestState::Completed { result, .. } => {
319                        super::RequestState::completed(result, crate::broker::now())
320                    }
321                    super::RequestState::Failed { error, code, .. } => {
322                        super::RequestState::failed(error, code, crate::broker::now())
323                    }
324                    _ => {
325                        return Err(Error::Invalid(
326                            "A Human submission needs a result or error.".into(),
327                        ));
328                    }
329                };
330                request.queue = None;
331                request.blocked_reason = None;
332                execution.state = request.state.clone().try_into().map_err(Error::Invalid)?;
333                execution.reviewer = Some(reviewer.clone());
334                execution.provenance.completed_at = request.completed_at();
335                request.provenance = Some(execution.provenance.clone());
336                executions::settle(&transaction, &execution, &request)?;
337                let followers = {
338                    let mut statement = transaction.prepare(
339                        "SELECT data FROM requests WHERE execution_id=? AND status IN ('WAITING_HUMAN','DEFERRED')",
340                    )?;
341                    statement
342                        .query_map([&execution.id], |row| row.get::<_, String>(0))?
343                        .filter_map(|row| match row {
344                            Ok(data) => super::unreadable::evidence("request", &data).map(Ok),
345                            Err(error) => Some(Err(Error::Sql(error))),
346                        })
347                        .collect::<Result<Vec<Request>, Error>>()?
348                };
349                for mut follower in followers {
350                    receive(&mut follower, &execution);
351                    update_request(&transaction, &follower)?;
352                }
353                unclaim(&transaction, &request.id)?;
354                transaction.commit()?;
355                Ok(request)
356            })
357            .await
358            .map_err(|e| e.to_string())
359    }
360}
361
362/// Issuance and retirement share the write lock, including when the new request joins.
363fn supersede_deferred(db: &rusqlite::Connection, request: &Request) -> Result<(), Error> {
364    let deferred = {
365        let mut statement = db.prepare("SELECT q.data,json_extract(e.data,'$.provenance.requestId'),e.id FROM requests q JOIN executions e ON e.id=q.execution_id JOIN runs r ON r.id=q.run_id WHERE e.status='WAITING_HUMAN' AND q.status='DEFERRED' AND q.eval_id=?1 AND r.repo=(SELECT repo FROM runs WHERE id=?2) AND json_extract(q.data,'$.key') IS NOT ?3 AND q.id<>?4")?;
366        statement
367            .query_map(
368                params![request.eval_id, request.run_id, request.key, request.id],
369                |row| {
370                    Ok((
371                        row.get::<_, String>(0)?,
372                        row.get::<_, crate::types::RequestId>(1)?,
373                        row.get::<_, crate::types::ExecutionId>(2)?,
374                    ))
375                },
376            )?
377            .collect::<Result<Vec<_>, _>>()?
378    };
379    for (data, original, execution) in deferred {
380        let request: Request = serde_json::from_str(&data)?;
381        let code = crate::types::FailureCode::Superseded;
382        let error = "Superseded by a newer Human request for this eval in this working tree.";
383        if request.id == original {
384            super::run_control::retire_execution(db, &execution, code, error)?;
385        } else {
386            // Retiring a follower must not end another tree's sign-off or a strict wait.
387            super::run_control::retire_request(db, request, code, error, crate::broker::now())?;
388        }
389    }
390    Ok(())
391}
392
393/// A completed record for the key settles a local Human wait, as a submission settles
394/// followers; the never-reviewed waiting execution becomes ERROR (SUPERSEDED).
395pub(super) fn settle_waiting(db: &rusqlite::Connection, entry: &Execution) -> Result<(), Error> {
396    let waiting: Option<String> = db
397        .query_row(
398            "SELECT data FROM executions WHERE key=? AND status='WAITING_HUMAN'",
399            [&entry.key],
400            |row| row.get(0),
401        )
402        .optional()?;
403    let Some(waiting) = waiting else {
404        return Ok(());
405    };
406    let Some(mut execution) = super::unreadable::evidence::<Execution>("execution", &waiting)
407    else {
408        return Ok(());
409    };
410    let now = crate::broker::now();
411    execution.state = super::ExecutionState::Failed {
412        error: format!(
413            "Superseded by the completed result {} for this key.",
414            entry.id
415        ),
416        code: Some(crate::types::FailureCode::Superseded),
417        at: now,
418    };
419    execution.provenance.completed_at = Some(now);
420    db.execute(
421        "UPDATE executions SET status='ERROR',data=? WHERE id=? AND status='WAITING_HUMAN'",
422        params![serde_json::to_string(&execution)?, execution.id],
423    )?;
424    let followers = {
425        let mut statement = db
426            .prepare("SELECT data FROM requests WHERE execution_id=? AND status IN ('WAITING_HUMAN','DEFERRED')")?;
427        statement
428            .query_map([&execution.id], |row| row.get::<_, String>(0))?
429            .filter_map(|row| match row {
430                Ok(data) => super::unreadable::evidence("request", &data).map(Ok),
431                Err(error) => Some(Err(Error::Sql(error))),
432            })
433            .collect::<Result<Vec<Request>, Error>>()?
434    };
435    for mut follower in followers {
436        receive(&mut follower, entry);
437        update_request(db, &follower)?;
438        unclaim(db, &follower.id)?;
439    }
440    Ok(())
441}
442
443fn receive(request: &mut Request, execution: &Execution) {
444    crate::cache::reuse(request, execution, crate::broker::now());
445}