use std::time::Duration;
use dapr_durabletask::api::{DurableTaskError, OrchestrationState, PurgeInstanceFilter, Result};
use dapr_durabletask::client::TaskHubGrpcClient;
use dapr_durabletask::worker::{Registry, TaskHubGrpcWorker};
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use tonic::transport::Channel;
use super::options::{EventOptions, FetchOptions, ScheduleOptions};
pub struct WorkflowClient {
inner: TaskHubGrpcClient,
channel: Channel,
worker: Option<TaskHubGrpcWorker>,
}
impl WorkflowClient {
pub async fn new() -> Result<Self> {
Self::new_with_address(default_sidecar_address()).await
}
pub async fn new_with_address(address: impl Into<String>) -> Result<Self> {
let address = ensure_url_scheme(address.into());
let channel = Channel::from_shared(address.clone())
.map_err(|e| DurableTaskError::InvalidAddress(e.to_string()))?
.connect()
.await
.map_err(|e| DurableTaskError::ConnectionFailed(e.to_string()))?;
let inner = TaskHubGrpcClient::from_channel(channel.clone());
let worker = Some(TaskHubGrpcWorker::new(&address));
Ok(Self {
inner,
channel,
worker,
})
}
pub fn registry_mut(&mut self) -> &mut Registry {
self.worker
.as_mut()
.expect("workflow worker registry is unavailable after start_worker has been called")
.registry_mut()
}
pub async fn start_worker(&mut self) -> Result<WorkerHandle> {
let worker = self.worker.take().ok_or_else(|| {
DurableTaskError::Other("workflow worker has already been started".to_string())
})?;
let token = CancellationToken::new();
let worker_token = token.clone();
let join = tokio::spawn(async move { worker.start(worker_token).await });
Ok(WorkerHandle {
token,
join: Some(join),
})
}
pub fn scheduling_client(&self) -> WorkflowSchedulingClient {
WorkflowSchedulingClient {
inner: TaskHubGrpcClient::from_channel(self.channel.clone()),
channel: self.channel.clone(),
}
}
pub async fn schedule_workflow(
&mut self,
name: &str,
options: ScheduleOptions,
) -> Result<String> {
schedule_workflow_impl(&mut self.inner, name, options).await
}
pub async fn suspend_workflow(
&mut self,
instance_id: &str,
reason: impl Into<String>,
) -> Result<()> {
suspend_workflow_impl(&mut self.inner, instance_id, reason).await
}
pub async fn resume_workflow(
&mut self,
instance_id: &str,
reason: impl Into<String>,
) -> Result<()> {
resume_workflow_impl(&mut self.inner, instance_id, reason).await
}
pub async fn raise_event(
&mut self,
instance_id: &str,
event_name: &str,
options: EventOptions,
) -> Result<()> {
raise_event_impl(&mut self.inner, instance_id, event_name, options).await
}
pub async fn fetch_workflow_metadata(
&mut self,
instance_id: &str,
options: FetchOptions,
) -> Result<OrchestrationState> {
fetch_workflow_metadata_impl(&mut self.inner, instance_id, options).await
}
pub async fn wait_for_workflow_start(
&mut self,
instance_id: &str,
) -> Result<OrchestrationState> {
wait_for_workflow_start_with_options_impl(
&mut self.inner,
instance_id,
FetchOptions::new(),
None,
)
.await
}
pub async fn wait_for_workflow_start_with_options(
&mut self,
instance_id: &str,
options: FetchOptions,
timeout: Option<Duration>,
) -> Result<OrchestrationState> {
wait_for_workflow_start_with_options_impl(&mut self.inner, instance_id, options, timeout)
.await
}
pub async fn wait_for_workflow_completion(
&mut self,
instance_id: &str,
) -> Result<OrchestrationState> {
wait_for_workflow_completion_with_options_impl(
&mut self.inner,
instance_id,
FetchOptions::new(),
None,
)
.await
}
pub async fn wait_for_workflow_completion_with_options(
&mut self,
instance_id: &str,
options: FetchOptions,
timeout: Option<Duration>,
) -> Result<OrchestrationState> {
wait_for_workflow_completion_with_options_impl(
&mut self.inner,
instance_id,
options,
timeout,
)
.await
}
pub async fn purge_workflow_state(&mut self, instance_id: &str) -> Result<()> {
purge_workflow_state_recursive_impl(&mut self.inner, instance_id, false).await
}
pub async fn purge_workflow_state_recursive(
&mut self,
instance_id: &str,
recursive: bool,
) -> Result<()> {
purge_workflow_state_recursive_impl(&mut self.inner, instance_id, recursive).await
}
pub async fn purge_workflow_state_by_filter(
&mut self,
filter: PurgeInstanceFilter,
recursive: bool,
) -> Result<i32> {
purge_workflow_state_by_filter_impl(&mut self.inner, filter, recursive).await
}
pub async fn terminate_workflow(&mut self, instance_id: &str) -> Result<()> {
terminate_workflow_recursive_impl(&mut self.inner, instance_id, false).await
}
pub async fn terminate_workflow_recursive(
&mut self,
instance_id: &str,
recursive: bool,
) -> Result<()> {
terminate_workflow_recursive_impl(&mut self.inner, instance_id, recursive).await
}
}
pub struct WorkflowSchedulingClient {
inner: TaskHubGrpcClient,
channel: Channel,
}
impl Clone for WorkflowSchedulingClient {
fn clone(&self) -> Self {
Self {
inner: TaskHubGrpcClient::from_channel(self.channel.clone()),
channel: self.channel.clone(),
}
}
}
impl WorkflowSchedulingClient {
pub async fn schedule_workflow(
&mut self,
name: &str,
options: ScheduleOptions,
) -> Result<String> {
schedule_workflow_impl(&mut self.inner, name, options).await
}
pub async fn suspend_workflow(
&mut self,
instance_id: &str,
reason: impl Into<String>,
) -> Result<()> {
suspend_workflow_impl(&mut self.inner, instance_id, reason).await
}
pub async fn resume_workflow(
&mut self,
instance_id: &str,
reason: impl Into<String>,
) -> Result<()> {
resume_workflow_impl(&mut self.inner, instance_id, reason).await
}
pub async fn raise_event(
&mut self,
instance_id: &str,
event_name: &str,
options: EventOptions,
) -> Result<()> {
raise_event_impl(&mut self.inner, instance_id, event_name, options).await
}
pub async fn fetch_workflow_metadata(
&mut self,
instance_id: &str,
options: FetchOptions,
) -> Result<OrchestrationState> {
fetch_workflow_metadata_impl(&mut self.inner, instance_id, options).await
}
pub async fn wait_for_workflow_start(
&mut self,
instance_id: &str,
) -> Result<OrchestrationState> {
wait_for_workflow_start_with_options_impl(
&mut self.inner,
instance_id,
FetchOptions::new(),
None,
)
.await
}
pub async fn wait_for_workflow_start_with_options(
&mut self,
instance_id: &str,
options: FetchOptions,
timeout: Option<Duration>,
) -> Result<OrchestrationState> {
wait_for_workflow_start_with_options_impl(&mut self.inner, instance_id, options, timeout)
.await
}
pub async fn wait_for_workflow_completion(
&mut self,
instance_id: &str,
) -> Result<OrchestrationState> {
wait_for_workflow_completion_with_options_impl(
&mut self.inner,
instance_id,
FetchOptions::new(),
None,
)
.await
}
pub async fn wait_for_workflow_completion_with_options(
&mut self,
instance_id: &str,
options: FetchOptions,
timeout: Option<Duration>,
) -> Result<OrchestrationState> {
wait_for_workflow_completion_with_options_impl(
&mut self.inner,
instance_id,
options,
timeout,
)
.await
}
pub async fn purge_workflow_state(&mut self, instance_id: &str) -> Result<()> {
purge_workflow_state_recursive_impl(&mut self.inner, instance_id, false).await
}
pub async fn purge_workflow_state_recursive(
&mut self,
instance_id: &str,
recursive: bool,
) -> Result<()> {
purge_workflow_state_recursive_impl(&mut self.inner, instance_id, recursive).await
}
pub async fn purge_workflow_state_by_filter(
&mut self,
filter: PurgeInstanceFilter,
recursive: bool,
) -> Result<i32> {
purge_workflow_state_by_filter_impl(&mut self.inner, filter, recursive).await
}
pub async fn terminate_workflow(&mut self, instance_id: &str) -> Result<()> {
terminate_workflow_recursive_impl(&mut self.inner, instance_id, false).await
}
pub async fn terminate_workflow_recursive(
&mut self,
instance_id: &str,
recursive: bool,
) -> Result<()> {
terminate_workflow_recursive_impl(&mut self.inner, instance_id, recursive).await
}
}
pub struct WorkerHandle {
token: CancellationToken,
join: Option<JoinHandle<dapr_durabletask::api::Result<()>>>,
}
impl WorkerHandle {
pub async fn shutdown(mut self) -> Result<()> {
self.token.cancel();
let join = self.join.take().ok_or_else(|| {
DurableTaskError::Other("workflow worker task was already joined".to_string())
})?;
join.await
.map_err(|e| DurableTaskError::Other(format!("workflow worker task failed: {e}")))?
}
}
impl Drop for WorkerHandle {
fn drop(&mut self) {
self.token.cancel();
}
}
fn default_sidecar_address() -> String {
ensure_url_scheme(crate::client::config::default_sidecar_address())
}
fn ensure_url_scheme(address: String) -> String {
if address.contains("://") {
address
} else {
format!("http://{address}")
}
}
async fn schedule_workflow_impl(
client: &mut TaskHubGrpcClient,
name: &str,
options: ScheduleOptions,
) -> Result<String> {
let instance_id = options.instance_id.clone();
let start_time = options.start_time_utc();
let input = options.input_json()?;
client
.schedule_new_orchestration(name, input, instance_id, start_time)
.await
}
async fn suspend_workflow_impl(
client: &mut TaskHubGrpcClient,
instance_id: &str,
reason: impl Into<String>,
) -> Result<()> {
let reason = reason.into();
client
.suspend_orchestration(instance_id, Some(reason))
.await
}
async fn resume_workflow_impl(
client: &mut TaskHubGrpcClient,
instance_id: &str,
reason: impl Into<String>,
) -> Result<()> {
let reason = reason.into();
client.resume_orchestration(instance_id, Some(reason)).await
}
async fn raise_event_impl(
client: &mut TaskHubGrpcClient,
instance_id: &str,
event_name: &str,
options: EventOptions,
) -> Result<()> {
client
.raise_orchestration_event(instance_id, event_name, options.payload_json()?)
.await
}
async fn fetch_workflow_metadata_impl(
client: &mut TaskHubGrpcClient,
instance_id: &str,
options: FetchOptions,
) -> Result<OrchestrationState> {
client
.get_orchestration_state(instance_id, options.fetch_payloads)
.await?
.ok_or_else(|| DurableTaskError::InstanceNotFound {
instance_id: instance_id.to_string(),
})
}
async fn wait_for_workflow_start_with_options_impl(
client: &mut TaskHubGrpcClient,
instance_id: &str,
options: FetchOptions,
timeout: Option<Duration>,
) -> Result<OrchestrationState> {
client
.wait_for_orchestration_start(instance_id, options.fetch_payloads, timeout)
.await?
.ok_or_else(|| DurableTaskError::InstanceNotFound {
instance_id: instance_id.to_string(),
})
}
async fn wait_for_workflow_completion_with_options_impl(
client: &mut TaskHubGrpcClient,
instance_id: &str,
options: FetchOptions,
timeout: Option<Duration>,
) -> Result<OrchestrationState> {
client
.wait_for_orchestration_completion(instance_id, options.fetch_payloads, timeout)
.await?
.ok_or_else(|| DurableTaskError::InstanceNotFound {
instance_id: instance_id.to_string(),
})
}
async fn purge_workflow_state_recursive_impl(
client: &mut TaskHubGrpcClient,
instance_id: &str,
recursive: bool,
) -> Result<()> {
client.purge_orchestration(instance_id, recursive).await?;
Ok(())
}
async fn purge_workflow_state_by_filter_impl(
client: &mut TaskHubGrpcClient,
filter: PurgeInstanceFilter,
recursive: bool,
) -> Result<i32> {
client
.purge_orchestrations_by_filter(filter, recursive)
.await
}
async fn terminate_workflow_recursive_impl(
client: &mut TaskHubGrpcClient,
instance_id: &str,
recursive: bool,
) -> Result<()> {
client
.terminate_orchestration(instance_id, None, recursive)
.await
}
#[cfg(test)]
mod tests {
use super::ensure_url_scheme;
#[test]
fn ensure_url_scheme_preserves_http() {
assert_eq!(
ensure_url_scheme("http://127.0.0.1:50001".to_string()),
"http://127.0.0.1:50001"
);
}
#[test]
fn ensure_url_scheme_preserves_https() {
assert_eq!(
ensure_url_scheme("https://sidecar:443".to_string()),
"https://sidecar:443"
);
}
#[test]
fn ensure_url_scheme_prepends_when_missing() {
assert_eq!(
ensure_url_scheme("sidecar:50001".to_string()),
"http://sidecar:50001"
);
}
#[test]
fn ensure_url_scheme_prepends_for_bare_host() {
assert_eq!(
ensure_url_scheme("127.0.0.1".to_string()),
"http://127.0.0.1"
);
}
}