#![forbid(unsafe_code)]
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use tokio::sync::watch;
use uuid::Uuid;
#[derive(Clone, Default)]
pub struct OperationRegistry {
senders: Arc<Mutex<HashMap<Uuid, watch::Sender<bool>>>>,
}
pub struct ActiveOperation {
id: Uuid,
registry: OperationRegistry,
cancellation: watch::Receiver<bool>,
parent_cancellation: Option<watch::Receiver<bool>>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RegistryError {
RegistryUnavailable,
SameAsParent,
OperationInProgress,
ParentNotRunning,
}
impl OperationRegistry {
pub fn register_request(
&self,
request_id: Uuid,
parent_operation_id: Option<Uuid>,
) -> Result<ActiveOperation, RegistryError> {
if let Some(parent_id) = parent_operation_id {
if request_id == parent_id {
return Err(RegistryError::SameAsParent);
}
let parent_cancellation = self
.senders
.lock()
.map_err(|_| RegistryError::RegistryUnavailable)?
.get(&parent_id)
.map(watch::Sender::subscribe)
.ok_or(RegistryError::ParentNotRunning)?;
let mut operation = self.register(request_id)?;
operation.parent_cancellation = Some(parent_cancellation);
Ok(operation)
} else {
self.register(request_id)
}
}
pub fn cancel(&self, id: Uuid) -> Result<bool, RegistryError> {
let sender = self
.senders
.lock()
.map_err(|_| RegistryError::RegistryUnavailable)?
.get(&id)
.cloned();
Ok(sender.is_some_and(|sender| sender.send(true).is_ok()))
}
fn register(&self, id: Uuid) -> Result<ActiveOperation, RegistryError> {
let (sender, cancellation) = watch::channel(false);
let mut senders = self
.senders
.lock()
.map_err(|_| RegistryError::RegistryUnavailable)?;
if senders.contains_key(&id) {
return Err(RegistryError::OperationInProgress);
}
senders.insert(id, sender);
Ok(ActiveOperation {
id,
registry: self.clone(),
cancellation,
parent_cancellation: None,
})
}
fn remove(&self, id: Uuid) {
if let Ok(mut senders) = self.senders.lock() {
senders.remove(&id);
}
}
}
impl ActiveOperation {
pub async fn cancelled(&mut self) {
if let Some(parent) = &mut self.parent_cancellation {
tokio::select! {
_ = cancellation_requested(&mut self.cancellation) => {}
_ = cancellation_requested(parent) => {}
}
} else {
cancellation_requested(&mut self.cancellation).await;
}
}
pub fn direct_cancellation_requested(&self) -> bool {
*self.cancellation.borrow()
}
}
async fn cancellation_requested(cancellation: &mut watch::Receiver<bool>) {
if *cancellation.borrow() {
return;
}
while cancellation.changed().await.is_ok() {
if *cancellation.borrow() {
return;
}
}
}
impl Drop for ActiveOperation {
fn drop(&mut self) {
self.registry.remove(self.id);
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
#[tokio::test]
async fn direct_cancellation_stops_the_registered_operation() {
let registry = OperationRegistry::default();
let id = Uuid::new_v4();
let mut operation = registry.register_request(id, None).unwrap();
assert!(registry.cancel(id).unwrap());
tokio::time::timeout(Duration::from_millis(50), operation.cancelled())
.await
.unwrap();
}
#[tokio::test]
async fn child_inherits_registered_parent_cancellation() {
let registry = OperationRegistry::default();
let parent_id = Uuid::new_v4();
let child_id = Uuid::new_v4();
let _parent = registry.register_request(parent_id, None).unwrap();
let mut child = registry
.register_request(child_id, Some(parent_id))
.unwrap();
assert!(registry.cancel(parent_id).unwrap());
tokio::time::timeout(Duration::from_millis(50), child.cancelled())
.await
.unwrap();
}
#[test]
fn duplicate_ids_are_rejected() {
let registry = OperationRegistry::default();
let id = Uuid::new_v4();
let _operation = registry.register_request(id, None).unwrap();
assert!(matches!(
registry.register_request(id, None),
Err(RegistryError::OperationInProgress)
));
}
#[test]
fn request_cannot_be_its_own_parent() {
let registry = OperationRegistry::default();
let id = Uuid::new_v4();
assert!(matches!(
registry.register_request(id, Some(id)),
Err(RegistryError::SameAsParent)
));
}
#[test]
fn missing_parent_is_rejected() {
let registry = OperationRegistry::default();
assert!(matches!(
registry.register_request(Uuid::new_v4(), Some(Uuid::new_v4())),
Err(RegistryError::ParentNotRunning)
));
}
#[test]
fn dropping_an_operation_cleans_up_and_allows_reuse() {
let registry = OperationRegistry::default();
let id = Uuid::new_v4();
let operation = registry.register_request(id, None).unwrap();
drop(operation);
assert!(!registry.cancel(id).unwrap());
assert!(registry.register_request(id, None).is_ok());
}
#[test]
fn nonblocking_probe_reports_only_direct_cancellation() {
let registry = OperationRegistry::default();
let parent_id = Uuid::new_v4();
let child_id = Uuid::new_v4();
let _parent = registry.register_request(parent_id, None).unwrap();
let child = registry
.register_request(child_id, Some(parent_id))
.unwrap();
assert!(registry.cancel(parent_id).unwrap());
assert!(!child.direct_cancellation_requested());
assert!(registry.cancel(child_id).unwrap());
assert!(child.direct_cancellation_requested());
}
}