use chrono::{DateTime, Utc};
use concepts::{
prefixed_ulid::ExecutionId,
storage::{CancelOutcome, DbConnection, DbErrorWrite},
};
use executor::AbortOnDropHandle;
use std::{
sync::{Arc, Mutex},
time::Duration,
};
use tokio::sync::{oneshot, watch};
use tracing::{Instrument, debug, info, info_span};
#[derive(Clone)]
pub struct CancelRegistry {
activity_cancellation_tokens: Arc<Mutex<hashbrown::HashMap<ExecutionId, ActivityInfo>>>,
running_workflows: Arc<Mutex<hashbrown::HashMap<ExecutionId, watch::Sender<bool>>>>,
}
struct ActivityInfo {
cancellation_sender: oneshot::Sender<()>,
}
impl Default for CancelRegistry {
fn default() -> Self {
Self::new()
}
}
impl CancelRegistry {
#[must_use]
pub fn new() -> CancelRegistry {
CancelRegistry {
activity_cancellation_tokens: Arc::default(),
running_workflows: Arc::default(),
}
}
pub fn spawn_cancel_watcher(&self, sleep_duration: Duration) -> AbortOnDropHandle {
let clone = self.clone();
AbortOnDropHandle::new(
tokio::spawn({
async move {
debug!("Spawned the cancel watcher");
loop {
clone.tick();
tokio::time::sleep(sleep_duration).await;
}
}
.instrument(info_span!(parent: None, "cancel_watcher"))
})
.abort_handle(),
)
}
fn tick(&self) {
self.activity_cancellation_tokens
.lock()
.unwrap()
.retain(|_exe, info| !info.cancellation_sender.is_closed());
self.running_workflows
.lock()
.unwrap()
.retain(|_exe, sender| !sender.is_closed());
}
pub(crate) fn activity_obtain_cancellation_token(
&self,
execution_id: ExecutionId,
) -> oneshot::Receiver<()> {
let mut guard = self.activity_cancellation_tokens.lock().unwrap();
let (cancellation_sender, receiver) = oneshot::channel();
guard.insert(
execution_id,
ActivityInfo {
cancellation_sender,
},
);
receiver
}
#[must_use]
pub fn register_running_workflow(&self, execution_id: ExecutionId) -> watch::Receiver<bool> {
let (sender, receiver) = watch::channel(false);
self.running_workflows
.lock()
.unwrap()
.insert(execution_id, sender);
receiver
}
pub fn signal_workflow_interrupt(&self, execution_id: &ExecutionId) {
if let Some(sender) = self.running_workflows.lock().unwrap().get(execution_id) {
let _ = sender.send(true);
}
}
pub async fn cancel_activity(
&self,
db_connection: &dyn DbConnection,
execution_id: &ExecutionId,
cancelled_at: DateTime<Utc>,
) -> Result<CancelOutcome, DbErrorWrite> {
info!(%execution_id, "Cancelling activity");
let outcome = db_connection
.cancel_activity_with_retries(execution_id, cancelled_at)
.await?;
if outcome == CancelOutcome::CancelRequested {
let info = {
let mut guard = self.activity_cancellation_tokens.lock().unwrap();
guard.remove(execution_id)
};
if let Some(info) = info {
let _ = info.cancellation_sender.send(());
}
}
Ok(outcome)
}
}