use std::collections::HashMap;
use std::future::Future;
use std::sync::Mutex;
use std::time::{Duration, SystemTime};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::kit::Reply;
use crate::ops::Ask;
use crate::seams::Space;
pub const TASK_TTL: Duration = Duration::from_secs(60 * 60);
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct StoredTask {
pub id: String,
pub space: Space,
pub operation: String,
#[serde(default)]
pub arguments: Value,
pub state: TaskState,
#[serde(default)]
pub status: Option<String>,
pub created_at: SystemTime,
pub updated_at: SystemTime,
#[serde(default)]
pub version: u64,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TaskState {
Working,
InputRequired(Vec<Ask>),
Succeeded(Reply),
Errored(String),
Cancelled,
}
impl TaskState {
pub fn is_terminal(&self) -> bool {
matches!(
self,
Self::Succeeded(_) | Self::Errored(_) | Self::Cancelled
)
}
}
pub trait TaskStore: Send + Sync + 'static {
fn put(
&self,
task: StoredTask,
expected: Option<u64>,
) -> impl Future<Output = std::io::Result<bool>> + Send;
fn get(&self, id: &str) -> impl Future<Output = std::io::Result<Option<StoredTask>>> + Send;
}
#[derive(Debug, Default)]
pub struct InProcessTasks {
tasks: Mutex<Kept>,
}
#[derive(Debug, Default)]
struct Kept {
tasks: HashMap<String, StoredTask>,
swept: Option<SystemTime>,
}
const SWEEP: Duration = Duration::from_secs(60);
impl TaskStore for InProcessTasks {
async fn put(&self, task: StoredTask, expected: Option<u64>) -> std::io::Result<bool> {
let mut kept = self
.tasks
.lock()
.unwrap_or_else(|poison| poison.into_inner());
let now = SystemTime::now();
if kept
.swept
.is_none_or(|swept| now.duration_since(swept).is_ok_and(|age| age >= SWEEP))
{
kept.tasks.retain(|_, task| {
!task.state.is_terminal()
|| now
.duration_since(task.updated_at)
.is_ok_and(|age| age < TASK_TTL)
});
kept.swept = Some(now);
}
if kept.tasks.get(&task.id).map(|stored| stored.version) != expected {
return Ok(false);
}
kept.tasks.insert(task.id.clone(), task);
Ok(true)
}
async fn get(&self, id: &str) -> std::io::Result<Option<StoredTask>> {
let kept = self
.tasks
.lock()
.unwrap_or_else(|poison| poison.into_inner());
Ok(kept.tasks.get(id).cloned())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn task(version: u64) -> StoredTask {
StoredTask {
id: "t".into(),
space: Space {
tenant: "t".into(),
user: "u".into(),
project: "p".into(),
},
operation: "op".into(),
arguments: Value::Null,
state: TaskState::Working,
status: None,
created_at: SystemTime::now(),
updated_at: SystemTime::now(),
version,
}
}
#[tokio::test]
async fn only_the_writer_that_saw_the_current_version_writes() {
let store = InProcessTasks::default();
assert!(store.put(task(0), None).await.unwrap());
assert!(!store.put(task(0), None).await.unwrap(), "the id exists");
assert!(store.put(task(1), Some(0)).await.unwrap());
assert!(
!store.put(task(1), Some(0)).await.unwrap(),
"version 0 is gone"
);
assert_eq!(store.get("t").await.unwrap().unwrap().version, 1);
}
}