use crate::error::{Error, Result};
use crate::schedule::{ScheduleFilter, ScheduleStatus, WorkflowSchedule};
use crate::tx::{TransactionOptions, TxBody};
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::time::Duration;
pub(crate) fn dedup_or(e: sqlx::Error, s: &WorkflowStatus) -> Error {
let err = Error::from(e);
if err.is_unique_violation() {
return Error::queue_deduplicated(
s.queue_name.clone().unwrap_or_default(),
s.dedup_id.clone().unwrap_or_default(),
);
}
err
}
pub(crate) fn nonexistent_or(e: sqlx::Error, destination_id: &str) -> Error {
let err = Error::from(e);
if err.is_foreign_key_violation() {
return Error::nonexistent_workflow(destination_id);
}
err
}
pub(crate) fn encode_roles(roles: &[String]) -> Option<String> {
if roles.is_empty() {
None
} else {
serde_json::to_string(roles).ok()
}
}
pub(crate) fn decode_roles(stored: Option<&str>) -> Vec<String> {
stored
.and_then(|s| serde_json::from_str(s).ok())
.unwrap_or_default()
}
pub const STATUS_ENQUEUED: &str = "ENQUEUED";
pub const STATUS_DELAYED: &str = "DELAYED";
pub const STATUS_PENDING: &str = "PENDING";
pub const STATUS_SUCCESS: &str = "SUCCESS";
pub const STATUS_ERROR: &str = "ERROR";
pub const STATUS_CANCELLED: &str = "CANCELLED";
pub const STATUS_MAX_RECOVERY_ATTEMPTS_EXCEEDED: &str = "MAX_RECOVERY_ATTEMPTS_EXCEEDED";
pub(crate) const STREAM_CLOSED_SENTINEL: &str = "__DBOS_STREAM_CLOSED__";
#[cfg(feature = "postgres")]
pub(crate) const NOTIFICATIONS_CHANNEL: &str = "dbos_notifications_channel";
#[cfg(feature = "postgres")]
pub(crate) const WORKFLOW_EVENTS_CHANNEL: &str = "dbos_workflow_events_channel";
#[derive(Clone, Copy, Debug)]
pub enum ChangeWait<'a> {
Notification {
workflow_id: &'a str,
topic: &'a str,
},
Event {
workflow_id: &'a str,
key: &'a str,
},
}
impl ChangeWait<'_> {
#[cfg(feature = "postgres")]
pub(crate) fn channel(&self) -> &'static str {
match self {
ChangeWait::Notification { .. } => NOTIFICATIONS_CHANNEL,
ChangeWait::Event { .. } => WORKFLOW_EVENTS_CHANNEL,
}
}
#[cfg(feature = "postgres")]
pub(crate) fn payload(&self) -> String {
match self {
ChangeWait::Notification { workflow_id, topic } => format!("{workflow_id}::{topic}"),
ChangeWait::Event { workflow_id, key } => format!("{workflow_id}::{key}"),
}
}
}
pub(crate) fn group_stream_rows(
serializer: &crate::serialize::Serializer,
rows: Vec<(String, String, Option<String>)>,
) -> Result<Vec<(String, Vec<Value>)>> {
let mut out: Vec<(String, Vec<Value>)> = Vec::new();
for (key, value, fmt) in rows {
if value == STREAM_CLOSED_SENTINEL {
if out.last().map(|(k, _)| k != &key).unwrap_or(true) {
out.push((key, Vec::new()));
}
continue;
}
let decoded = crate::serialize::decode(serializer, fmt.as_deref(), &value)?;
match out.last_mut() {
Some((k, vals)) if *k == key => vals.push(decoded),
_ => out.push((key, vec![decoded])),
}
}
Ok(out)
}
const STREAM_POLL_INTERVAL: Duration = Duration::from_millis(25);
pub(crate) async fn drain_stream<T: DeserializeOwned>(
provider: &dyn StateProvider,
workflow_id: &str,
key: &str,
) -> Result<(Vec<T>, bool)> {
drain_stream_from(provider, workflow_id, key).await
}
#[async_trait]
trait StreamBackend {
async fn stream_entries(
&self,
workflow_id: &str,
key: &str,
from_offset: i32,
) -> Result<(Vec<Value>, bool)>;
async fn producer_status(&self, workflow_id: &str) -> Result<Option<String>>;
}
#[async_trait]
impl<T: StateProvider + ?Sized> StreamBackend for T {
async fn stream_entries(
&self,
workflow_id: &str,
key: &str,
from_offset: i32,
) -> Result<(Vec<Value>, bool)> {
self.read_stream(workflow_id, key, from_offset).await
}
async fn producer_status(&self, workflow_id: &str) -> Result<Option<String>> {
Ok(self
.get_workflow_status(workflow_id)
.await?
.map(|s| s.status))
}
}
async fn drain_stream_from<T: DeserializeOwned, S: StreamBackend + ?Sized>(
source: &S,
workflow_id: &str,
key: &str,
) -> Result<(Vec<T>, bool)> {
let mut all = Vec::new();
let mut offset = 0_i32;
let mut final_read = false;
loop {
let (values, closed) = source.stream_entries(workflow_id, key, offset).await?;
offset += values.len() as i32;
for v in values {
all.push(serde_json::from_value(v)?);
}
if closed {
return Ok((all, true));
}
if final_read {
return Ok((all, true));
}
match source.producer_status(workflow_id).await? {
None => return Err(Error::nonexistent_workflow(workflow_id)),
Some(s) if s != STATUS_PENDING && s != STATUS_ENQUEUED => {
final_read = true;
continue;
}
_ => {}
}
tokio::time::sleep(STREAM_POLL_INTERVAL).await;
}
}
pub(crate) async fn snapshot_stream<T: DeserializeOwned>(
provider: &dyn StateProvider,
workflow_id: &str,
key: &str,
from_offset: i32,
) -> Result<(Vec<T>, bool)> {
let (values, closed) = provider.read_stream(workflow_id, key, from_offset).await?;
let out = values
.into_iter()
.map(serde_json::from_value)
.collect::<std::result::Result<Vec<T>, _>>()?;
Ok((out, closed))
}
pub(crate) fn stream_values<'a, T>(
source: &'a dyn StateProvider,
workflow_id: &str,
key: &str,
) -> std::pin::Pin<Box<dyn futures_util::Stream<Item = Result<T>> + 'a>>
where
T: DeserializeOwned + 'a,
{
struct State<'a> {
source: &'a dyn StateProvider,
workflow_id: String,
key: String,
offset: i32,
buffer: std::collections::VecDeque<Value>,
final_read: bool,
done: bool,
}
let init = State {
source,
workflow_id: workflow_id.to_string(),
key: key.to_string(),
offset: 0,
buffer: std::collections::VecDeque::new(),
final_read: false,
done: false,
};
Box::pin(futures_util::stream::unfold(init, |mut st| async move {
if st.done {
return None;
}
loop {
if let Some(v) = st.buffer.pop_front() {
return match serde_json::from_value::<T>(v) {
Ok(t) => Some((Ok(t), st)),
Err(e) => {
st.done = true;
Some((Err(Error::from(e)), st))
}
};
}
let (values, closed) = match st
.source
.stream_entries(&st.workflow_id, &st.key, st.offset)
.await
{
Ok(v) => v,
Err(e) => {
st.done = true;
return Some((Err(e), st));
}
};
st.offset += values.len() as i32;
st.buffer.extend(values);
if !st.buffer.is_empty() {
continue; }
if closed || st.final_read {
return None;
}
match st.source.producer_status(&st.workflow_id).await {
Err(e) => {
st.done = true;
return Some((Err(e), st));
}
Ok(None) => {
st.done = true;
return Some((Err(Error::nonexistent_workflow(&st.workflow_id)), st));
}
Ok(Some(s)) if s != STATUS_PENDING && s != STATUS_ENQUEUED => {
st.final_read = true;
continue;
}
Ok(_) => {}
}
tokio::time::sleep(STREAM_POLL_INTERVAL).await;
}
}))
}
pub fn is_terminal(status: &str) -> bool {
matches!(
status,
STATUS_SUCCESS | STATUS_ERROR | STATUS_CANCELLED | STATUS_MAX_RECOVERY_ATTEMPTS_EXCEEDED
)
}
#[derive(Clone, Debug)]
pub struct WorkflowStatus {
pub id: String,
pub name: String,
pub status: String,
pub input: Value,
pub output: Option<Value>,
pub error: Option<String>,
pub error_info: Option<crate::PortableWorkflowError>,
pub executor_id: String,
pub app_version: String,
pub queue_name: Option<String>,
pub queue_partition_key: Option<String>,
pub priority: i32,
pub dedup_id: Option<String>,
pub recovery_attempts: i32,
pub parent_workflow_id: Option<String>,
pub timeout_ms: Option<i64>,
pub deadline_ms: Option<i64>,
pub started_at_ms: Option<i64>,
pub rate_limited: bool,
pub delay_until_ms: Option<i64>,
pub completed_at_ms: Option<i64>,
pub forked_from: Option<String>,
pub authenticated_user: Option<String>,
pub assumed_role: Option<String>,
pub authenticated_roles: Vec<String>,
pub class_name: Option<String>,
pub config_name: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl WorkflowStatus {
pub fn new(
id: impl Into<String>,
name: impl Into<String>,
input: Value,
status: impl Into<String>,
executor_id: impl Into<String>,
app_version: impl Into<String>,
) -> Self {
let now = Utc::now();
Self {
id: id.into(),
name: name.into(),
status: status.into(),
input,
output: None,
error: None,
error_info: None,
executor_id: executor_id.into(),
app_version: app_version.into(),
queue_name: None,
queue_partition_key: None,
priority: 0,
dedup_id: None,
recovery_attempts: 0,
parent_workflow_id: None,
timeout_ms: None,
deadline_ms: None,
started_at_ms: None,
rate_limited: false,
delay_until_ms: None,
completed_at_ms: None,
forked_from: None,
authenticated_user: None,
assumed_role: None,
authenticated_roles: Vec::new(),
class_name: None,
config_name: None,
created_at: now,
updated_at: now,
}
}
}
#[derive(Clone)]
pub struct ListFilter {
pub workflow_ids: Vec<String>,
pub workflow_id_prefix: Vec<String>,
pub name: Vec<String>,
pub status: Vec<String>,
pub queue_name: Vec<String>,
pub app_version: Vec<String>,
pub executor_ids: Vec<String>,
pub authenticated_users: Vec<String>,
pub forked_from: Vec<String>,
pub parent_workflow_ids: Vec<String>,
pub was_forked_from: Option<bool>,
pub start_time_ms: Option<i64>,
pub end_time_ms: Option<i64>,
pub completed_after_ms: Option<i64>,
pub completed_before_ms: Option<i64>,
pub dequeued_after_ms: Option<i64>,
pub dequeued_before_ms: Option<i64>,
pub has_parent: Option<bool>,
pub limit: Option<i64>,
pub offset: Option<i64>,
pub sort_desc: bool,
pub queues_only: bool,
pub load_input: bool,
pub load_output: bool,
}
impl Default for ListFilter {
fn default() -> Self {
Self {
workflow_ids: Vec::new(),
workflow_id_prefix: Vec::new(),
name: Vec::new(),
status: Vec::new(),
queue_name: Vec::new(),
app_version: Vec::new(),
executor_ids: Vec::new(),
authenticated_users: Vec::new(),
forked_from: Vec::new(),
parent_workflow_ids: Vec::new(),
was_forked_from: None,
start_time_ms: None,
end_time_ms: None,
completed_after_ms: None,
completed_before_ms: None,
dequeued_after_ms: None,
dequeued_before_ms: None,
has_parent: None,
limit: None,
offset: None,
sort_desc: false,
queues_only: false,
load_input: true,
load_output: true,
}
}
}
#[derive(Clone, Default)]
pub struct WorkflowAggregateQuery {
pub by_status: bool,
pub by_name: bool,
pub by_queue_name: bool,
pub by_executor_id: bool,
pub by_app_version: bool,
pub select_count: bool,
pub select_min_created_at: bool,
pub select_max_queue_wait_ms: bool,
pub select_max_total_latency_ms: bool,
pub time_bucket_ms: Option<i64>,
pub status: Vec<String>,
pub name: Vec<String>,
pub app_version: Vec<String>,
pub executor_ids: Vec<String>,
pub queue_names: Vec<String>,
pub workflow_id_prefix: Option<String>,
pub start_time_ms: Option<i64>,
pub end_time_ms: Option<i64>,
pub completed_after_ms: Option<i64>,
pub completed_before_ms: Option<i64>,
pub dequeued_after_ms: Option<i64>,
pub dequeued_before_ms: Option<i64>,
pub limit: Option<i64>,
}
pub(crate) const AGG_DIMENSIONS: &[(&str, &str)] = &[
("status", "status"),
("name", "name"),
("queue_name", "queue_name"),
("executor_id", "executor_id"),
("application_version", "application_version"),
];
impl WorkflowAggregateQuery {
pub(crate) fn enabled_columns(&self) -> Vec<(&'static str, &'static str)> {
let flags = [
self.by_status,
self.by_name,
self.by_queue_name,
self.by_executor_id,
self.by_app_version,
];
AGG_DIMENSIONS
.iter()
.zip(flags)
.filter(|(_, on)| *on)
.map(|(d, _)| *d)
.collect()
}
pub fn is_empty(&self) -> bool {
self.enabled_columns().is_empty() && self.time_bucket_ms.is_none()
}
pub fn no_select(&self) -> bool {
!self.select_count
&& !self.select_min_created_at
&& !self.select_max_queue_wait_ms
&& !self.select_max_total_latency_ms
}
}
pub(crate) fn workflow_agg_selects(q: &WorkflowAggregateQuery) -> Vec<&'static str> {
let mut sel = Vec::new();
if q.select_count {
sel.push("COUNT(*) AS cnt");
}
if q.select_min_created_at {
sel.push("MIN(created_at) AS min_created_at");
}
if q.select_max_queue_wait_ms {
sel.push("MAX(started_at_epoch_ms - created_at) AS max_queue_wait_ms");
}
if q.select_max_total_latency_ms {
sel.push("MAX(completed_at - created_at) AS max_total_latency_ms");
}
sel
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WorkflowAggregate {
pub group: std::collections::BTreeMap<String, Option<String>>,
pub count: Option<i64>,
pub min_created_at: Option<i64>,
pub max_queue_wait_ms: Option<i64>,
pub max_total_latency_ms: Option<i64>,
}
pub(crate) const STEP_STATUS_EXPR: &str =
"(CASE WHEN error IS NULL THEN 'SUCCESS' ELSE 'ERROR' END)";
#[derive(Clone, Default)]
pub struct StepAggregateQuery {
pub by_function_name: bool,
pub by_status: bool,
pub select_count: bool,
pub select_max_duration_ms: bool,
pub time_bucket_ms: Option<i64>,
pub status: Vec<String>,
pub function_name: Vec<String>,
pub workflow_id_prefix: Option<String>,
pub completed_after_ms: Option<i64>,
pub completed_before_ms: Option<i64>,
pub limit: Option<i64>,
}
impl StepAggregateQuery {
pub(crate) fn group_exprs(&self) -> Vec<(&'static str, &'static str)> {
let mut v = Vec::new();
if self.by_function_name {
v.push(("function_name", "function_name"));
}
if self.by_status {
v.push(("status", STEP_STATUS_EXPR));
}
v
}
pub fn no_grouping(&self) -> bool {
!self.by_function_name && !self.by_status && self.time_bucket_ms.is_none()
}
pub fn no_select(&self) -> bool {
!self.select_count && !self.select_max_duration_ms
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StepAggregate {
pub group: std::collections::BTreeMap<String, Option<String>>,
pub count: Option<i64>,
pub max_duration_ms: Option<i64>,
}
#[derive(Clone, Debug)]
pub struct NotificationInfo {
pub topic: Option<String>,
pub message: Value,
pub created_at_ms: i64,
pub consumed: bool,
}
#[derive(Clone, Debug)]
pub struct StepInfo {
pub step_id: i32,
pub name: String,
pub output: Option<Value>,
pub error: Option<String>,
pub child_workflow_id: Option<String>,
pub started_at: Option<DateTime<Utc>>,
pub completed_at: Option<DateTime<Utc>>,
}
#[derive(Clone, Debug)]
pub enum StepOutcome {
Output(Value),
Failure {
message: String,
info: Option<crate::PortableWorkflowError>,
},
}
impl StepOutcome {
pub(crate) fn into_value_result(self) -> Result<Value> {
match self {
StepOutcome::Output(v) => Ok(v),
StepOutcome::Failure { message, info } => Err(match info {
Some(pe) => Error::Portable(Box::new(pe)),
None => Error::app(message),
}),
}
}
}
#[derive(Clone, Debug)]
pub struct RecordedStep {
pub name: String,
pub outcome: StepOutcome,
}
pub(crate) fn step_outcome_from(
serializer: &crate::serialize::Serializer,
fmt: Option<&str>,
output: Option<&str>,
error: Option<&str>,
) -> Result<Option<StepOutcome>> {
if let Some(err) = error {
let (message, info) = crate::serialize::decode_error(fmt, err);
return Ok(Some(StepOutcome::Failure { message, info }));
}
match output {
Some(o) => Ok(Some(StepOutcome::Output(crate::serialize::decode(
serializer, fmt, o,
)?))),
None => Ok(None),
}
}
#[derive(Clone, Debug)]
pub struct VersionInfo {
pub version_id: String,
pub version_name: String,
pub version_timestamp: DateTime<Utc>,
pub created_at: DateTime<Utc>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ExportedWorkflow {
#[serde(default)]
pub workflow_status: Map<String, Value>,
#[serde(default, deserialize_with = "null_seq")]
pub operation_outputs: Vec<Map<String, Value>>,
#[serde(default, deserialize_with = "null_seq")]
pub workflow_events: Vec<Map<String, Value>>,
#[serde(default, deserialize_with = "null_seq")]
pub workflow_events_history: Vec<Map<String, Value>>,
#[serde(default, deserialize_with = "null_seq")]
pub streams: Vec<Map<String, Value>>,
}
fn null_seq<'de, D, T>(d: D) -> std::result::Result<Vec<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: Deserialize<'de>,
{
Ok(Option::<Vec<T>>::deserialize(d)?.unwrap_or_default())
}
pub(crate) const EXPORT_STATUS_STR_COLS: &[&str] = &[
"workflow_uuid",
"status",
"name",
"authenticated_user",
"assumed_role",
"authenticated_roles",
"output",
"error",
"executor_id",
"application_version",
"application_id",
"class_name",
"config_name",
"queue_name",
"deduplication_id",
"inputs",
"queue_partition_key",
"forked_from",
"parent_workflow_id",
"serialization",
];
#[cfg(feature = "sqlite")]
pub(crate) const EXPORT_STATUS_INT_COLS: &[&str] = &[
"created_at",
"updated_at",
"recovery_attempts",
"workflow_timeout_ms",
"workflow_deadline_epoch_ms",
"started_at_epoch_ms",
"priority",
"delay_until_epoch_ms",
];
pub(crate) fn col_str(m: &Map<String, Value>, key: &str) -> Option<String> {
m.get(key).and_then(|v| v.as_str()).map(str::to_string)
}
pub(crate) fn col_i64(m: &Map<String, Value>, key: &str) -> Option<i64> {
m.get(key).and_then(Value::as_i64)
}
pub(crate) fn col_bool(m: &Map<String, Value>, key: &str) -> Option<bool> {
m.get(key).and_then(Value::as_bool)
}
#[derive(Clone, Debug)]
pub struct ForkParams {
pub original_id: String,
pub new_id: String,
pub start_step: i32,
pub app_version: Option<String>,
pub queue_name: String,
pub partition_key: Option<String>,
}
#[derive(Clone, Debug)]
pub struct DequeueRequest {
pub queue_name: String,
pub executor_id: String,
pub app_version: String,
pub partition_key: Option<String>,
pub max_tasks: i64,
pub global_concurrency: Option<i64>,
pub rate_limit_max: Option<i64>,
pub rate_limit_period_ms: Option<i64>,
}
#[async_trait]
pub trait StateProvider: Send + Sync {
async fn init(&self) -> Result<()>;
fn serializer(&self) -> crate::serialize::Serializer {
crate::serialize::Serializer::Json
}
fn supports_listen_notify(&self) -> bool {
false
}
async fn await_change(&self, wait: ChangeWait<'_>, within: std::time::Duration) {
let _ = wait;
tokio::time::sleep(within).await;
}
async fn insert_workflow_status(
&self,
status: WorkflowStatus,
) -> Result<(WorkflowStatus, bool)>;
async fn get_deduplicated_workflow(
&self,
queue_name: &str,
dedup_id: &str,
) -> Result<Option<String>>;
async fn get_workflow_status(&self, id: &str) -> Result<Option<WorkflowStatus>>;
async fn set_workflow_status(
&self,
id: &str,
status: &str,
output: Option<&Value>,
error: Option<&str>,
) -> Result<()>;
async fn get_step_result(&self, workflow_id: &str, seq: i32) -> Result<Option<RecordedStep>>;
async fn record_step_result(
&self,
workflow_id: &str,
seq: i32,
name: &str,
value: Value,
error: Option<&str>,
started_at_ms: Option<i64>,
) -> Result<StepOutcome>;
async fn run_transaction_step(
&self,
workflow_id: &str,
seq: i32,
started_at_ms: i64,
opts: &TransactionOptions,
body: TxBody<'_>,
) -> Result<Value>;
async fn dequeue_workflows(&self, req: &DequeueRequest) -> Result<Vec<WorkflowStatus>>;
async fn transition_delayed_workflows(&self, now_ms: i64) -> Result<u64>;
async fn queue_partitions(&self, queue_name: &str) -> Result<Vec<String>>;
async fn insert_notification(
&self,
destination_id: &str,
topic: &str,
message: Value,
idempotency_key: Option<&str>,
) -> Result<()>;
async fn consume_notification(
&self,
workflow_id: &str,
topic: &str,
seq: i32,
step_name: &str,
) -> Result<Option<Value>>;
async fn upsert_event(&self, workflow_id: &str, key: &str, value: Value) -> Result<()>;
async fn get_event_value(&self, workflow_id: &str, key: &str) -> Result<Option<Value>>;
async fn list_workflows(&self, filter: &ListFilter) -> Result<Vec<WorkflowStatus>>;
async fn get_workflow_aggregates(
&self,
query: &WorkflowAggregateQuery,
) -> Result<Vec<WorkflowAggregate>>;
async fn get_step_aggregates(&self, query: &StepAggregateQuery) -> Result<Vec<StepAggregate>>;
async fn cancel_workflow(&self, id: &str) -> Result<()>;
async fn resume_workflow(&self, id: &str) -> Result<bool>;
async fn enqueue_existing(&self, id: &str, queue: &str) -> Result<()>;
async fn cancel_workflows(&self, ids: &[String]) -> Result<()>;
async fn resume_workflows(&self, ids: &[String]) -> Result<Vec<String>>;
async fn delete_workflows(&self, ids: &[String], delete_children: bool) -> Result<()>;
async fn set_workflow_delay(&self, id: &str, delay_until_ms: i64) -> Result<bool>;
async fn fork_workflow(&self, params: &ForkParams) -> Result<()>;
async fn bump_recovery_attempts(&self, id: &str, max: i32) -> Result<i32>;
async fn record_child_workflow(
&self,
parent_id: &str,
seq: i32,
name: &str,
child_id: &str,
) -> Result<()>;
async fn check_child_workflow(
&self,
parent_id: &str,
seq: i32,
) -> Result<Option<(String, String)>>;
async fn get_workflow_steps(&self, workflow_id: &str) -> Result<Vec<StepInfo>>;
async fn get_step_name(&self, workflow_id: &str, seq: i32) -> Result<Option<String>>;
async fn record_patch(&self, workflow_id: &str, seq: i32, name: &str) -> Result<()>;
async fn write_stream(
&self,
workflow_id: &str,
key: &str,
value: Option<Value>,
function_id: i32,
) -> Result<()>;
async fn read_stream(
&self,
workflow_id: &str,
key: &str,
from_offset: i32,
) -> Result<(Vec<Value>, bool)>;
async fn list_workflow_events(&self, workflow_id: &str) -> Result<Vec<(String, Value)>>;
async fn list_workflow_notifications(&self, workflow_id: &str)
-> Result<Vec<NotificationInfo>>;
async fn list_workflow_streams(&self, workflow_id: &str) -> Result<Vec<(String, Vec<Value>)>>;
async fn create_schedule(&self, schedule: &WorkflowSchedule) -> Result<()>;
async fn apply_schedules(&self, schedules: &[WorkflowSchedule]) -> Result<()>;
async fn list_schedules(&self, filter: &ScheduleFilter) -> Result<Vec<WorkflowSchedule>>;
async fn set_schedule_status(&self, name: &str, status: ScheduleStatus) -> Result<bool>;
async fn set_schedule_last_fired(&self, name: &str, at_ms: i64) -> Result<()>;
async fn delete_schedule(&self, name: &str) -> Result<bool>;
async fn create_application_version(&self, version_name: &str) -> Result<()>;
async fn list_application_versions(&self) -> Result<Vec<VersionInfo>>;
async fn get_latest_application_version(&self) -> Result<Option<VersionInfo>>;
async fn set_latest_application_version(&self, version_name: &str) -> Result<bool>;
async fn upsert_queue(&self, queue: &crate::WorkflowQueue, update_existing: bool)
-> Result<()>;
async fn list_queues(&self) -> Result<Vec<crate::WorkflowQueue>>;
async fn export_workflow(
&self,
workflow_id: &str,
export_children: bool,
) -> Result<Vec<ExportedWorkflow>>;
async fn import_workflow(&self, workflows: &[ExportedWorkflow]) -> Result<()>;
}
#[cfg(test)]
mod tests {
use super::{decode_roles, drain_stream_from, encode_roles, StreamBackend, STATUS_SUCCESS};
use crate::error::Result;
use async_trait::async_trait;
use serde_json::{json, Value};
use std::sync::atomic::{AtomicUsize, Ordering};
struct ScriptedStream {
reads: AtomicUsize,
}
#[async_trait]
impl StreamBackend for ScriptedStream {
async fn stream_entries(
&self,
_workflow_id: &str,
_key: &str,
_from_offset: i32,
) -> Result<(Vec<Value>, bool)> {
if self.reads.fetch_add(1, Ordering::SeqCst) == 0 {
Ok((vec![], false))
} else {
Ok((vec![json!("final")], false))
}
}
async fn producer_status(&self, _workflow_id: &str) -> Result<Option<String>> {
Ok(Some(STATUS_SUCCESS.to_string()))
}
}
#[tokio::test]
async fn drain_stream_drains_value_committed_before_producer_inactive() {
let source = ScriptedStream {
reads: AtomicUsize::new(0),
};
let (values, closed): (Vec<String>, bool) =
drain_stream_from(&source, "wf", "stream").await.unwrap();
assert!(
closed,
"stream is reported closed once the producer is terminal"
);
assert_eq!(
values,
vec!["final".to_string()],
"the value committed just before the producer went inactive must not be dropped",
);
}
#[test]
fn roles_round_trip_as_json_array() {
assert_eq!(encode_roles(&[]), None);
assert!(decode_roles(None).is_empty());
let roles = vec!["admin".to_string(), "user".to_string()];
let stored = encode_roles(&roles).expect("non-empty roles encode to Some");
assert_eq!(stored, r#"["admin","user"]"#);
assert_eq!(decode_roles(Some(&stored)), roles);
}
#[test]
fn decode_roles_tolerates_garbage() {
assert!(decode_roles(Some("not json")).is_empty());
}
}