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    pub(crate) async fn wait_for_human(
115        &self,
116        execution: &Execution,
117        request: &Request,
118    ) -> Result<(), String> {
119        let execution = execution.clone();
120        let request = request.clone();
121        self.connection
122            .call(move |db| -> Result<(), Error> {
123                let transaction =
124                    db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
125                let data = serde_json::to_string(&execution)?;
126                if execution.key.is_some() && !request.force {
127                    if transaction.execute(
128                        "UPDATE executions SET status='WAITING_HUMAN',data=? WHERE id=? AND owner_pid=? AND owner_start_time=? AND status='RUNNING'",
129                        params![
130                            data,
131                            execution.id,
132                            execution.owner_pid(),
133                            execution.owner_start_time() as i64
134                        ],
135                    )? != 1
136                    {
137                        return Err(Error::Invalid("Active execution not found.".into()));
138                    }
139                } else {
140                    // Unclaimed: a forced or unkeyed wait never holds its key's active slot.
141                    transaction.execute(
142                        "INSERT INTO executions(id,key,eval_def_hash,status,owner_pid,owner_start_time,data) VALUES (?,NULL,?,'WAITING_HUMAN',?,?,?)",
143                        params![
144                            execution.id,
145                            execution.eval_def_hash,
146                            execution.owner_pid(),
147                            execution.owner_start_time() as i64,
148                            data
149                        ],
150                    )?;
151                }
152                update_request(&transaction, &request)?;
153                transaction.commit()?;
154                Ok(())
155            })
156            .await
157            .map_err(|e| e.to_string())
158    }
159
160    pub(crate) async fn follow_human(&self, request: &Request) -> Result<Request, String> {
161        let mut request = request.clone();
162        self.connection
163            .call(move |db| -> Result<_, Error> {
164                let transaction =
165                    db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
166                let data: String = transaction.query_row(
167                    "SELECT data FROM executions WHERE id=? AND key=?",
168                    params![request.execution_id, request.key],
169                    |row| row.get(0),
170                )?;
171                let Some(execution) = super::unreadable::evidence::<Execution>("execution", &data)
172                else {
173                    request.execution_id = None;
174                    request.state =
175                        super::RequestState::pending(crate::types::RequestStatus::Queued, None)
176                            .expect("queued state");
177                    update_request(&transaction, &request)?;
178                    transaction.commit()?;
179                    return Ok(request);
180                };
181                if execution.status() != crate::types::ExecutionStatus::WaitingHuman {
182                    receive(&mut request, &execution);
183                }
184                update_request(&transaction, &request)?;
185                transaction.commit()?;
186                Ok(request)
187            })
188            .await
189            .map_err(|e| e.to_string())
190    }
191
192    pub(crate) async fn claim_human(
193        &self,
194        id: &crate::types::RequestId,
195        reviewer: &crate::types::ReviewerId,
196    ) -> Result<HumanClaim, String> {
197        let id = id.to_owned();
198        let reviewer = reviewer.to_owned();
199        self.connection
200            .call(move |db| -> Result<_, Error> {
201                let transaction =
202                    db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
203                let (request, _) = waiting(&transaction, &id)?;
204                transaction.execute(
205                    "UPDATE requests SET claimed_by=?,claimed_at=? WHERE id=? AND claimed_by IS NULL",
206                    params![reviewer, crate::broker::now(), request.id],
207                )?;
208                claimant(&transaction, &request.id, &reviewer)?;
209                let claim = claim(&transaction, &request.id)?;
210                transaction.commit()?;
211                Ok(claim)
212            })
213            .await
214            .map_err(|e| e.to_string())
215    }
216
217    /// Only the claimant releases, and only while the original request still waits.
218    pub(crate) async fn release_human(
219        &self,
220        id: &crate::types::RequestId,
221        reviewer: &crate::types::ReviewerId,
222    ) -> Result<HumanClaim, String> {
223        let id = id.to_owned();
224        let reviewer = reviewer.to_owned();
225        self.connection
226            .call(move |db| -> Result<_, Error> {
227                let transaction =
228                    db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
229                let (request, _) = waiting(&transaction, &id)?;
230                claimant(&transaction, &request.id, &reviewer)?;
231                let claim = claim(&transaction, &request.id)?;
232                unclaim(&transaction, &request.id)?;
233                transaction.commit()?;
234                Ok(claim)
235            })
236            .await
237            .map_err(|e| e.to_string())
238    }
239
240    /// The original waiting request that a request or follower forwards Human actions to.
241    pub(crate) async fn waiting_human(
242        &self,
243        id: &crate::types::RequestId,
244    ) -> Result<Request, String> {
245        let id = id.to_owned();
246        self.connection
247            .call(move |db| -> Result<_, Error> {
248                let transaction = db.transaction()?;
249                let (request, _) = waiting(&transaction, &id)?;
250                transaction.commit()?;
251                Ok(request)
252            })
253            .await
254            .map_err(|e| e.to_string())
255    }
256
257    pub(crate) async fn human_request(
258        &self,
259        id: &crate::types::RequestId,
260        reviewer: &crate::types::ReviewerId,
261    ) -> Result<(Request, Execution), String> {
262        let id = id.to_owned();
263        let reviewer = reviewer.to_owned();
264        self.connection
265            .call(move |db| -> Result<_, Error> {
266                let transaction = db.transaction()?;
267                let (request, execution) = waiting(&transaction, &id)?;
268                claimant(&transaction, &request.id, &reviewer)?;
269                transaction.commit()?;
270                Ok((request, execution))
271            })
272            .await
273            .map_err(|e| e.to_string())
274    }
275
276    pub(crate) async fn settle_human(
277        &self,
278        request: &Request,
279        reviewer: &crate::types::ReviewerId,
280    ) -> Result<Request, String> {
281        let mut request = request.clone();
282        let reviewer = reviewer.to_owned();
283        self.connection
284            .call(move |db| -> Result<_, Error> {
285                let transaction =
286                    db.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
287                let (current, mut execution) = waiting(&transaction, &request.id)?;
288                claimant(&transaction, &current.id, &reviewer)?;
289                request.state = match request.state {
290                    super::RequestState::Completed { result, .. } => {
291                        super::RequestState::completed(result, crate::broker::now())
292                    }
293                    super::RequestState::Failed { error, code, .. } => {
294                        super::RequestState::failed(error, code, crate::broker::now())
295                    }
296                    _ => {
297                        return Err(Error::Invalid(
298                            "A Human submission needs a result or error.".into(),
299                        ));
300                    }
301                };
302                request.queue = None;
303                request.blocked_reason = None;
304                execution.state = request.state.clone().try_into().map_err(Error::Invalid)?;
305                execution.reviewer = Some(reviewer.clone());
306                execution.provenance.completed_at = request.completed_at();
307                request.provenance = Some(execution.provenance.clone());
308                let published = executions::settle(&transaction, &execution, &request)?;
309                let followers = {
310                    let mut statement = transaction.prepare(
311                        "SELECT data FROM requests WHERE execution_id=? AND status IN ('WAITING_HUMAN','DEFERRED')",
312                    )?;
313                    statement
314                        .query_map([&execution.id], |row| row.get::<_, String>(0))?
315                        .filter_map(|row| match row {
316                            Ok(data) => super::unreadable::evidence("request", &data).map(Ok),
317                            Err(error) => Some(Err(Error::Sql(error))),
318                        })
319                        .collect::<Result<Vec<Request>, Error>>()?
320                };
321                for mut follower in followers {
322                    receive(&mut follower, &execution);
323                    update_request(&transaction, &follower)?;
324                }
325                unclaim(&transaction, &request.id)?;
326                transaction.commit()?;
327                if published {
328                    super::history::collect_after(db);
329                }
330                Ok(request)
331            })
332            .await
333            .map_err(|e| e.to_string())
334    }
335}
336
337/// A completed record for the key settles a local Human wait, as a submission settles
338/// followers; the never-reviewed waiting execution becomes ERROR (SUPERSEDED).
339pub(super) fn settle_waiting(db: &rusqlite::Connection, entry: &Execution) -> Result<(), Error> {
340    let waiting: Option<String> = db
341        .query_row(
342            "SELECT data FROM executions WHERE key=? AND status='WAITING_HUMAN'",
343            [&entry.key],
344            |row| row.get(0),
345        )
346        .optional()?;
347    let Some(waiting) = waiting else {
348        return Ok(());
349    };
350    let Some(mut execution) = super::unreadable::evidence::<Execution>("execution", &waiting)
351    else {
352        return Ok(());
353    };
354    let now = crate::broker::now();
355    execution.state = super::ExecutionState::Failed {
356        error: format!(
357            "Superseded by the completed result {} for this key.",
358            entry.id
359        ),
360        code: Some(crate::types::FailureCode::Superseded),
361        at: now,
362    };
363    execution.provenance.completed_at = Some(now);
364    db.execute(
365        "UPDATE executions SET status='ERROR',data=? WHERE id=? AND status='WAITING_HUMAN'",
366        params![serde_json::to_string(&execution)?, execution.id],
367    )?;
368    let followers = {
369        let mut statement = db
370            .prepare("SELECT data FROM requests WHERE execution_id=? AND status IN ('WAITING_HUMAN','DEFERRED')")?;
371        statement
372            .query_map([&execution.id], |row| row.get::<_, String>(0))?
373            .filter_map(|row| match row {
374                Ok(data) => super::unreadable::evidence("request", &data).map(Ok),
375                Err(error) => Some(Err(Error::Sql(error))),
376            })
377            .collect::<Result<Vec<Request>, Error>>()?
378    };
379    for mut follower in followers {
380        receive(&mut follower, entry);
381        update_request(db, &follower)?;
382        unclaim(db, &follower.id)?;
383    }
384    Ok(())
385}
386
387fn receive(request: &mut Request, execution: &Execution) {
388    crate::cache::reuse(request, execution, crate::broker::now());
389}