use crate::backoff::Backoff;
use crate::context::{Context, JournalEntry};
use crate::error::Error;
use crate::outcome::{Outcome, TaskError};
use crate::task::Handler;
use futures_util::FutureExt;
use std::collections::HashMap;
use std::future::Future;
use std::panic::AssertUnwindSafe;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use tokio_util::sync::CancellationToken;
pub(crate) struct RunInput<S> {
pub payload: serde_json::Value,
pub carry: serde_json::Value,
pub run_count: u32,
pub history: Vec<JournalEntry>,
pub state: Arc<S>,
pub cancel: CancellationToken,
pub panic_policy: super::PanicPolicy,
}
pub(crate) struct RunReport {
pub result: Result<Outcome, TaskError>,
pub carry: serde_json::Value,
pub backoff: Option<Backoff>,
pub attachment: Option<serde_json::Value>,
pub duration: Duration,
}
type RunFuture = Pin<Box<dyn Future<Output = Result<RunReport, Error>> + Send>>;
type ErasedRun<S> = Box<dyn Fn(RunInput<S>) -> RunFuture + Send + Sync>;
type ErasedLease = Box<dyn Fn(&serde_json::Value) -> Option<Duration> + Send + Sync>;
struct Entry<S> {
run: ErasedRun<S>,
lease: ErasedLease,
cap: Option<usize>,
}
pub(crate) struct Registry<S> {
entries: HashMap<&'static str, Entry<S>>,
}
impl<S> Registry<S>
where
S: Send + Sync + 'static,
{
pub(crate) fn new() -> Registry<S> {
Registry {
entries: HashMap::new(),
}
}
pub(crate) fn register<T>(&mut self, cap: Option<usize>)
where
T: Handler<S>,
{
self.entries.insert(
T::KIND,
Entry {
run: erased_run::<T, S>(),
lease: erased_lease::<T>(),
cap,
},
);
}
pub(crate) fn kinds(&self) -> Vec<String> {
self.entries.keys().map(|k| (*k).to_owned()).collect()
}
pub(crate) fn cap(&self, kind: &str) -> Option<usize> {
self.entries.get(kind).and_then(|entry| entry.cap)
}
pub(crate) fn dispatch(&self, kind: &str, input: RunInput<S>) -> Result<RunFuture, Error> {
let entry = self.entries.get(kind).ok_or_else(|| Error::UnknownKind {
kind: kind.to_owned(),
})?;
Ok((entry.run)(input))
}
pub(crate) fn lease_for(&self, kind: &str, payload: &serde_json::Value) -> Option<Duration> {
self.entries
.get(kind)
.and_then(|entry| (entry.lease)(payload))
}
}
fn erased_lease<T>() -> ErasedLease
where
T: crate::task::Task,
{
Box::new(|payload: &serde_json::Value| {
serde_json::from_value::<T>(payload.clone())
.ok()
.and_then(|task| task.lease())
})
}
fn erased_run<T, S>() -> ErasedRun<S>
where
T: Handler<S>,
S: Send + Sync + 'static,
{
Box::new(move |input: RunInput<S>| {
Box::pin(async move {
let payload: T = serde_json::from_value(input.payload)?;
let carry_before = input.carry.clone();
let carry: T::Carry = if input.carry.is_null() {
T::Carry::default()
} else {
serde_json::from_value(input.carry)?
};
let mut ctx = Context::new(input.run_count, input.history, carry, input.cancel);
let started = std::time::Instant::now();
let caught = AssertUnwindSafe(payload.handle(&mut ctx, &input.state))
.catch_unwind()
.await;
let duration = started.elapsed();
let backoff = payload.backoff();
match caught {
Ok(result) => {
let (carry, attachment) = ctx.into_parts();
let carry = serde_json::to_value(carry)?;
Ok(RunReport {
result,
carry,
backoff,
attachment,
duration,
})
}
Err(panic) => {
let message = panic_message(panic);
let error = match input.panic_policy {
super::PanicPolicy::Retry => TaskError::retryable(message),
super::PanicPolicy::Dead => TaskError::permanent(message),
};
Ok(RunReport {
result: Err(error),
carry: carry_before,
backoff,
attachment: None,
duration,
})
}
}
})
})
}
fn panic_message(panic: Box<dyn std::any::Any + Send>) -> String {
if let Some(message) = panic.downcast_ref::<&'static str>() {
format!("handler panicked: {message}")
} else if let Some(message) = panic.downcast_ref::<String>() {
format!("handler panicked: {message}")
} else {
"handler panicked".to_owned()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::task::Task;
use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicI32, Ordering};
#[derive(Default)]
struct Sink {
total: AtomicI32,
}
#[derive(Serialize, Deserialize)]
struct Ping {
n: i32,
}
impl Task for Ping {
const KIND: &'static str = "ping";
type Carry = ();
}
impl Handler<Sink> for Ping {
async fn handle(&self, _ctx: &mut Context<()>, state: &Sink) -> Result<Outcome, TaskError> {
state.total.fetch_add(self.n, Ordering::SeqCst);
Ok(Outcome::completed())
}
}
fn input(payload: serde_json::Value, state: Arc<Sink>) -> RunInput<Sink> {
RunInput {
payload,
carry: serde_json::Value::Null,
run_count: 1,
history: Vec::new(),
state,
cancel: CancellationToken::new(),
panic_policy: crate::worker::PanicPolicy::Retry,
}
}
#[tokio::test]
async fn dispatch_runs_the_registered_handler() {
let mut registry = Registry::new();
registry.register::<Ping>(None);
let state = Arc::new(Sink::default());
let report = registry
.dispatch("ping", input(serde_json::json!({ "n": 5 }), state.clone()))
.expect("kind is registered")
.await
.expect("run succeeds");
assert!(matches!(report.result, Ok(Outcome::Completed { .. })));
assert_eq!(state.total.load(Ordering::SeqCst), 5);
}
#[tokio::test]
async fn dispatch_unknown_kind_is_an_error() {
let registry: Registry<Sink> = Registry::new();
let state = Arc::new(Sink::default());
let result = registry.dispatch("missing", input(serde_json::Value::Null, state));
assert!(matches!(result, Err(Error::UnknownKind { kind }) if kind == "missing"));
}
}