use core::fmt;
use parking_lot::RwLock;
use smallvec::SmallVec;
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use crate::types::symbol::{ObjectId, Symbol};
use crate::types::{Budget, CancelAttributionConfig, CancelKind, CancelReason, Time};
use crate::util::DetRng;
fn cancel_kind_to_u8(kind: CancelKind) -> u8 {
match kind {
CancelKind::User => 0,
CancelKind::Timeout => 1,
CancelKind::Deadline => 2,
CancelKind::PollQuota => 3,
CancelKind::CostBudget => 4,
CancelKind::FailFast => 5,
CancelKind::RaceLost => 6,
CancelKind::ParentCancelled => 7,
CancelKind::ResourceUnavailable => 8,
CancelKind::Shutdown => 9,
CancelKind::LinkedExit => 10,
}
}
fn cancel_kind_from_u8(b: u8) -> Option<CancelKind> {
match b {
0 => Some(CancelKind::User),
1 => Some(CancelKind::Timeout),
2 => Some(CancelKind::Deadline),
3 => Some(CancelKind::PollQuota),
4 => Some(CancelKind::CostBudget),
5 => Some(CancelKind::FailFast),
6 => Some(CancelKind::RaceLost),
7 => Some(CancelKind::ParentCancelled),
8 => Some(CancelKind::ResourceUnavailable),
9 => Some(CancelKind::Shutdown),
10 => Some(CancelKind::LinkedExit),
_ => None,
}
}
pub trait CancelListener: Send + Sync {
fn on_cancel(&self, reason: &CancelReason, at: Time);
}
impl<F> CancelListener for F
where
F: Fn(&CancelReason, Time) + Send + Sync,
{
fn on_cancel(&self, reason: &CancelReason, at: Time) {
self(reason, at);
}
}
struct CancelTokenState {
token_id: u64,
object_id: ObjectId,
cancelled: AtomicBool,
cancelled_at: AtomicU64,
reason: RwLock<Option<CancelReason>>,
cleanup_budget: Budget,
children: RwLock<SmallVec<[SymbolCancelToken; 2]>>,
listeners: RwLock<SmallVec<[ListenerEntry; 2]>>,
listener_panic_count: AtomicU64,
}
struct ListenerEntry {
listener: Box<dyn CancelListener>,
notified_severity: u8,
}
#[derive(Clone)]
pub struct SymbolCancelToken {
state: Arc<CancelTokenState>,
}
impl SymbolCancelToken {
#[must_use]
pub fn new(object_id: ObjectId, rng: &mut DetRng) -> Self {
Self {
state: Arc::new(CancelTokenState {
token_id: rng.next_u64(),
object_id,
cancelled: AtomicBool::new(false),
cancelled_at: AtomicU64::new(u64::MAX),
reason: RwLock::new(None),
cleanup_budget: Budget::default(),
children: RwLock::new(SmallVec::new()),
listeners: RwLock::new(SmallVec::new()),
listener_panic_count: AtomicU64::new(0),
}),
}
}
#[must_use]
pub fn with_budget(object_id: ObjectId, budget: Budget, rng: &mut DetRng) -> Self {
Self {
state: Arc::new(CancelTokenState {
token_id: rng.next_u64(),
object_id,
cancelled: AtomicBool::new(false),
cancelled_at: AtomicU64::new(u64::MAX),
reason: RwLock::new(None),
cleanup_budget: budget,
children: RwLock::new(SmallVec::new()),
listeners: RwLock::new(SmallVec::new()),
listener_panic_count: AtomicU64::new(0),
}),
}
}
#[must_use]
pub fn listener_panic_count(&self) -> u64 {
self.state.listener_panic_count.load(Ordering::Relaxed)
}
fn record_listener_panic(
state: &CancelTokenState,
panic_payload: Box<dyn std::any::Any + Send>,
) {
state.listener_panic_count.fetch_add(1, Ordering::Relaxed);
#[cfg(feature = "tracing-integration")]
{
let _trace_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let panic_msg = if let Some(s) = panic_payload.downcast_ref::<&str>() {
(*s).to_string()
} else if let Some(s) = panic_payload.downcast_ref::<String>() {
s.clone()
} else {
"<non-string panic payload>".to_string()
};
tracing::warn!(
object_id = ?state.object_id,
token_id = state.token_id,
panic = %panic_msg,
"cancel listener panicked during on_cancel — caught and logged \
instead of silently swallowed (br-asupersync-mzamuo)"
);
}));
}
#[cfg(not(feature = "tracing-integration"))]
{
let _ = panic_payload;
}
}
fn notify_listener_with_panic_logging(
state: &CancelTokenState,
listener: &dyn CancelListener,
reason: &CancelReason,
now: Time,
) {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
listener.on_cancel(reason, now);
}));
if let Err(panic_payload) = result {
Self::record_listener_panic(state, panic_payload);
}
}
fn notify_owned_listener_with_panic_logging(
state: &CancelTokenState,
listener: Box<dyn CancelListener>,
reason: &CancelReason,
now: Time,
) {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
listener.on_cancel(reason, now);
drop(listener);
}));
if let Err(panic_payload) = result {
Self::record_listener_panic(state, panic_payload);
}
}
fn notify_retained_listeners_until_current(
state: &CancelTokenState,
target_reason: &CancelReason,
target_severity: u8,
force_target_notification: bool,
) {
let notify_at_nanos = state.cancelled_at.load(Ordering::Acquire);
let notify_at = if notify_at_nanos == u64::MAX {
Time::ZERO
} else {
Time::from_nanos(notify_at_nanos)
};
let mut retained = {
let mut listeners = state.listeners.write();
std::mem::take(&mut *listeners)
};
for entry in &mut retained {
if force_target_notification || entry.notified_severity < target_severity {
Self::notify_listener_with_panic_logging(
state,
entry.listener.as_ref(),
target_reason,
notify_at,
);
entry.notified_severity = target_severity;
}
}
const MAX_CATCH_UP_ITERATIONS: u32 = 8;
for iteration in 0..MAX_CATCH_UP_ITERATIONS {
let reason_guard = state.reason.write();
let Some(current_reason) = reason_guard.clone() else {
let mut listeners = state.listeners.write();
listeners.extend(retained);
return;
};
let current_severity = current_reason.kind.severity();
if retained
.iter()
.all(|entry| entry.notified_severity >= current_severity)
{
let mut listeners = state.listeners.write();
listeners.extend(retained);
return;
}
drop(reason_guard);
for entry in &mut retained {
if entry.notified_severity < current_severity {
Self::notify_listener_with_panic_logging(
state,
entry.listener.as_ref(),
¤t_reason,
notify_at,
);
entry.notified_severity = current_severity;
}
}
if iteration < MAX_CATCH_UP_ITERATIONS - 1 {
std::hint::spin_loop();
}
}
let final_reason = {
let reason_guard = state.reason.write();
reason_guard
.clone()
.unwrap_or_else(CancelReason::parent_cancelled)
};
let final_severity = final_reason.kind.severity();
for entry in &mut retained {
if entry.notified_severity < final_severity {
Self::notify_listener_with_panic_logging(
state,
entry.listener.as_ref(),
&final_reason,
notify_at,
);
entry.notified_severity = final_severity;
}
}
let mut listeners = state.listeners.write();
listeners.extend(retained);
}
#[inline]
#[must_use]
pub fn token_id(&self) -> u64 {
self.state.token_id
}
#[inline]
#[must_use]
pub fn object_id(&self) -> ObjectId {
self.state.object_id
}
#[inline]
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.state.cancelled.load(Ordering::Acquire)
}
#[must_use]
pub fn reason(&self) -> Option<CancelReason> {
self.state.reason.read().clone()
}
#[inline]
#[must_use]
pub fn cancelled_at(&self) -> Option<Time> {
let nanos = self.state.cancelled_at.load(Ordering::Acquire);
if nanos == u64::MAX {
if self.is_cancelled() {
let _guard = self.state.reason.read();
let nanos_sync = self.state.cancelled_at.load(Ordering::Acquire);
if nanos_sync == u64::MAX {
None } else {
Some(Time::from_nanos(nanos_sync))
}
} else {
None
}
} else {
Some(Time::from_nanos(nanos))
}
}
#[must_use]
pub fn cleanup_budget(&self) -> Budget {
self.state.cleanup_budget
}
fn parent_cancelled_with_cause(parent_reason: &CancelReason, at: Time) -> CancelReason {
CancelReason::parent_cancelled()
.with_timestamp(at)
.with_cause_limited(parent_reason.clone(), &CancelAttributionConfig::default())
}
fn parent_cascade_reason_at(&self, at: Time) -> CancelReason {
self.state.reason.read().as_ref().map_or_else(
|| CancelReason::parent_cancelled().with_timestamp(at),
|reason| Self::parent_cancelled_with_cause(reason, at),
)
}
#[allow(clippy::must_use_candidate)]
pub fn cancel(&self, reason: &CancelReason, now: Time) -> bool {
let mut reason_guard = self.state.reason.write();
if !self.state.cancelled.load(Ordering::Acquire) {
let stored_nanos = now.as_nanos().min(u64::MAX - 1);
self.state
.cancelled_at
.store(stored_nanos, Ordering::Release);
*reason_guard = Some(reason.clone());
self.state.cancelled.store(true, Ordering::Release);
drop(reason_guard);
let new_severity = reason.kind.severity();
Self::notify_retained_listeners_until_current(&self.state, reason, new_severity, true);
let children = {
let mut children = self.state.children.write();
std::mem::take(&mut *children)
};
let parent_reason = self.parent_cascade_reason_at(now);
for child in children {
child.cancel(&parent_reason, now);
}
true
} else {
let prior_severity;
let strengthened_reason;
if let Some(ref mut stored) = *reason_guard {
prior_severity = stored.kind.severity();
stored.strengthen(reason);
strengthened_reason = stored.clone();
} else {
prior_severity = 0;
*reason_guard = Some(reason.clone());
strengthened_reason = reason.clone();
let stored_nanos = now.as_nanos().min(u64::MAX - 1);
self.state
.cancelled_at
.compare_exchange(u64::MAX, stored_nanos, Ordering::Release, Ordering::Relaxed)
.ok();
}
let new_severity = strengthened_reason.kind.severity();
drop(reason_guard);
if new_severity > prior_severity {
Self::notify_retained_listeners_until_current(
&self.state,
&strengthened_reason,
new_severity,
false,
);
}
false
}
}
fn inherited_cancelled_at(&self) -> Time {
let nanos = self.state.cancelled_at.load(Ordering::Acquire);
if nanos == u64::MAX {
Time::ZERO
} else {
Time::from_nanos(nanos)
}
}
#[must_use]
pub fn child(&self, rng: &mut DetRng) -> Self {
let child = Self::new(self.state.object_id, rng);
let mut children = self.state.children.write();
if !self.state.cancelled.load(Ordering::Acquire) {
children.push(child.clone());
return child;
}
drop(children);
let at = self.inherited_cancelled_at();
let parent_reason = self.parent_cascade_reason_at(at);
child.cancel(&parent_reason, at);
child
}
pub fn add_listener(&self, listener: impl CancelListener + 'static) {
let reason_guard = self.state.reason.write();
let mut listeners = self.state.listeners.write();
if self.state.cancelled.load(Ordering::Acquire) {
let reason = reason_guard
.clone()
.unwrap_or_else(CancelReason::parent_cancelled);
let at_nanos = self.state.cancelled_at.load(Ordering::Acquire);
debug_assert!(
at_nanos != u64::MAX || reason_guard.is_none(),
"add_listener must not observe reason=Some(_) with unpublished cancelled_at"
);
let at = if at_nanos == u64::MAX {
Time::ZERO
} else {
Time::from_nanos(at_nanos)
};
drop(listeners);
drop(reason_guard);
let boxed: Box<dyn CancelListener> = Box::new(listener);
Self::notify_owned_listener_with_panic_logging(&self.state, boxed, &reason, at);
} else {
listeners.push(ListenerEntry {
listener: Box::new(listener),
notified_severity: 0,
});
drop(listeners);
drop(reason_guard);
}
}
#[must_use]
pub fn to_bytes(&self) -> [u8; TOKEN_WIRE_SIZE] {
let mut buf = [0u8; TOKEN_WIRE_SIZE];
buf[0..8].copy_from_slice(&self.state.token_id.to_be_bytes());
buf[8..16].copy_from_slice(&self.state.object_id.high().to_be_bytes());
buf[16..24].copy_from_slice(&self.state.object_id.low().to_be_bytes());
buf[24] = u8::from(self.is_cancelled());
buf
}
#[must_use]
pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < TOKEN_WIRE_SIZE {
return None;
}
let token_id = u64::from_be_bytes(data[0..8].try_into().ok()?);
let high = u64::from_be_bytes(data[8..16].try_into().ok()?);
let low = u64::from_be_bytes(data[16..24].try_into().ok()?);
let cancelled = data[24] != 0;
Some(Self {
state: Arc::new(CancelTokenState {
token_id,
object_id: ObjectId::new(high, low),
cancelled: AtomicBool::new(cancelled),
cancelled_at: AtomicU64::new(u64::MAX),
reason: RwLock::new(None),
cleanup_budget: Budget::default(),
children: RwLock::new(SmallVec::new()),
listeners: RwLock::new(SmallVec::new()),
listener_panic_count: AtomicU64::new(0),
}),
})
}
#[doc(hidden)]
#[must_use]
#[cfg(test)]
pub fn new_for_test(token_id: u64, object_id: ObjectId) -> Self {
Self {
state: Arc::new(CancelTokenState {
token_id,
object_id,
cancelled: AtomicBool::new(false),
cancelled_at: AtomicU64::new(u64::MAX),
reason: RwLock::new(None),
cleanup_budget: Budget::default(),
children: RwLock::new(SmallVec::new()),
listeners: RwLock::new(SmallVec::new()),
listener_panic_count: AtomicU64::new(0),
}),
}
}
}
impl fmt::Debug for SymbolCancelToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SymbolCancelToken")
.field("token_id", &format!("{:016x}", self.state.token_id))
.field("object_id", &self.state.object_id)
.field("cancelled", &self.is_cancelled())
.finish()
}
}
const TOKEN_WIRE_SIZE: usize = 25;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CancelMessage {
token_id: u64,
object_id: ObjectId,
kind: CancelKind,
initiated_at: Time,
sequence: u64,
hops: u8,
max_hops: u8,
}
const MESSAGE_WIRE_SIZE: usize = 43;
impl CancelMessage {
#[must_use]
pub fn new(
token_id: u64,
object_id: ObjectId,
kind: CancelKind,
initiated_at: Time,
sequence: u64,
) -> Self {
Self {
token_id,
object_id,
kind,
initiated_at,
sequence,
hops: 0,
max_hops: 10,
}
}
#[inline]
#[must_use]
pub const fn token_id(&self) -> u64 {
self.token_id
}
#[inline]
#[must_use]
pub const fn object_id(&self) -> ObjectId {
self.object_id
}
#[inline]
#[must_use]
pub const fn kind(&self) -> CancelKind {
self.kind
}
#[inline]
#[must_use]
pub const fn initiated_at(&self) -> Time {
self.initiated_at
}
#[inline]
#[must_use]
pub const fn sequence(&self) -> u64 {
self.sequence
}
#[inline]
#[must_use]
pub const fn hops(&self) -> u8 {
self.hops
}
#[inline]
#[must_use]
pub const fn can_forward(&self) -> bool {
self.hops < self.max_hops
}
#[must_use]
pub fn forwarded(&self) -> Option<Self> {
if !self.can_forward() {
return None;
}
Some(Self {
hops: self.hops + 1,
..self.clone()
})
}
#[inline]
#[must_use]
pub const fn with_max_hops(mut self, max: u8) -> Self {
self.max_hops = max;
self
}
#[must_use]
pub fn to_bytes(&self) -> [u8; MESSAGE_WIRE_SIZE] {
let mut buf = [0u8; MESSAGE_WIRE_SIZE];
buf[0..8].copy_from_slice(&self.token_id.to_be_bytes());
buf[8..16].copy_from_slice(&self.object_id.high().to_be_bytes());
buf[16..24].copy_from_slice(&self.object_id.low().to_be_bytes());
buf[24] = cancel_kind_to_u8(self.kind);
buf[25..33].copy_from_slice(&self.initiated_at.as_nanos().to_be_bytes());
buf[33..41].copy_from_slice(&self.sequence.to_be_bytes());
buf[41] = self.hops;
buf[42] = self.max_hops;
buf
}
#[must_use]
pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < MESSAGE_WIRE_SIZE {
return None;
}
let token_id = u64::from_be_bytes(data[0..8].try_into().ok()?);
let high = u64::from_be_bytes(data[8..16].try_into().ok()?);
let low = u64::from_be_bytes(data[16..24].try_into().ok()?);
let kind = cancel_kind_from_u8(data[24])?;
let initiated_at = Time::from_nanos(u64::from_be_bytes(data[25..33].try_into().ok()?));
let sequence = u64::from_be_bytes(data[33..41].try_into().ok()?);
let hops = data[41];
let max_hops = data[42];
Some(Self {
token_id,
object_id: ObjectId::new(high, low),
kind,
initiated_at,
sequence,
hops,
max_hops,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct PeerId(String);
impl PeerId {
#[inline]
#[must_use]
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
#[inline]
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
pub trait CancelSink: Send + Sync {
fn send_to(
&self,
peer: &PeerId,
msg: &CancelMessage,
) -> impl std::future::Future<Output = crate::error::Result<()>> + Send;
fn broadcast(
&self,
msg: &CancelMessage,
) -> impl std::future::Future<Output = crate::error::Result<usize>> + Send;
}
#[derive(Clone, Debug, Default)]
pub struct CancelBroadcastMetrics {
pub initiated: u64,
pub received: u64,
pub forwarded: u64,
pub duplicates: u64,
pub max_hops_reached: u64,
pub pending_retries: u64,
}
pub struct CancelBroadcaster<S: CancelSink> {
peers: RwLock<SmallVec<[PeerId; 4]>>,
active_tokens: RwLock<HashMap<ObjectId, SymbolCancelToken>>,
seen_sequences: RwLock<SeenSequences>,
max_seen: usize,
sink: S,
next_sequence: AtomicU64,
pending_retries: RwLock<VecDeque<CancelMessage>>,
retry_in_progress: AtomicBool,
sender_tag: u64,
initiated: AtomicU64,
received: AtomicU64,
forwarded: AtomicU64,
duplicates: AtomicU64,
max_hops_reached: AtomicU64,
}
type SeenKey = (ObjectId, u64, u64);
#[derive(Debug, Default)]
struct SeenSequences {
set: HashSet<SeenKey>,
order: VecDeque<SeenKey>,
}
impl SeenSequences {
fn insert(&mut self, key: SeenKey) -> bool {
if self.set.insert(key) {
self.order.push_back(key);
true
} else {
false
}
}
fn remove_oldest(&mut self) -> Option<SeenKey> {
let oldest = self.order.pop_front()?;
self.set.remove(&oldest);
Some(oldest)
}
}
impl<S: CancelSink> CancelBroadcaster<S> {
pub fn new(sink: S) -> Self {
let mut tag_buf = [0u8; 8];
getrandom::fill(&mut tag_buf).expect("OS entropy source unavailable");
let sender_tag = u64::from_ne_bytes(tag_buf);
Self {
peers: RwLock::new(SmallVec::new()),
active_tokens: RwLock::new(HashMap::new()),
seen_sequences: RwLock::new(SeenSequences::default()),
max_seen: 10_000,
sink,
next_sequence: AtomicU64::new(0),
sender_tag,
pending_retries: RwLock::new(VecDeque::new()),
retry_in_progress: AtomicBool::new(false),
initiated: AtomicU64::new(0),
received: AtomicU64::new(0),
forwarded: AtomicU64::new(0),
duplicates: AtomicU64::new(0),
max_hops_reached: AtomicU64::new(0),
}
}
pub fn add_peer(&self, peer: PeerId) {
let mut peers = self.peers.write();
if !peers.contains(&peer) {
peers.push(peer);
}
}
pub fn remove_peer(&self, peer: &PeerId) {
self.peers.write().retain(|p| p != peer);
}
pub fn register_token(&self, token: SymbolCancelToken) {
self.active_tokens.write().insert(token.object_id(), token);
}
pub fn unregister_token(&self, object_id: &ObjectId) {
self.active_tokens.write().remove(object_id);
}
pub fn prepare_cancel(
&self,
object_id: ObjectId,
reason: &CancelReason,
now: Time,
) -> CancelMessage {
let (token, token_id) = {
let tokens = self.active_tokens.read();
tokens.get(&object_id).map_or_else(
|| (None, self.sender_tag ^ object_id.high() ^ object_id.low()),
|token| (Some(token.clone()), token.token_id()),
)
};
if let Some(token) = token {
token.cancel(reason, now);
}
let sequence = self.next_sequence.fetch_add(1, Ordering::Relaxed);
let msg = CancelMessage::new(token_id, object_id, reason.kind(), now, sequence);
self.mark_seen(object_id, msg.token_id(), sequence);
self.initiated.fetch_add(1, Ordering::Relaxed);
msg
}
pub fn receive_message(
&self,
msg: &CancelMessage,
_received_at: Time,
) -> Option<CancelMessage> {
if self.is_seen(msg.object_id(), msg.token_id(), msg.sequence()) {
self.duplicates.fetch_add(1, Ordering::Relaxed);
return None;
}
self.mark_seen(msg.object_id(), msg.token_id(), msg.sequence());
self.received.fetch_add(1, Ordering::Relaxed);
let token = self.active_tokens.read().get(&msg.object_id()).cloned(); if let Some(token) = token {
let reason = CancelReason::new(msg.kind()).with_timestamp(msg.initiated_at());
token.cancel(&reason, msg.initiated_at());
}
msg.forwarded().map_or_else(
|| {
self.max_hops_reached.fetch_add(1, Ordering::Relaxed);
None
},
|forwarded| {
self.forwarded.fetch_add(1, Ordering::Relaxed);
Some(forwarded)
},
)
}
pub async fn cancel(
&self,
object_id: ObjectId,
reason: &CancelReason,
now: Time,
) -> crate::error::Result<usize> {
let msg = self.prepare_cancel(object_id, reason, now);
match self.sink.broadcast(&msg).await {
Ok(count) => Ok(count),
Err(err) => {
self.pending_retries.write().push_back(msg);
Err(err)
}
}
}
pub async fn handle_message(&self, msg: CancelMessage, now: Time) -> crate::error::Result<()> {
if let Some(forwarded) = self.receive_message(&msg, now) {
match self.sink.broadcast(&forwarded).await {
Ok(_) => Ok(()),
Err(err) => {
self.pending_retries.write().push_back(forwarded);
Err(err)
}
}
} else {
Ok(())
}
}
pub async fn retry_failed_broadcasts(&self) -> (usize, Option<crate::error::Error>) {
struct RetryGuard<'a>(&'a AtomicBool);
impl Drop for RetryGuard<'_> {
fn drop(&mut self) {
self.0.store(false, Ordering::Release);
}
}
if self
.retry_in_progress
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_err()
{
return (0, None);
}
let _retry_guard = RetryGuard(&self.retry_in_progress);
let mut retried_count = 0;
let mut last_error = None;
loop {
let (msg, original_queue_len) = {
let mut retries = self.pending_retries.write();
let msg = retries.pop_front();
let queue_len = retries.len();
(msg, queue_len)
};
let Some(msg) = msg else {
break; };
match self.sink.broadcast(&msg).await {
Ok(_) => {
retried_count += 1;
}
Err(err) => {
{
let mut retries = self.pending_retries.write();
let current_len = retries.len();
if current_len > original_queue_len {
retries.insert(original_queue_len, msg);
} else {
retries.push_front(msg);
}
}
last_error = Some(err);
break; }
}
}
(retried_count, last_error)
}
#[must_use]
pub fn metrics(&self) -> CancelBroadcastMetrics {
CancelBroadcastMetrics {
initiated: self.initiated.load(Ordering::Relaxed),
received: self.received.load(Ordering::Relaxed),
forwarded: self.forwarded.load(Ordering::Relaxed),
duplicates: self.duplicates.load(Ordering::Relaxed),
max_hops_reached: self.max_hops_reached.load(Ordering::Relaxed),
pending_retries: self.pending_retries.read().len() as u64,
}
}
fn is_seen(&self, object_id: ObjectId, token_id: u64, sequence: u64) -> bool {
self.seen_sequences
.read()
.set
.contains(&(object_id, token_id, sequence))
}
fn mark_seen(&self, object_id: ObjectId, token_id: u64, sequence: u64) {
let mut seen = self.seen_sequences.write();
if seen.set.contains(&(object_id, token_id, sequence)) {
return;
}
while seen.set.len() >= self.max_seen {
if seen.remove_oldest().is_none() {
break;
}
}
seen.insert((object_id, token_id, sequence));
}
}
pub trait CleanupHandler: Send + Sync {
#[allow(clippy::result_large_err)]
fn cleanup(&self, object_id: ObjectId, symbols: Vec<Symbol>) -> crate::error::Result<usize>;
fn name(&self) -> &'static str;
}
#[derive(Clone)]
struct PendingSymbolSet {
symbols: Vec<Symbol>,
total_bytes: usize,
_created_at: Time,
}
#[derive(Clone, Debug)]
pub struct CleanupResult {
pub object_id: ObjectId,
pub symbols_cleaned: usize,
pub bytes_freed: usize,
pub within_budget: bool,
pub completed: bool,
pub handlers_run: Vec<String>,
pub handler_errors: Vec<String>,
}
#[derive(Clone, Debug, Default)]
pub struct CleanupStats {
pub pending_objects: usize,
pub pending_symbols: usize,
pub pending_bytes: usize,
}
struct ActiveCleanupGuard<'a> {
object_id: ObjectId,
active: &'a RwLock<HashSet<ObjectId>>,
}
impl Drop for ActiveCleanupGuard<'_> {
fn drop(&mut self) {
self.active.write().remove(&self.object_id);
}
}
pub struct CleanupCoordinator {
pending: RwLock<HashMap<ObjectId, PendingSymbolSet>>,
handlers: RwLock<HashMap<ObjectId, Box<dyn CleanupHandler>>>,
completed: RwLock<HashSet<ObjectId>>,
cleanup_buffer: RwLock<HashMap<ObjectId, Vec<Symbol>>>,
cleanup_active: RwLock<HashSet<ObjectId>>,
default_budget: Budget,
}
impl CleanupCoordinator {
#[must_use]
pub fn new() -> Self {
Self {
pending: RwLock::new(HashMap::new()),
handlers: RwLock::new(HashMap::new()),
completed: RwLock::new(HashSet::new()),
cleanup_buffer: RwLock::new(HashMap::new()),
cleanup_active: RwLock::new(HashSet::new()),
default_budget: Budget::new().with_poll_quota(1000),
}
}
#[must_use]
pub fn with_default_budget(mut self, budget: Budget) -> Self {
self.default_budget = budget;
self
}
#[allow(clippy::significant_drop_tightening)]
pub fn register_pending(&self, object_id: ObjectId, symbol: Symbol, now: Time) {
let mut pending = self.pending.write();
if self.completed.read().contains(&object_id) {
return;
}
let mut cleanup_buffer = self.cleanup_buffer.write();
if cleanup_buffer.contains_key(&object_id) {
cleanup_buffer.entry(object_id).or_default().push(symbol);
return;
}
drop(cleanup_buffer);
let set = pending
.entry(object_id)
.or_insert_with(|| PendingSymbolSet {
symbols: Vec::new(),
total_bytes: 0,
_created_at: now,
});
set.total_bytes = set.total_bytes.saturating_add(symbol.len());
set.symbols.push(symbol);
}
#[allow(clippy::significant_drop_tightening)]
fn restore_retry_state(
&self,
object_id: ObjectId,
handler: Box<dyn CleanupHandler>,
mut pending_set: PendingSymbolSet,
) {
let mut handlers = self.handlers.write();
let mut pending = self.pending.write();
let mut completed = self.completed.write();
if completed.contains(&object_id) {
self.cleanup_buffer.write().remove(&object_id);
return;
}
handlers.insert(object_id, handler);
let mut cleanup_buffer = self.cleanup_buffer.write();
if let Some(buffered_symbols) = cleanup_buffer.remove(&object_id) {
for symbol in buffered_symbols {
pending_set.total_bytes = pending_set.total_bytes.saturating_add(symbol.len());
pending_set.symbols.push(symbol);
}
}
pending.insert(object_id, pending_set);
completed.remove(&object_id);
}
#[allow(clippy::significant_drop_tightening)]
fn restore_pending_only_state(&self, object_id: ObjectId, mut pending_set: PendingSymbolSet) {
let mut pending = self.pending.write();
let mut completed = self.completed.write();
if completed.contains(&object_id) {
self.cleanup_buffer.write().remove(&object_id);
return;
}
let mut cleanup_buffer = self.cleanup_buffer.write();
if let Some(buffered_symbols) = cleanup_buffer.remove(&object_id) {
for symbol in buffered_symbols {
pending_set.total_bytes = pending_set.total_bytes.saturating_add(symbol.len());
pending_set.symbols.push(symbol);
}
}
pending.insert(object_id, pending_set);
completed.remove(&object_id);
}
pub fn register_handler(&self, object_id: ObjectId, handler: impl CleanupHandler + 'static) {
self.handlers.write().insert(object_id, Box::new(handler));
}
#[inline]
fn empty_pending_set() -> PendingSymbolSet {
PendingSymbolSet {
symbols: Vec::new(),
total_bytes: 0,
_created_at: Time::ZERO,
}
}
pub fn clear_pending(&self, object_id: &ObjectId) -> Option<usize> {
self.handlers.write().remove(object_id);
let mut pending = self.pending.write();
self.completed.write().insert(*object_id);
pending.remove(object_id).map(|set| set.symbols.len())
}
pub fn cleanup(&self, object_id: ObjectId, budget: Option<Budget>) -> CleanupResult {
let budget = budget.unwrap_or(self.default_budget);
let mut result = CleanupResult {
object_id,
symbols_cleaned: 0,
bytes_freed: 0,
within_budget: true,
completed: true,
handlers_run: Vec::new(),
handler_errors: Vec::new(),
};
let _active_guard = {
let mut active = self.cleanup_active.write();
if !active.insert(object_id) {
result.completed = false;
result.handler_errors.push(format!(
"cleanup already in progress for object {object_id:?}; \
rejecting reentrant cleanup attempt (br-asupersync-a19xwn)"
));
return result;
}
ActiveCleanupGuard {
object_id,
active: &self.cleanup_active,
}
};
self.cleanup_buffer.write().entry(object_id).or_default();
let handler = { self.handlers.write().remove(&object_id) };
let pending_set = { self.pending.write().remove(&object_id) };
let had_handler = handler.is_some();
if let Some(set) = pending_set {
let symbol_count = set.symbols.len();
let total_bytes = set.total_bytes;
if let Some(handler) = handler {
if budget.poll_quota == 0 {
self.restore_retry_state(object_id, handler, set);
result.within_budget = false;
result.completed = false;
} else {
let handler_name = handler.name().to_string();
let retry_set = set.clone();
result.handlers_run.push(handler_name.clone());
let symbols = set.symbols;
let cleanup_outcome =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
handler.cleanup(object_id, symbols)
}));
match cleanup_outcome {
Ok(Ok(_)) => {
self.completed.write().insert(object_id);
self.cleanup_buffer.write().remove(&object_id);
result.symbols_cleaned = symbol_count;
result.bytes_freed = total_bytes;
}
Ok(Err(err)) => {
self.restore_retry_state(object_id, handler, retry_set);
result.completed = false;
result.handler_errors.push(format!("{handler_name}: {err}"));
}
Err(panic_payload) => {
let panic_msg = panic_payload
.downcast_ref::<&str>()
.map(|s| (*s).to_string())
.or_else(|| panic_payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "unknown panic".to_string());
self.restore_retry_state(object_id, handler, retry_set);
result.completed = false;
result
.handler_errors
.push(format!("{handler_name}: cleanup panicked: {panic_msg}"));
}
}
}
} else {
result.completed = false;
result.handler_errors.push(format!(
"no cleanup handler registered for object {object_id:?}; \
{symbol_count} symbol(s) / {total_bytes} byte(s) deferred \
(br-asupersync-batcyw)"
));
self.restore_pending_only_state(object_id, set);
}
} else {
let buffered_symbol_count = {
let _pending_guard = self.pending.write();
let mut completed = self.completed.write();
let mut cleanup_buffer = self.cleanup_buffer.write();
let count = cleanup_buffer.get(&object_id).map_or(0, Vec::len);
if count == 0 {
cleanup_buffer.remove(&object_id);
if result.completed && had_handler {
completed.insert(object_id);
}
}
count
};
if buffered_symbol_count > 0 {
let new_set = Self::empty_pending_set();
if let Some(handler) = handler {
self.restore_retry_state(object_id, handler, new_set);
} else {
self.restore_pending_only_state(object_id, new_set);
}
result.completed = false; }
}
if result.completed {
self.handlers.write().remove(&object_id);
}
result
}
#[must_use]
pub fn stats(&self) -> CleanupStats {
let pending = self.pending.read();
let mut total_symbols = 0;
let mut total_bytes = 0;
for set in pending.values() {
total_symbols += set.symbols.len();
total_bytes += set.total_bytes;
}
CleanupStats {
pending_objects: pending.len(),
pending_symbols: total_symbols,
pending_bytes: total_bytes,
}
}
}
impl Default for CleanupCoordinator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
include!("symbol_cancel_tests.rs");
#[cfg(test)]
#[path = "symbol_cancel_metamorphic.rs"]
mod symbol_cancel_metamorphic;