1use std::collections::HashMap;
10use std::future::Future;
11use std::sync::Mutex;
12use std::time::{Duration, SystemTime};
13
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16
17use crate::kit::Reply;
18use crate::ops::Ask;
19use crate::seams::Space;
20
21pub const TASK_TTL: Duration = Duration::from_secs(60 * 60);
24
25#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
27pub struct StoredTask {
28 pub id: String,
29 pub space: Space,
31 pub operation: String,
32 #[serde(default)]
35 pub arguments: Value,
36 pub state: TaskState,
37 #[serde(default)]
39 pub status: Option<String>,
40 pub created_at: SystemTime,
41 pub updated_at: SystemTime,
42 #[serde(default)]
46 pub version: u64,
47}
48
49#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub enum TaskState {
52 Working,
53 InputRequired(Vec<Ask>),
55 Succeeded(Reply),
57 Errored(String),
59 Cancelled,
60}
61
62impl TaskState {
63 pub fn is_terminal(&self) -> bool {
64 matches!(
65 self,
66 Self::Succeeded(_) | Self::Errored(_) | Self::Cancelled
67 )
68 }
69}
70
71pub trait TaskStore: Send + Sync + 'static {
74 fn put(
81 &self,
82 task: StoredTask,
83 expected: Option<u64>,
84 ) -> impl Future<Output = std::io::Result<bool>> + Send;
85
86 fn get(&self, id: &str) -> impl Future<Output = std::io::Result<Option<StoredTask>>> + Send;
88}
89
90#[derive(Debug, Default)]
94pub struct InProcessTasks {
95 tasks: Mutex<Kept>,
96}
97
98#[derive(Debug, Default)]
99struct Kept {
100 tasks: HashMap<String, StoredTask>,
101 swept: Option<SystemTime>,
103}
104
105const SWEEP: Duration = Duration::from_secs(60);
107
108impl TaskStore for InProcessTasks {
109 async fn put(&self, task: StoredTask, expected: Option<u64>) -> std::io::Result<bool> {
110 let mut kept = self
111 .tasks
112 .lock()
113 .unwrap_or_else(|poison| poison.into_inner());
114 let now = SystemTime::now();
115 if kept
119 .swept
120 .is_none_or(|swept| now.duration_since(swept).is_ok_and(|age| age >= SWEEP))
121 {
122 kept.tasks.retain(|_, task| {
123 !task.state.is_terminal()
124 || now
125 .duration_since(task.updated_at)
126 .is_ok_and(|age| age < TASK_TTL)
127 });
128 kept.swept = Some(now);
129 }
130 if kept.tasks.get(&task.id).map(|stored| stored.version) != expected {
131 return Ok(false);
132 }
133 kept.tasks.insert(task.id.clone(), task);
134 Ok(true)
135 }
136
137 async fn get(&self, id: &str) -> std::io::Result<Option<StoredTask>> {
138 let kept = self
139 .tasks
140 .lock()
141 .unwrap_or_else(|poison| poison.into_inner());
142 Ok(kept.tasks.get(id).cloned())
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 fn task(version: u64) -> StoredTask {
151 StoredTask {
152 id: "t".into(),
153 space: Space {
154 tenant: "t".into(),
155 user: "u".into(),
156 project: "p".into(),
157 },
158 operation: "op".into(),
159 arguments: Value::Null,
160 state: TaskState::Working,
161 status: None,
162 created_at: SystemTime::now(),
163 updated_at: SystemTime::now(),
164 version,
165 }
166 }
167
168 #[tokio::test]
169 async fn only_the_writer_that_saw_the_current_version_writes() {
170 let store = InProcessTasks::default();
171 assert!(store.put(task(0), None).await.unwrap());
172 assert!(!store.put(task(0), None).await.unwrap(), "the id exists");
173 assert!(store.put(task(1), Some(0)).await.unwrap());
174 assert!(
175 !store.put(task(1), Some(0)).await.unwrap(),
176 "version 0 is gone"
177 );
178 assert_eq!(store.get("t").await.unwrap().unwrap().version, 1);
179 }
180}