use std::collections::HashMap;
use std::future::Future;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::PoisonError;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use leptoskit_db::Actor;
use leptoskit_db::Db;
use leptoskit_db::Id;
use leptoskit_db::Json;
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::Value;
use super::Enqueue;
use super::Faults;
use super::JobRow;
use super::JobState;
use crate::Error;
use crate::Job;
use crate::MutationCx;
use crate::Operation;
use crate::Result;
use crate::inbox::Notice;
use crate::server::Identity;
use crate::server::inbox::Recipient;
tokio::task_local! {
static STEP: String;
}
#[derive(Default)]
pub(crate) struct RunState {
steps: Mutex<serde_json::Map<String, Value>>,
progress: Mutex<Progress>,
cancelled: AtomicBool,
killed: AtomicBool,
faults: Option<Arc<Faults>>,
mutations: Mutex<HashMap<String, u32>>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct Progress {
pub text: Option<String>,
pub fraction: Option<f64>,
pub dirty: bool,
}
impl RunState {
pub(crate) fn new(
steps: Value,
cancelled: bool,
progress: Progress,
faults: Option<Arc<Faults>>,
) -> Self {
Self {
steps: Mutex::new(match steps {
Value::Object(steps) => steps,
_ => serde_json::Map::new(),
}),
progress: Mutex::new(progress),
cancelled: AtomicBool::new(cancelled),
killed: AtomicBool::new(false),
faults,
mutations: Mutex::default(),
}
}
pub(crate) fn take_progress(&self) -> Option<Progress> {
let mut progress = self.progress.lock().unwrap_or_else(PoisonError::into_inner);
if !progress.dirty {
return None;
}
progress.dirty = false;
Some(progress.clone())
}
pub(crate) fn progress(&self) -> Progress {
self.progress
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone()
}
pub(crate) fn cancel(&self) {
self.cancelled.store(true, Ordering::Relaxed);
}
pub(crate) fn is_cancelled(&self) -> bool {
self.cancelled.load(Ordering::Relaxed)
}
pub(crate) fn is_killed(&self) -> bool {
self.killed.load(Ordering::Relaxed)
}
}
pub struct JobCx {
db: Db,
identity: Identity,
id: String,
row: Option<Id<JobRow>>,
attempt: u32,
owner: Option<Actor>,
state: Arc<RunState>,
}
impl crate::server::sealed::Sealed for JobCx {}
impl JobCx {
#[doc(hidden)]
pub fn new(db: Db, identity: Identity, id: String, attempt: u32) -> Self {
let owner = identity.context.actor();
Self {
db,
identity,
id,
row: None,
attempt,
owner,
state: Arc::default(),
}
}
pub(crate) fn for_run(
db: Db,
identity: Identity,
row: Id<JobRow>,
attempt: u32,
owner: Option<Actor>,
state: Arc<RunState>,
) -> Self {
Self {
db,
identity,
id: row.to_string(),
row: Some(row),
attempt,
owner,
state,
}
}
pub fn db(&self) -> &Db {
&self.db
}
pub fn identity(&self) -> &Identity {
&self.identity
}
pub fn id(&self) -> &str {
&self.id
}
pub fn attempt(&self) -> u32 {
self.attempt
}
pub fn owner(&self) -> Option<Actor> {
self.owner
}
pub fn key(&self) -> String {
match STEP.try_with(Clone::clone) {
Ok(step) => self.step_key(&step),
Err(_) => self.id.clone(),
}
}
pub fn step_key(&self, name: &str) -> String {
format!("{}:{name}", self.id)
}
pub fn cancelled(&self) -> bool {
self.state.is_cancelled()
}
pub fn progress(&self, text: impl Into<String>) {
let mut progress = self
.state
.progress
.lock()
.unwrap_or_else(PoisonError::into_inner);
progress.text = Some(text.into());
progress.dirty = true;
}
pub fn progress_fraction(&self, fraction: f64) {
let mut progress = self
.state
.progress
.lock()
.unwrap_or_else(PoisonError::into_inner);
progress.fraction = Some(if fraction.is_nan() {
0.0
} else {
fraction.clamp(0.0, 1.0)
});
progress.dirty = true;
}
pub async fn step<T, F, Fut>(&self, name: &str, work: F) -> Result<T>
where
T: Serialize + DeserializeOwned,
F: FnOnce() -> Fut,
Fut: Future<Output = Result<T>>,
{
let saved = self
.state
.steps
.lock()
.unwrap_or_else(PoisonError::into_inner)
.get(name)
.cloned();
if let Some(saved) = saved {
return serde_json::from_value(saved).map_err(|error| {
Error::permanent(format!(
"step `{name}` saved a result that no longer fits: {error}"
))
});
}
self.check_cancelled().await?;
let value = STEP.scope(name.to_owned(), work()).await?;
if self
.state
.faults
.as_ref()
.is_some_and(|faults| faults.kills(name))
{
self.state.killed.store(true, Ordering::Relaxed);
return Err(Error::server("the worker stopped"));
}
let saved = serde_json::to_value(&value).map_err(|error| {
Error::permanent(format!("step `{name}`'s result isn't JSON: {error}"))
})?;
let steps = {
let mut steps = self
.state
.steps
.lock()
.unwrap_or_else(PoisonError::into_inner);
steps.insert(name.to_owned(), saved);
Value::Object(steps.clone())
};
self.save(steps).await?;
Ok(value)
}
async fn save(&self, steps: Value) -> Result<()> {
let Some(row) = self.row else {
return Ok(());
};
let attempt = self.attempt;
let mut update = JobRow::update()
.filter(|job| {
job.id.eq(row) & job.state.eq(JobState::Running) & job.attempts.eq(attempt)
})
.set(|job| job.steps, Json(steps));
if let Some(progress) = self.state.take_progress() {
update = update
.set(|job| job.progress, progress.text)
.set(|job| job.fraction, progress.fraction);
}
if update.run(&self.db).await? == 0 {
return Err(Error::retry("another worker took this job over"));
}
Ok(())
}
async fn check_cancelled(&self) -> Result<()> {
if let Some(row) = self.row
&& !self.state.is_cancelled()
&& JobRow::query()
.filter(|job| job.id.eq(row) & job.cancel_requested.eq(true))
.exists(&self.db)
.await?
{
self.state.cancel();
}
if self.state.is_cancelled() {
return Err(Error::permanent("the job was cancelled"));
}
Ok(())
}
pub async fn mutate<M: Operation<Context = MutationCx>>(&self, op: M) -> Result<M::Output> {
let Some(_) = self.row else {
return crate::server::run::mutate(&self.db, self.identity.clone(), None, op).await;
};
let scope = STEP.try_with(Clone::clone).unwrap_or_default();
let count = {
let mut mutations = self
.state
.mutations
.lock()
.unwrap_or_else(PoisonError::into_inner);
let count = mutations.entry(format!("{scope}/{}", M::NAME)).or_default();
*count += 1;
*count
};
let request = format!("{scope}/{}/{count}", M::NAME);
crate::server::run::mutate(&self.db, self.identity.clone(), Some(&request), op).await
}
pub async fn notify(&self, account: impl Recipient, notice: Notice) -> Result<Option<String>> {
let Some(delivery) = crate::server::inbox::insert(&self.db, &account, notice).await? else {
return Ok(None);
};
let id = delivery.id.clone();
tokio::spawn(crate::server::inbox::deliver(delivery));
Ok(Some(id))
}
pub fn enqueue<J: Job>(&self, job: J) -> Enqueue<'static, J> {
Enqueue::on(self.db.clone(), job, self.owner)
}
}
impl std::fmt::Debug for JobCx {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("JobCx")
.field("id", &self.id)
.field("attempt", &self.attempt)
.finish_non_exhaustive()
}
}