use std::collections::HashMap;
use std::hash::Hash;
use std::pin::Pin;
use std::sync::{Arc, RwLock};
use std::task::{Context, Poll};
use bytes::Bytes;
use futures_core::Stream;
use tokio_util::sync::CancellationToken;
use crate::bus::{validate_subject_token, Headers, Pubsub};
use crate::error::BusError;
#[derive(Clone, Debug, Default)]
pub struct CancelRegistry<K>
where
K: Eq + Hash + Clone,
{
inner: Arc<RwLock<HashMap<K, CancellationToken>>>,
}
impl<K> CancelRegistry<K>
where
K: Eq + Hash + Clone + std::fmt::Debug,
{
pub fn new() -> Self {
Self {
inner: Arc::new(RwLock::new(HashMap::new())),
}
}
#[tracing::instrument(skip_all, fields(klieo.cancel.key = ?id), level = "debug")]
pub fn register(&self, id: K, token: CancellationToken) {
self.inner
.write()
.expect("CancelRegistry lock poisoned")
.insert(id, token);
}
#[tracing::instrument(skip_all, fields(klieo.cancel.key = ?id), level = "debug")]
pub fn deregister(&self, id: &K) {
self.inner
.write()
.expect("CancelRegistry lock poisoned")
.remove(id);
}
#[tracing::instrument(
skip_all,
fields(klieo.cancel.key = ?id, klieo.cancel.hit = tracing::field::Empty),
level = "debug",
)]
pub fn cancel(&self, id: &K) -> bool {
let guard = self.inner.read().expect("CancelRegistry lock poisoned");
let hit = match guard.get(id) {
Some(token) => {
token.cancel();
true
}
None => false,
};
tracing::Span::current().record("klieo.cancel.hit", hit);
hit
}
}
pub async fn publish_cancel_signal(
pubsub: &Arc<dyn Pubsub>,
subject_prefix: &str,
id: &str,
) -> Result<(), BusError> {
validate_subject_token(id)?;
let subject = format!("{subject_prefix}{id}");
pubsub
.publish(&subject, Bytes::new(), Headers::default())
.await
}
pub fn spawn_wildcard_cancel_subscriber(
pubsub: Arc<dyn Pubsub>,
subject_pattern: String,
subject_prefix: String,
registry: CancelRegistry<String>,
log_target: &'static str,
) {
tokio::spawn(async move {
let durable =
crate::ids::DurableName::new(format!("klieo-cancel-{}", uuid::Uuid::new_v4()));
let mut stream = match pubsub.subscribe(&subject_pattern, durable).await {
Ok(s) => s,
Err(e) => {
tracing::error!(
target: "klieo.cancel",
transport = %log_target,
pattern = %subject_pattern,
error = %e,
"wildcard cancel subscription failed; \
replica falls back to single-replica semantics",
);
return;
}
};
while let Some(item) = tokio_stream::StreamExt::next(&mut stream).await {
match item {
Ok(msg) => {
let id = msg
.subject
.strip_prefix(subject_prefix.as_str())
.unwrap_or_default()
.to_string();
let subject = msg.subject.clone();
let ack = msg.ack;
let span = tracing::info_span!(
"cancel_dispatch",
messaging.system = "klieo-bus",
messaging.destination = %subject,
messaging.operation = "receive",
transport = %log_target,
);
#[cfg(feature = "otel")]
{
use tracing_opentelemetry::OpenTelemetrySpanExt as _;
let parent_cx = crate::bus::extract_traceparent(&msg.headers);
span.set_parent(parent_cx);
}
use tracing::Instrument as _;
let registry = registry.clone();
async move {
if let Err(e) = ack.ack().await {
tracing::warn!(
target: "klieo.cancel",
transport = %log_target,
subject = %subject,
error = %e,
"cancel ack failed; expect redelivery",
);
}
if !id.is_empty() {
let hit = registry.cancel(&id);
tracing::debug!(
target: "klieo.cancel",
transport = %log_target,
id = %id,
hit = hit,
"cancel signal dispatched",
);
}
}
.instrument(span)
.await;
}
Err(e) => {
tracing::warn!(
target: "klieo.cancel",
transport = %log_target,
error = %e,
"cancel subscription stream error; continuing",
);
}
}
}
});
}
pub fn spawn_drop_publish(
pubsub: Arc<dyn Pubsub>,
subject: String,
log_target: &'static str,
permits: Option<Arc<tokio::sync::Semaphore>>,
trace_headers: Headers,
) {
if subject.is_empty() {
return;
}
let permit = match permits.as_ref() {
Some(sem) => match sem.clone().try_acquire_owned() {
Ok(p) => Some(p),
Err(_) => {
tracing::warn!(
target: "klieo.cancel",
transport = log_target,
subject = %subject,
available_permits = sem.available_permits(),
"cancel publish dropped: concurrency cap reached",
);
return;
}
},
None => None,
};
match tokio::runtime::Handle::try_current() {
Ok(handle) => {
handle.spawn(async move {
let _permit = permit;
if let Err(e) = pubsub.publish(&subject, Bytes::new(), trace_headers).await {
tracing::warn!(
target: "klieo.cancel",
transport = %log_target,
subject = %subject,
error = %e,
"cancel publish failed; cross-replica cancel degraded",
);
}
});
}
Err(_) => {
tracing::warn!(
target: "klieo.cancel",
transport = %log_target,
subject = %subject,
"cancel publish skipped: no tokio runtime active during Drop",
);
}
}
}
pub fn spawn_publish_bounded(
pubsub: Arc<dyn Pubsub>,
subject: String,
payload: Bytes,
log_target: &'static str,
permits: Arc<tokio::sync::Semaphore>,
trace_headers: Headers,
) {
let permit = match permits.clone().try_acquire_owned() {
Ok(p) => p,
Err(_) => {
tracing::warn!(
target: "klieo.cancel",
transport = log_target,
subject = %subject,
available_permits = permits.available_permits(),
"publish dropped: concurrency cap reached",
);
return;
}
};
match tokio::runtime::Handle::try_current() {
Ok(handle) => {
handle.spawn(async move {
let _permit = permit;
if let Err(e) = pubsub.publish(&subject, payload, trace_headers).await {
tracing::warn!(
target: "klieo.cancel",
transport = log_target,
subject = %subject,
error = %e,
"publish failed",
);
}
});
}
Err(_) => {
tracing::warn!(
target: "klieo.cancel",
transport = log_target,
subject = %subject,
"publish skipped: no tokio runtime active",
);
}
}
}
pub struct RegistryDeregisterOnDrop<S> {
inner: S,
registry: CancelRegistry<String>,
id: String,
}
impl<S> RegistryDeregisterOnDrop<S> {
pub fn new(inner: S, registry: CancelRegistry<String>, id: String) -> Self {
Self {
inner,
registry,
id,
}
}
}
impl<S: Stream + Unpin> Stream for RegistryDeregisterOnDrop<S> {
type Item = S::Item;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Pin::new(&mut self.inner).poll_next(cx)
}
}
impl<S> Drop for RegistryDeregisterOnDrop<S> {
fn drop(&mut self) {
if !self.id.is_empty() {
self.registry.deregister(&self.id);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bus::MsgStream;
use crate::ids::DurableName;
use async_trait::async_trait;
use std::time::Duration;
use tokio::sync::{mpsc, Semaphore};
struct RecordingPubsub {
tx: mpsc::UnboundedSender<(String, Bytes)>,
}
#[async_trait]
impl Pubsub for RecordingPubsub {
async fn publish(
&self,
subject: &str,
payload: Bytes,
_headers: Headers,
) -> Result<(), crate::error::BusError> {
let _ = self.tx.send((subject.to_string(), payload));
Ok(())
}
async fn subscribe(
&self,
_subject: &str,
_durable: DurableName,
) -> Result<MsgStream, crate::error::BusError> {
Err(crate::error::BusError::Unsupported(
"RecordingPubsub::subscribe is not used in tests".into(),
))
}
}
fn recording_pubsub() -> (Arc<dyn Pubsub>, mpsc::UnboundedReceiver<(String, Bytes)>) {
let (tx, rx) = mpsc::unbounded_channel();
(Arc::new(RecordingPubsub { tx }), rx)
}
#[test]
fn register_then_cancel_fires_token() {
let registry: CancelRegistry<String> = CancelRegistry::new();
let token = CancellationToken::new();
registry.register("task-1".to_string(), token.clone());
assert!(!token.is_cancelled());
let hit = registry.cancel(&"task-1".to_string());
assert!(hit, "cancel on registered id returns true");
assert!(token.is_cancelled(), "registered token fires on cancel");
}
#[test]
fn deregister_removes_entry() {
let registry: CancelRegistry<String> = CancelRegistry::new();
let token = CancellationToken::new();
registry.register("task-2".to_string(), token.clone());
registry.deregister(&"task-2".to_string());
let hit = registry.cancel(&"task-2".to_string());
assert!(!hit, "cancel after deregister returns false");
assert!(
!token.is_cancelled(),
"deregistered token is not fired by later cancel",
);
}
#[test]
fn cancel_on_missing_key_is_noop() {
let registry: CancelRegistry<String> = CancelRegistry::new();
let hit = registry.cancel(&"never".to_string());
assert!(!hit, "cancel on absent id returns false");
}
#[tokio::test(flavor = "current_thread")]
async fn spawn_publish_bounded_drops_on_saturation() {
let (pubsub, mut rx) = recording_pubsub();
let _keepalive = pubsub.clone();
let permits = Arc::new(Semaphore::new(0));
spawn_publish_bounded(
pubsub,
"subj.saturated".to_string(),
Bytes::from_static(b"x"),
"test.cancel",
permits,
Headers::default(),
);
let recv = tokio::time::timeout(Duration::from_millis(100), rx.recv()).await;
assert!(
recv.is_err(),
"no publish should reach recorder under zero-permit saturation"
);
}
#[tokio::test(flavor = "current_thread")]
async fn spawn_publish_bounded_publishes_with_available_permit() {
let (pubsub, mut rx) = recording_pubsub();
let permits = Arc::new(Semaphore::new(1));
spawn_publish_bounded(
pubsub,
"subj.ok".to_string(),
Bytes::from_static(b"hello"),
"test.cancel",
permits,
Headers::default(),
);
let recv = tokio::time::timeout(Duration::from_millis(500), rx.recv())
.await
.expect("publish should arrive within 500ms when a permit is available");
let (subject, payload) = recv.expect("recorder channel closed unexpectedly");
assert_eq!(subject, "subj.ok");
assert_eq!(payload.as_ref(), b"hello");
}
#[tokio::test(flavor = "current_thread")]
async fn spawn_drop_publish_with_permits_drops_on_saturation() {
let (pubsub, mut rx) = recording_pubsub();
let _keepalive = pubsub.clone();
let permits = Arc::new(Semaphore::new(0));
spawn_drop_publish(
pubsub,
"subj.drop.saturated".to_string(),
"test.cancel",
Some(permits),
Headers::default(),
);
let recv = tokio::time::timeout(Duration::from_millis(100), rx.recv()).await;
assert!(
recv.is_err(),
"spawn_drop_publish with zero permits must not publish",
);
}
}