use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::sync::{Arc, RwLock};
use serde::Serialize;
use serde::de::DeserializeOwned;
use crate::error::{DurableError, EngineOnly, Failure};
use crate::serialization::{decode, encode};
use crate::{DBOS, Error, Result};
pub(crate) type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
pub(crate) type ErasedWorkflow = Arc<
dyn Fn(Option<String>) -> BoxFuture<'static, std::result::Result<Option<String>, Failure>>
+ Send
+ Sync,
>;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct WorkflowKey {
pub name: String,
pub class_name: Option<String>,
pub config_name: Option<String>,
}
impl WorkflowKey {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
class_name: None,
config_name: None,
}
}
pub(crate) fn from_row(
name: impl Into<String>,
class_name: Option<&str>,
config_name: Option<&str>,
) -> Self {
let absent = |v: Option<&str>| v.filter(|s| !s.is_empty()).map(str::to_owned);
Self {
name: name.into(),
class_name: absent(class_name),
config_name: absent(config_name),
}
}
pub fn instance(
name: impl Into<String>,
class_name: impl Into<String>,
config_name: impl Into<String>,
) -> Self {
Self {
name: name.into(),
class_name: Some(class_name.into()),
config_name: Some(config_name.into()),
}
}
}
impl std::fmt::Display for WorkflowKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match (&self.class_name, &self.config_name) {
(Some(class), Some(config)) => write!(f, "{}/{class}/{config}", self.name),
(Some(class), None) => write!(f, "{}/{class}", self.name),
_ => f.write_str(&self.name),
}
}
}
#[derive(Default)]
pub(crate) struct Registry {
state: RwLock<State>,
}
#[derive(Default)]
struct State {
workflows: HashMap<WorkflowKey, ErasedWorkflow>,
frozen: bool,
}
pub(crate) type Snapshot = Arc<HashMap<WorkflowKey, ErasedWorkflow>>;
impl Registry {
fn insert(&self, key: WorkflowKey, workflow: ErasedWorkflow) -> Result<()> {
let mut state = self.write();
if state.frozen {
return Err(Error::AlreadyLaunched {
operation: "register_workflow".into(),
});
}
match state.workflows.entry(key) {
Entry::Occupied(taken) => Err(Error::AlreadyRegistered {
key: taken.key().to_string(),
}),
Entry::Vacant(slot) => {
slot.insert(workflow);
Ok(())
}
}
}
pub(crate) fn snapshot(&self) -> Snapshot {
let mut state = self.write();
state.frozen = true;
Arc::new(state.workflows.clone())
}
pub(crate) fn thaw(&self) {
self.write().frozen = false;
}
fn write(&self) -> std::sync::RwLockWriteGuard<'_, State> {
self.state
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
pub struct WorkflowRef<P, R, E = EngineOnly> {
dbos: DBOS,
key: Arc<WorkflowKey>,
types: PhantomData<fn(P) -> (R, E)>,
}
impl<P, R, E> WorkflowRef<P, R, E> {
pub fn key(&self) -> &WorkflowKey {
&self.key
}
pub fn name(&self) -> &str {
&self.key.name
}
pub(crate) fn dbos(&self) -> &DBOS {
&self.dbos
}
}
impl<P, R, E> Clone for WorkflowRef<P, R, E> {
fn clone(&self) -> Self {
Self {
dbos: self.dbos.clone(),
key: Arc::clone(&self.key),
types: PhantomData,
}
}
}
impl<P, R, E> std::fmt::Debug for WorkflowRef<P, R, E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WorkflowRef")
.field("key", &self.key.to_string())
.finish_non_exhaustive()
}
}
impl DBOS {
pub fn register_workflow<P, R, E, F, Fut>(
&self,
name: &str,
workflow: F,
) -> Result<WorkflowRef<P, R, E>>
where
F: Fn(P) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<R, E>> + Send + 'static,
P: Serialize + DeserializeOwned + Send + 'static,
R: Serialize + DeserializeOwned + Send + 'static,
E: DurableError,
{
self.register_workflow_as(WorkflowKey::new(name), workflow)
}
pub fn register_workflow_as<P, R, E, F, Fut>(
&self,
key: WorkflowKey,
workflow: F,
) -> Result<WorkflowRef<P, R, E>>
where
F: Fn(P) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<R, E>> + Send + 'static,
P: Serialize + DeserializeOwned + Send + 'static,
R: Serialize + DeserializeOwned + Send + 'static,
E: DurableError,
{
let workflow = Arc::new(workflow);
let erased: ErasedWorkflow = Arc::new(move |input: Option<String>| {
let workflow = Arc::clone(&workflow);
Box::pin(async move {
let input = decode::<P, EngineOnly>(input.as_deref(), "argument")
.map_err(Failure::Control)?;
match workflow(input).await {
Ok(output) => encode(&output, "result")
.map(Some)
.map_err(Failure::Control),
Err(error) => Err(match error.control() {
Some(control) => Failure::Control(control),
None => {
Failure::Recorded(encode(&error, "error").map_err(Failure::Control)?)
}
}),
}
})
});
self.registry().insert(key.clone(), erased)?;
tracing::debug!(workflow = %key, "registered a workflow");
Ok(WorkflowRef {
dbos: self.clone(),
key: Arc::new(key),
types: PhantomData,
})
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use super::*;
use crate::Config;
fn dbos() -> DBOS {
DBOS::new(Config::new("registry-test", "postgres://unused"))
}
async fn takes_nothing(_: ()) -> Result<String> {
Ok("nothing".to_owned())
}
async fn takes_one(n: u32) -> Result<u32> {
Ok(n * 2)
}
#[test]
fn registration_infers_the_argument_and_result_types() {
let dbos = dbos();
let nothing = dbos
.register_workflow("takes_nothing", takes_nothing)
.unwrap();
let one = dbos.register_workflow("takes_one", takes_one).unwrap();
assert_eq!(nothing.name(), "takes_nothing");
assert_eq!(one.name(), "takes_one");
dbos.register_workflow(
"closure",
|s: String| async move { Ok::<_, Error>(s.len()) },
)
.unwrap();
}
#[tokio::test]
async fn an_erased_workflow_round_trips_through_json() {
let dbos = dbos();
dbos.register_workflow("double", takes_one).unwrap();
let snapshot = dbos.registry().snapshot();
let erased = snapshot
.get(&WorkflowKey::new("double"))
.expect("registered");
assert_eq!(
erased(Some("21".to_owned())).await.unwrap(),
Some("42".to_owned())
);
}
#[tokio::test]
async fn a_zero_argument_workflow_is_called_with_no_input_at_all() {
let dbos = dbos();
dbos.register_workflow("nothing", takes_nothing).unwrap();
let snapshot = dbos.registry().snapshot();
let erased = snapshot
.get(&WorkflowKey::new("nothing"))
.expect("registered");
assert_eq!(erased(None).await.unwrap(), Some("\"nothing\"".to_owned()));
}
#[tokio::test]
async fn a_malformed_argument_is_reported_rather_than_panicking() {
let dbos = dbos();
dbos.register_workflow("double", takes_one).unwrap();
let snapshot = dbos.registry().snapshot();
let erased = snapshot
.get(&WorkflowKey::new("double"))
.expect("registered");
let err = erased(Some("\"not a number\"".to_owned()))
.await
.unwrap_err();
assert!(
matches!(
err,
Failure::Control(Error::Deserialization {
what: Cow::Borrowed("argument"),
..
})
),
"{err:?}"
);
}
#[test]
fn a_snapshot_closes_the_registry_and_releasing_it_reopens() {
let dbos = dbos();
dbos.register_workflow("before", takes_nothing).unwrap();
let snapshot = dbos.registry().snapshot();
assert_eq!(snapshot.len(), 1);
let err = dbos.register_workflow("after", takes_nothing).unwrap_err();
assert!(
matches!(
err,
Error::AlreadyLaunched {
operation: Cow::Borrowed("register_workflow")
}
),
"{err}"
);
assert_eq!(
dbos.registry().snapshot().len(),
1,
"the refused registration is not in the map either"
);
dbos.registry().thaw();
dbos.register_workflow("after", takes_nothing)
.expect("releasing the snapshot reopens registration");
assert_eq!(dbos.registry().snapshot().len(), 2);
}
#[test]
fn one_identity_registers_once() {
let dbos = dbos();
dbos.register_workflow("same", takes_nothing).unwrap();
let err = dbos.register_workflow("same", takes_nothing).unwrap_err();
assert!(matches!(err, Error::AlreadyRegistered { .. }), "{err}");
}
#[test]
fn a_name_and_an_instance_of_it_are_different_identities() {
let dbos = dbos();
dbos.register_workflow("shared", takes_nothing).unwrap();
dbos.register_workflow_as(
WorkflowKey::instance("shared", "Checkout", "eu"),
takes_nothing,
)
.unwrap();
assert_eq!(dbos.registry().snapshot().len(), 2);
}
#[test]
fn a_row_written_with_empty_strings_resolves_to_the_same_key_as_one_written_with_nulls() {
let free = WorkflowKey::new("checkout");
assert_eq!(WorkflowKey::from_row("checkout", None, None), free);
assert_eq!(WorkflowKey::from_row("checkout", Some(""), Some("")), free);
}
#[test]
fn a_key_displays_as_the_triple_the_references_spell() {
assert_eq!(WorkflowKey::new("checkout").to_string(), "checkout");
assert_eq!(
WorkflowKey::instance("checkout", "Checkout", "eu").to_string(),
"checkout/Checkout/eu"
);
}
}