use super::Error;
use std::fmt;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
#[derive(
Debug,
Clone,
Copy,
Default,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
serde::Serialize,
serde::Deserialize,
)]
pub struct Timestamp(i64);
impl Timestamp {
pub const fn from_epoch_ms(ms: i64) -> Self {
Self(ms)
}
pub const fn as_epoch_ms(self) -> i64 {
self.0
}
pub fn now() -> Self {
Self(
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0),
)
}
pub fn to_system_time(self) -> Option<SystemTime> {
u64::try_from(self.0)
.ok()
.map(|ms| UNIX_EPOCH + Duration::from_millis(ms))
}
pub fn from_system_time(time: SystemTime) -> Option<Self> {
time.duration_since(UNIX_EPOCH)
.ok()
.and_then(|d| i64::try_from(d.as_millis()).ok())
.map(Self)
}
pub fn checked_add(self, duration: Duration) -> Option<Self> {
i64::try_from(duration.as_millis())
.ok()
.and_then(|ms| self.0.checked_add(ms))
.map(Self)
}
pub fn duration_since(self, earlier: Self) -> Option<Duration> {
self.0
.checked_sub(earlier.0)
.and_then(|ms| u64::try_from(ms).ok())
.map(Duration::from_millis)
}
pub fn parse_iso8601(text: &str) -> Option<Self> {
let parsed = OffsetDateTime::parse(text, &Rfc3339).ok()?;
i64::try_from(parsed.unix_timestamp_nanos().div_euclid(1_000_000))
.ok()
.map(Self)
}
pub fn to_iso8601(self) -> String {
OffsetDateTime::from_unix_timestamp_nanos(i128::from(self.0) * 1_000_000)
.ok()
.and_then(|t| t.format(&Rfc3339).ok())
.unwrap_or_default()
}
}
impl fmt::Display for Timestamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}ms", self.0)
}
}
pub fn duration_from_ms(ms: i64) -> Option<Duration> {
u64::try_from(ms).ok().map(Duration::from_millis)
}
pub fn duration_from_secs(secs: f64) -> Option<Duration> {
Duration::try_from_secs_f64(secs).ok()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum WorkflowStatus {
Pending,
Enqueued,
Delayed,
Success,
Error,
Cancelled,
MaxRecoveryAttemptsExceeded,
}
impl WorkflowStatus {
pub fn as_str(self) -> &'static str {
match self {
WorkflowStatus::Pending => "PENDING",
WorkflowStatus::Enqueued => "ENQUEUED",
WorkflowStatus::Delayed => "DELAYED",
WorkflowStatus::Success => "SUCCESS",
WorkflowStatus::Error => "ERROR",
WorkflowStatus::Cancelled => "CANCELLED",
WorkflowStatus::MaxRecoveryAttemptsExceeded => "MAX_RECOVERY_ATTEMPTS_EXCEEDED",
}
}
pub fn parse(value: &str) -> Option<Self> {
Some(match value {
"PENDING" => WorkflowStatus::Pending,
"ENQUEUED" => WorkflowStatus::Enqueued,
"DELAYED" => WorkflowStatus::Delayed,
"SUCCESS" => WorkflowStatus::Success,
"ERROR" => WorkflowStatus::Error,
"CANCELLED" => WorkflowStatus::Cancelled,
"MAX_RECOVERY_ATTEMPTS_EXCEEDED" => WorkflowStatus::MaxRecoveryAttemptsExceeded,
_ => return None,
})
}
pub fn is_terminal(self) -> bool {
matches!(
self,
WorkflowStatus::Success | WorkflowStatus::Error | WorkflowStatus::Cancelled
)
}
}
impl fmt::Display for WorkflowStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct WorkflowRecord {
pub workflow_id: String,
pub status: WorkflowStatus,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub class_name: Option<String>,
#[serde(default)]
pub config_name: Option<String>,
#[serde(default)]
pub input: Option<String>,
#[serde(default)]
pub output: Option<String>,
#[serde(default)]
pub error: Option<String>,
#[serde(default)]
pub serialization: Option<String>,
#[serde(default)]
pub executor_id: Option<String>,
#[serde(default)]
pub application_version: Option<String>,
#[serde(default)]
pub recovery_attempts: i64,
#[serde(default)]
pub queue_name: Option<String>,
#[serde(default)]
pub created_at: Timestamp,
#[serde(default)]
pub updated_at: Timestamp,
#[serde(default)]
pub started_at: Option<Timestamp>,
#[serde(default)]
pub completed_at: Option<Timestamp>,
#[serde(default)]
pub forked_from: Option<String>,
#[serde(default)]
pub parent_workflow_id: Option<String>,
#[serde(default)]
pub was_forked_from: bool,
#[serde(default)]
pub owner_xid: Option<String>,
#[serde(default)]
pub application_id: Option<String>,
#[serde(default)]
pub authenticated_user: Option<String>,
#[serde(default)]
pub authenticated_roles: Vec<String>,
#[serde(default)]
pub assumed_role: Option<String>,
#[serde(default)]
pub request: Option<String>,
#[serde(default)]
pub application_name: Option<String>,
#[serde(default)]
pub deduplication_id: Option<String>,
#[serde(default)]
pub priority: i32,
#[serde(default)]
pub queue_partition_key: Option<String>,
#[serde(default)]
pub rate_limited: bool,
#[serde(default)]
pub schedule_name: Option<String>,
#[serde(default)]
pub timeout: Option<Duration>,
#[serde(default)]
pub deadline: Option<Timestamp>,
#[serde(default)]
pub delay_until: Option<Timestamp>,
#[serde(default)]
pub debounce_deadline: Option<Timestamp>,
#[serde(default)]
pub is_debounced: bool,
#[serde(default)]
pub attributes: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct NewWorkflow<'a> {
pub workflow_id: &'a str,
pub name: Option<&'a str>,
pub class_name: Option<&'a str>,
pub config_name: Option<&'a str>,
pub input: Option<&'a str>,
pub serialization: Option<&'a str>,
pub queue_name: Option<&'a str>,
pub deduplication_id: Option<&'a str>,
pub priority: i32,
pub queue_partition_key: Option<&'a str>,
pub delay: Option<Duration>,
pub is_debounced: bool,
pub debounce_deadline: Option<Timestamp>,
pub timeout: Option<Duration>,
pub deadline: Option<Timestamp>,
pub executor_id: Option<&'a str>,
pub application_name: Option<&'a str>,
pub application_version: Option<&'a str>,
pub application_id: Option<&'a str>,
pub authenticated_user: Option<&'a str>,
pub authenticated_roles: Vec<&'a str>,
pub assumed_role: Option<&'a str>,
pub schedule_name: Option<&'a str>,
pub attributes: Option<&'a str>,
}
impl<'a> NewWorkflow<'a> {
pub fn new(workflow_id: &'a str) -> Self {
Self {
workflow_id,
..Self::default()
}
}
pub fn validate(&self) -> Result<(), Error> {
if self.workflow_id.is_empty() {
return Err(Error::InvalidInput {
field: "workflow_id".into(),
detail: "must not be empty".to_owned(),
});
}
for (field, value) in [
("name", &self.name),
("class_name", &self.class_name),
("config_name", &self.config_name),
("queue_name", &self.queue_name),
("deduplication_id", &self.deduplication_id),
("queue_partition_key", &self.queue_partition_key),
("schedule_name", &self.schedule_name),
("input", &self.input),
("serialization", &self.serialization),
("application_version", &self.application_version),
] {
if *value == Some("") {
return Err(Error::InvalidInput {
field: field.into(),
detail: "must be absent rather than empty".to_owned(),
});
}
}
for (field, value) in [("delay", self.delay), ("timeout", self.timeout)] {
if value == Some(Duration::ZERO) {
return Err(Error::InvalidInput {
field: field.into(),
detail: "must be a positive, non-zero duration".to_owned(),
});
}
}
validate_attributes(self.attributes)?;
Ok(())
}
pub fn initial_status(&self) -> WorkflowStatus {
match (&self.queue_name, &self.delay) {
(None, _) => WorkflowStatus::Pending,
(Some(_), None) => WorkflowStatus::Enqueued,
(Some(_), Some(_)) => WorkflowStatus::Delayed,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_instant_formats_as_iso8601() {
let iso = |ms| Timestamp::from_epoch_ms(ms).to_iso8601();
assert_eq!(iso(0), "1970-01-01T00:00:00Z");
assert_eq!(iso(1_786_492_800_000), "2026-08-12T00:00:00Z");
assert_eq!(iso(1_786_492_800_123), "2026-08-12T00:00:00.123Z");
assert_eq!(iso(-1), "1969-12-31T23:59:59.999Z");
assert_eq!(iso(-86_400_000), "1969-12-31T00:00:00Z");
}
#[test]
fn an_iso8601_instant_parses_from_every_implementations_format() {
let parse = Timestamp::parse_iso8601;
let expected = Some(Timestamp::from_epoch_ms(1_786_492_800_000));
assert_eq!(parse("2026-08-12T00:00:00.000Z"), expected, "TypeScript");
assert_eq!(parse("2026-08-12T00:00:00Z"), expected, "Go and Java");
assert_eq!(parse("2026-08-12T00:00:00+00:00"), expected, "Python");
assert_eq!(
parse("2026-08-12T00:00:00.000000000Z"),
expected,
"RFC3339Nano"
);
assert_eq!(parse("2026-08-12T01:30:00+01:30"), expected);
assert_eq!(parse("2026-08-11T19:00:00-05:00"), expected);
assert_eq!(parse("2026-08-12T00:00:00+00:00"), expected);
assert_eq!(
parse("2026-08-12T00:00:00.123+00:00"),
Some(Timestamp::from_epoch_ms(1_786_492_800_123))
);
let ms = |t: Option<Timestamp>| t.unwrap().as_epoch_ms() % 1_000;
assert_eq!(ms(parse("2026-08-12T00:00:00.5Z")), 500);
assert_eq!(ms(parse("2026-08-12T00:00:00.123999Z")), 123);
for bad in [
"",
"2026-08-12",
"2026-08-12T00:00",
"not-a-date-at-all",
"2026-13-01T00:00:00Z",
"2026-08-12T24:00:00Z",
"2026-08-12T00:00:00",
"2026-08-12T00:00:00+0130",
] {
assert_eq!(parse(bad), None, "{bad:?} is not an instant this reads");
}
}
#[test]
fn an_iso8601_instant_round_trips() {
for ms in [
0,
-1,
1_786_492_800_000,
1_709_209_845_123,
951_827_445_999,
-2_203_977_600_000,
] {
let instant = Timestamp::from_epoch_ms(ms);
assert_eq!(
Timestamp::parse_iso8601(&instant.to_iso8601()),
Some(instant),
"{ms} did not survive the round trip"
);
}
}
#[test]
fn a_deadline_is_a_start_plus_a_timeout() {
let start = Timestamp::from_epoch_ms(1_700_000_000_000);
let timeout = Duration::from_secs(30);
let deadline = start.checked_add(timeout).unwrap();
assert_eq!(deadline.as_epoch_ms(), 1_700_000_030_000);
assert_eq!(deadline.duration_since(start), Some(timeout));
assert_eq!(start.duration_since(deadline), None);
}
#[test]
fn timestamps_round_trip_through_system_time() {
let t = Timestamp::from_epoch_ms(1_700_000_000_123);
let round_tripped = Timestamp::from_system_time(t.to_system_time().unwrap()).unwrap();
assert_eq!(round_tripped, t);
}
#[test]
fn an_unstorable_system_time_is_not_a_timestamp() {
assert_eq!(
Timestamp::from_system_time(UNIX_EPOCH - Duration::from_secs(1)),
None,
"before the epoch"
);
let far = UNIX_EPOCH
.checked_add(Duration::from_secs(i64::MAX as u64 / 100))
.expect("a SystemTime that far out is constructible");
assert_eq!(Timestamp::from_system_time(far), None, "too far after it");
}
#[test]
fn a_pre_epoch_instant_does_not_convert() {
assert_eq!(Timestamp::from_epoch_ms(-1).to_system_time(), None);
}
#[test]
fn durations_read_from_either_stored_unit() {
assert_eq!(duration_from_ms(1_500), Some(Duration::from_millis(1_500)));
assert_eq!(duration_from_secs(1.5), Some(Duration::from_millis(1_500)));
assert_eq!(duration_from_ms(-1), None);
assert_eq!(duration_from_secs(-1.0), None);
assert_eq!(duration_from_secs(f64::NAN), None);
assert_eq!(duration_from_secs(f64::INFINITY), None);
assert_eq!(duration_from_secs(1e300), None);
}
#[test]
fn both_readers_accept_zero() {
assert_eq!(duration_from_ms(0), Some(Duration::ZERO));
assert_eq!(duration_from_secs(0.0), Some(Duration::ZERO));
assert_eq!(duration_from_secs(-0.0), Some(Duration::ZERO));
}
#[test]
fn status_round_trips_through_its_stored_spelling() {
for status in [
WorkflowStatus::Pending,
WorkflowStatus::Enqueued,
WorkflowStatus::Delayed,
WorkflowStatus::Success,
WorkflowStatus::Error,
WorkflowStatus::Cancelled,
WorkflowStatus::MaxRecoveryAttemptsExceeded,
] {
assert_eq!(WorkflowStatus::parse(status.as_str()), Some(status));
}
}
#[test]
fn stored_spellings_match_the_other_implementations() {
assert_eq!(WorkflowStatus::Pending.as_str(), "PENDING");
assert_eq!(WorkflowStatus::Enqueued.as_str(), "ENQUEUED");
assert_eq!(WorkflowStatus::Delayed.as_str(), "DELAYED");
assert_eq!(WorkflowStatus::Success.as_str(), "SUCCESS");
assert_eq!(WorkflowStatus::Error.as_str(), "ERROR");
assert_eq!(WorkflowStatus::Cancelled.as_str(), "CANCELLED");
assert_eq!(
WorkflowStatus::MaxRecoveryAttemptsExceeded.as_str(),
"MAX_RECOVERY_ATTEMPTS_EXCEEDED",
);
}
#[test]
fn an_unrecognised_status_does_not_parse() {
assert_eq!(WorkflowStatus::parse("SOMETHING_NEWER"), None);
assert_eq!(WorkflowStatus::parse("pending"), None, "matching is exact");
}
#[test]
fn terminal_states_are_the_three_that_stop_execution() {
assert!(WorkflowStatus::Success.is_terminal());
assert!(WorkflowStatus::Error.is_terminal());
assert!(WorkflowStatus::Cancelled.is_terminal());
assert!(!WorkflowStatus::Pending.is_terminal());
assert!(!WorkflowStatus::Enqueued.is_terminal());
assert!(!WorkflowStatus::Delayed.is_terminal());
assert!(!WorkflowStatus::MaxRecoveryAttemptsExceeded.is_terminal());
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum Applications<'a> {
#[default]
Unset,
Any,
Named(Vec<&'a str>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkflowFilter<'a> {
pub workflow_ids: Vec<&'a str>,
pub workflow_id_prefixes: Vec<&'a str>,
pub applications: Applications<'a>,
pub names: Vec<&'a str>,
pub class_names: Vec<&'a str>,
pub config_names: Vec<&'a str>,
pub status: Vec<WorkflowStatus>,
pub application_versions: Vec<&'a str>,
pub executor_ids: Vec<&'a str>,
pub authenticated_users: Vec<&'a str>,
pub queue_names: Vec<&'a str>,
pub queues_only: bool,
pub schedule_names: Vec<&'a str>,
pub deduplication_ids: Vec<&'a str>,
pub is_debounced: Option<bool>,
pub parent_workflow_ids: Vec<&'a str>,
pub has_parent: Option<bool>,
pub forked_from: Vec<&'a str>,
pub was_forked_from: Option<bool>,
pub created_after: Option<Timestamp>,
pub created_before: Option<Timestamp>,
pub completed_after: Option<Timestamp>,
pub completed_before: Option<Timestamp>,
pub started_after: Option<Timestamp>,
pub started_before: Option<Timestamp>,
pub attributes: Option<&'a str>,
pub limit: Option<i64>,
pub offset: Option<i64>,
pub sort_desc: bool,
pub load_input: bool,
pub load_output: bool,
}
impl Default for WorkflowFilter<'_> {
fn default() -> Self {
Self {
workflow_ids: Vec::new(),
workflow_id_prefixes: Vec::new(),
applications: Applications::Unset,
names: Vec::new(),
class_names: Vec::new(),
config_names: Vec::new(),
status: Vec::new(),
application_versions: Vec::new(),
executor_ids: Vec::new(),
authenticated_users: Vec::new(),
queue_names: Vec::new(),
queues_only: false,
schedule_names: Vec::new(),
deduplication_ids: Vec::new(),
is_debounced: None,
parent_workflow_ids: Vec::new(),
has_parent: None,
forked_from: Vec::new(),
was_forked_from: None,
created_after: None,
created_before: None,
completed_after: None,
completed_before: None,
started_after: None,
started_before: None,
attributes: None,
limit: None,
offset: None,
sort_desc: false,
load_input: true,
load_output: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct StepRecord {
pub workflow_id: String,
pub step_id: i32,
pub step_name: String,
#[serde(default)]
pub output: Option<String>,
#[serde(default)]
pub error: Option<String>,
#[serde(default)]
pub child_workflow_id: Option<String>,
#[serde(default)]
pub serialization: Option<String>,
#[serde(default)]
pub started_at: Option<Timestamp>,
#[serde(default)]
pub completed_at: Option<Timestamp>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Outcome<'a> {
Output(Option<&'a str>),
Error(&'a str),
}
impl<'a> Outcome<'a> {
pub fn status(self) -> WorkflowStatus {
match self {
Outcome::Output(_) => WorkflowStatus::Success,
Outcome::Error(_) => WorkflowStatus::Error,
}
}
pub(crate) fn columns(self) -> (Option<&'a str>, Option<&'a str>) {
match self {
Outcome::Output(value) => (value, None),
Outcome::Error(message) => (None, Some(message)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AwaitedOutcome {
Succeeded {
output: Option<String>,
serialization: Option<String>,
},
Failed {
error: String,
serialization: Option<String>,
},
Cancelled,
Parked {
recovery_attempts: i64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StepTiming {
pub started_at: Timestamp,
pub completed_at: Timestamp,
}
pub(crate) fn validate_attributes(attributes: Option<&str>) -> Result<(), Error> {
let Some(json) = attributes else {
return Ok(());
};
serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(json).map_err(|e| {
Error::InvalidInput {
field: "attributes".into(),
detail: format!("must be a JSON object: {e}"),
}
})?;
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorkflowDelay {
For(Duration),
Until(Timestamp),
}
impl WorkflowDelay {
pub(crate) fn resolve(self, now: Timestamp) -> Timestamp {
match self {
WorkflowDelay::For(d) => {
Timestamp::from_epoch_ms(now.as_epoch_ms() + d.as_millis() as i64)
}
WorkflowDelay::Until(t) => t,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Submission {
#[default]
Fresh,
Recovery,
Dequeue,
}
impl Submission {
pub(crate) fn claims_ownership(self) -> bool {
matches!(self, Submission::Recovery | Submission::Dequeue)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutcomeWrite {
Recorded,
AlreadyFinished,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkflowInitResult {
pub status: WorkflowStatus,
pub recovery_attempts: i64,
pub deadline: Option<Timestamp>,
pub serialization: Option<String>,
pub should_execute: bool,
}
pub fn is_valid_application_name(name: &str) -> bool {
(3..=256).contains(&name.len())
&& name
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-' || b == b'_')
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenameFrom<'a> {
Application(&'a str),
ApplicationAndUnclaimed(&'a str),
Unclaimed,
}
impl<'a> RenameFrom<'a> {
pub fn application(self) -> Option<&'a str> {
match self {
RenameFrom::Application(name) | RenameFrom::ApplicationAndUnclaimed(name) => Some(name),
RenameFrom::Unclaimed => None,
}
}
}
pub const DEFAULT_RENAME_BATCH_SIZE: u32 = 10_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenameBatching {
Unbatched,
Batched(u32),
}
impl Default for RenameBatching {
fn default() -> Self {
RenameBatching::Batched(DEFAULT_RENAME_BATCH_SIZE)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ApplicationRowCounts {
pub queues: u64,
pub schedules: u64,
pub versions: u64,
pub workflows: u64,
pub steps: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RateLimit {
pub limit: i32,
pub period: Duration,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Change<T> {
Leave,
Set(T),
}
impl<T> Default for Change<T> {
fn default() -> Self {
Change::Leave
}
}
impl<T> Change<T> {
pub fn set(self) -> Option<T> {
match self {
Change::Set(value) => Some(value),
Change::Leave => None,
}
}
pub fn is_leave(&self) -> bool {
matches!(self, Change::Leave)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct QueueUpdate {
pub concurrency: Change<Option<i32>>,
pub worker_concurrency: Change<Option<i32>>,
pub rate_limit: Change<Option<RateLimit>>,
pub priority_enabled: Change<bool>,
pub partition_queue: Change<bool>,
pub partition_concurrency: Change<Option<i32>>,
pub partition_worker_concurrency: Change<Option<i32>>,
pub partition_rate_limit: Change<Option<RateLimit>>,
pub polling_interval: Change<Duration>,
}
impl QueueUpdate {
pub fn apply_to(&self, record: &QueueRecord) -> QueueRecord {
let partition_concurrency = self
.partition_concurrency
.set()
.unwrap_or(record.partition_concurrency);
let partition_worker_concurrency = self
.partition_worker_concurrency
.set()
.unwrap_or(record.partition_worker_concurrency);
let partition_rate_limit = self
.partition_rate_limit
.set()
.unwrap_or(record.partition_rate_limit);
let partition_queue = match self.partition_queue.set() {
Some(flag) => flag,
None if !(self.partition_concurrency.is_leave()
&& self.partition_worker_concurrency.is_leave()
&& self.partition_rate_limit.is_leave()) =>
{
partition_concurrency.is_some()
|| partition_worker_concurrency.is_some()
|| partition_rate_limit.is_some()
}
None => record.partition_queue,
};
QueueRecord {
name: record.name.clone(),
concurrency: self.concurrency.set().unwrap_or(record.concurrency),
worker_concurrency: self
.worker_concurrency
.set()
.unwrap_or(record.worker_concurrency),
rate_limit: self.rate_limit.set().unwrap_or(record.rate_limit),
priority_enabled: self
.priority_enabled
.set()
.unwrap_or(record.priority_enabled),
partition_queue,
partition_concurrency,
partition_worker_concurrency,
partition_rate_limit,
polling_interval: self
.polling_interval
.set()
.unwrap_or(record.polling_interval),
application_name: record.application_name.clone(),
}
}
pub fn is_empty(&self) -> bool {
self.concurrency.is_leave()
&& self.worker_concurrency.is_leave()
&& self.rate_limit.is_leave()
&& self.priority_enabled.is_leave()
&& self.partition_queue.is_leave()
&& self.partition_concurrency.is_leave()
&& self.partition_worker_concurrency.is_leave()
&& self.partition_rate_limit.is_leave()
&& self.polling_interval.is_leave()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DebounceRequest<'a> {
pub workflow_name: &'a str,
pub class_name: Option<&'a str>,
pub config_name: Option<&'a str>,
pub queue_name: &'a str,
pub deduplication_id: &'a str,
pub delay_until: Timestamp,
pub inputs: Option<&'a str>,
pub serialization: Option<&'a str>,
pub application_name: Option<&'a str>,
}
impl DebounceRequest<'_> {
pub fn validate(&self) -> Result<(), Error> {
for (field, value) in [
("workflow_name", self.workflow_name),
("queue_name", self.queue_name),
("deduplication_id", self.deduplication_id),
] {
if value.is_empty() {
return Err(Error::InvalidInput {
field: field.into(),
detail: "must not be empty".to_owned(),
});
}
}
for (field, value) in [
("class_name", self.class_name),
("config_name", self.config_name),
("inputs", self.inputs),
("serialization", self.serialization),
("application_name", self.application_name),
] {
if value == Some("") {
return Err(Error::InvalidInput {
field: field.into(),
detail: "must be absent rather than empty".to_owned(),
});
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Debounce {
Bounced {
workflow_id: String,
},
Held(DebounceHolder),
Unheld,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct DebounceHolder {
pub workflow_id: String,
pub is_debounced: bool,
pub workflow_name: Option<String>,
pub class_name: Option<String>,
pub config_name: Option<String>,
pub application_name: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueueRecord {
pub name: String,
pub concurrency: Option<i32>,
pub worker_concurrency: Option<i32>,
pub rate_limit: Option<RateLimit>,
pub priority_enabled: bool,
pub partition_queue: bool,
pub partition_concurrency: Option<i32>,
pub partition_worker_concurrency: Option<i32>,
pub partition_rate_limit: Option<RateLimit>,
pub polling_interval: Duration,
pub application_name: Option<String>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ResolvedLimits {
pub concurrency: Option<i32>,
pub worker_concurrency: Option<i32>,
pub rate_limit: Option<RateLimit>,
pub partition_concurrency: Option<i32>,
pub partition_worker_concurrency: Option<i32>,
pub partition_rate_limit: Option<RateLimit>,
}
impl ResolvedLimits {
pub fn is_partitioned(&self) -> bool {
self.partition_concurrency.is_some()
|| self.partition_worker_concurrency.is_some()
|| self.partition_rate_limit.is_some()
}
}
impl QueueRecord {
pub fn has_partition_limits(&self) -> bool {
self.partition_concurrency.is_some()
|| self.partition_worker_concurrency.is_some()
|| self.partition_rate_limit.is_some()
}
pub fn is_legacy_partitioned(&self) -> bool {
self.partition_queue && !self.has_partition_limits()
}
pub fn resolved_limits(&self) -> ResolvedLimits {
if self.is_legacy_partitioned() {
return ResolvedLimits {
partition_concurrency: self.concurrency,
partition_worker_concurrency: self.worker_concurrency,
partition_rate_limit: self.rate_limit,
..ResolvedLimits::default()
};
}
ResolvedLimits {
concurrency: self.concurrency,
worker_concurrency: self.worker_concurrency,
rate_limit: self.rate_limit,
partition_concurrency: self.partition_concurrency,
partition_worker_concurrency: self.partition_worker_concurrency,
partition_rate_limit: self.partition_rate_limit,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NewQueue<'a> {
pub name: &'a str,
pub concurrency: Option<i32>,
pub worker_concurrency: Option<i32>,
pub rate_limit: Option<RateLimit>,
pub priority_enabled: bool,
pub partition_queue: bool,
pub partition_concurrency: Option<i32>,
pub partition_worker_concurrency: Option<i32>,
pub partition_rate_limit: Option<RateLimit>,
pub polling_interval: Duration,
pub application_name: Option<&'a str>,
}
impl<'a> NewQueue<'a> {
pub fn new(name: &'a str) -> Self {
Self {
name,
concurrency: None,
worker_concurrency: None,
rate_limit: None,
priority_enabled: false,
partition_queue: false,
partition_concurrency: None,
partition_worker_concurrency: None,
partition_rate_limit: None,
polling_interval: Duration::from_secs(1),
application_name: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OnExistingQueue {
Update,
Leave,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VersionInfo {
pub application_name: Option<String>,
pub version_id: String,
pub version_name: String,
pub version_timestamp: Timestamp,
pub created_at: Timestamp,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NotificationRecord {
pub message_uuid: String,
pub topic: Option<String>,
pub message: String,
pub serialization: Option<String>,
pub created_at: Timestamp,
pub consumed: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GetEventCaller<'a> {
pub workflow_id: &'a str,
pub step_id: i32,
pub timeout_step_id: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InitWorkflowCaller<'a> {
pub parent_workflow_id: &'a str,
pub step_id: i32,
pub step_name: &'a str,
pub started_at: Timestamp,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EncodedValue {
pub value: String,
pub serialization: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EventRecord {
pub key: String,
pub value: String,
pub serialization: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StreamRead {
pub status: WorkflowStatus,
pub value: Option<EncodedValue>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StreamRecord {
pub key: String,
pub offset: i32,
pub value: String,
pub serialization: Option<String>,
pub step_id: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Fork<'a> {
pub source_id: &'a str,
pub forked_id: Option<&'a str>,
pub start_step: i32,
}
impl<'a> Fork<'a> {
pub fn new(source_id: &'a str) -> Self {
Self {
source_id,
forked_id: None,
start_step: 0,
}
}
pub(crate) fn validate(&self) -> Result<(), Error> {
if self.source_id.is_empty() {
return Err(Error::InvalidInput {
field: "source_id".into(),
detail: "must not be empty".to_owned(),
});
}
if self.forked_id == Some("") {
return Err(Error::InvalidInput {
field: "forked_id".into(),
detail: "must be absent rather than empty".to_owned(),
});
}
if self.start_step < 0 {
return Err(Error::InvalidInput {
field: "start_step".into(),
detail: "must not be negative".to_owned(),
});
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ForkPoint<'a> {
LastFailure,
LastStep,
Step(i32),
StepNamed(&'a str),
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ForkOptions<'a> {
pub application_version: Option<&'a str>,
pub queue_name: Option<&'a str>,
pub queue_partition_key: Option<&'a str>,
pub timeout: Option<Duration>,
pub replacement_children: &'a [(&'a str, &'a str)],
}
impl ForkOptions<'_> {
pub(crate) fn validate(&self) -> Result<(), Error> {
for (field, value) in [
("application_version", &self.application_version),
("queue_name", &self.queue_name),
("queue_partition_key", &self.queue_partition_key),
] {
if *value == Some("") {
return Err(Error::InvalidInput {
field: field.into(),
detail: "must be absent rather than empty".to_owned(),
});
}
}
if self.timeout == Some(Duration::ZERO) {
return Err(Error::InvalidInput {
field: "timeout".into(),
detail: "must be absent rather than zero".to_owned(),
});
}
for (index, (original, _)) in self.replacement_children.iter().enumerate() {
if original.is_empty() {
return Err(Error::InvalidInput {
field: "replacement_children".into(),
detail: "a replaced child id must not be empty".to_owned(),
});
}
if self.replacement_children[..index]
.iter()
.any(|(earlier, _)| earlier == original)
{
return Err(Error::InvalidInput {
field: "replacement_children".into(),
detail: format!("{original} is replaced more than once"),
});
}
}
Ok(())
}
}
pub mod step_names {
pub const SLEEP: &str = "DBOS.sleep";
pub const SEND: &str = "DBOS.send";
pub const SEND_BULK: &str = "DBOS.sendBulk";
pub const WRITE_STREAM: &str = "DBOS.writeStream";
pub const CLOSE_STREAM: &str = "DBOS.closeStream";
pub const SET_EVENT: &str = "DBOS.setEvent";
pub const GET_EVENT: &str = "DBOS.getEvent";
pub const GET_RESULT: &str = "DBOS.getResult";
pub const SELECT_WORKFLOW: &str = "DBOS.selectWorkflow";
pub const SELECT_STEP: &str = "DBOS.selectStep";
pub const RECV: &str = "DBOS.recv";
pub const DEBOUNCE: &str = "DBOS.debounceDelayedWorkflow";
pub const CANCEL_WORKFLOW: &str = "DBOS.cancelWorkflow";
pub const RESUME_WORKFLOW: &str = "DBOS.resumeWorkflow";
pub const DELETE_WORKFLOW: &str = "DBOS.deleteWorkflow";
pub const FORK_WORKFLOW: &str = "DBOS.forkWorkflow";
pub const SET_WORKFLOW_DELAY: &str = "DBOS.setWorkflowDelay";
pub const UPDATE_WORKFLOW_ATTRIBUTES: &str = "DBOS.updateWorkflowAttributes";
pub const LIST_WORKFLOWS: &str = "DBOS.listWorkflows";
pub const LIST_WORKFLOW_STEPS: &str = "DBOS.listWorkflowSteps";
pub const CREATE_SCHEDULE: &str = "DBOS.createSchedule";
pub const UPSERT_SCHEDULE: &str = "DBOS.upsertSchedule";
pub const GET_SCHEDULE: &str = "DBOS.getSchedule";
pub const LIST_SCHEDULES: &str = "DBOS.listSchedules";
pub const UPDATE_SCHEDULE: &str = "DBOS.updateSchedule";
pub const PAUSE_SCHEDULE: &str = "DBOS.pauseSchedule";
pub const RESUME_SCHEDULE: &str = "DBOS.resumeSchedule";
pub const DELETE_SCHEDULE: &str = "DBOS.deleteSchedule";
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Message<'a> {
pub destination_id: &'a str,
pub topic: Option<&'a str>,
pub message: &'a str,
pub idempotency_key: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WrittenBy {
Workflow,
Step,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum ScheduleStatus {
Active,
Paused,
}
impl ScheduleStatus {
pub fn as_str(self) -> &'static str {
match self {
ScheduleStatus::Active => "ACTIVE",
ScheduleStatus::Paused => "PAUSED",
}
}
pub fn parse(value: &str) -> Option<Self> {
Some(match value {
"ACTIVE" => ScheduleStatus::Active,
"PAUSED" => ScheduleStatus::Paused,
_ => return None,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ScheduleRecord {
pub schedule_id: String,
pub schedule_name: String,
pub workflow_name: String,
pub workflow_class_name: Option<String>,
pub schedule: String,
pub status: ScheduleStatus,
pub context: String,
pub last_fired_at: Option<Timestamp>,
pub automatic_backfill: bool,
pub cron_timezone: Option<String>,
pub queue_name: Option<String>,
pub application_name: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NewSchedule<'a> {
pub schedule_id: Option<&'a str>,
pub schedule_name: &'a str,
pub workflow_name: &'a str,
pub workflow_class_name: Option<&'a str>,
pub schedule: &'a str,
pub status: ScheduleStatus,
pub context: &'a str,
pub last_fired_at: Option<Timestamp>,
pub automatic_backfill: bool,
pub cron_timezone: Option<&'a str>,
pub queue_name: Option<&'a str>,
pub application_name: Option<&'a str>,
}
impl<'a> NewSchedule<'a> {
pub fn new(schedule_name: &'a str, workflow_name: &'a str, schedule: &'a str) -> Self {
Self {
schedule_id: None,
schedule_name,
workflow_name,
workflow_class_name: None,
schedule,
status: ScheduleStatus::Active,
context: "null",
last_fired_at: None,
automatic_backfill: false,
cron_timezone: None,
queue_name: None,
application_name: None,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ScheduleUpdate<'a> {
pub schedule: Change<&'a str>,
pub context: Change<&'a str>,
pub automatic_backfill: Change<bool>,
pub cron_timezone: Change<Option<&'a str>>,
pub queue_name: Change<Option<&'a str>>,
}
impl ScheduleUpdate<'_> {
pub fn is_empty(&self) -> bool {
self.schedule.is_leave()
&& self.context.is_leave()
&& self.automatic_backfill.is_leave()
&& self.cron_timezone.is_leave()
&& self.queue_name.is_leave()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ScheduleFilter<'a> {
pub statuses: Vec<ScheduleStatus>,
pub workflow_names: Vec<&'a str>,
pub schedule_name_prefixes: Vec<&'a str>,
pub applications: Applications<'a>,
}