use crate::compat::{lock, read_lock, write_lock, Box, HashMap, Vec};
use crate::compat::{Mutex, RwLock, VecDeque};
use alloc::sync::Arc;
use core::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Priority {
High,
#[default]
Normal,
Low,
}
impl Priority {
fn rank(&self) -> u8 {
match self {
Priority::High => 0,
Priority::Normal => 1,
Priority::Low => 2,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ConnectionHandle(pub u64);
static NEXT_HANDLE: AtomicU64 = AtomicU64::new(1);
type SlotFn<T> = Box<dyn FnMut(Arc<T>) + Send + Sync + 'static>;
struct SlotSlot<T: Clone + Send + 'static> {
callback: Mutex<SlotFn<T>>,
pending: Mutex<VecDeque<T>>,
draining: Mutex<bool>,
}
impl<T: Clone + Send + 'static> SlotSlot<T> {
fn new(callback: SlotFn<T>) -> Self {
Self {
callback: Mutex::new(callback),
pending: Mutex::new(VecDeque::new()),
draining: Mutex::new(false),
}
}
fn deliver(&self, value: Arc<T>) -> bool {
let Some(mut callback) = crate::compat::try_lock_recover(&self.callback) else {
self.defer(value);
return false;
};
*lock(&self.draining) = true;
callback(value);
loop {
let mut pending = lock(&self.pending);
if let Some(queued) = pending.pop_front() {
drop(pending);
callback(Arc::new(queued));
continue;
}
*lock(&self.draining) = false;
break;
}
true
}
fn defer(&self, value: Arc<T>) {
lock(&self.pending).push_back((*value).clone());
self.drain_if_orphaned();
}
fn drain_if_orphaned(&self) {
{
let mut draining = lock(&self.draining);
if *draining {
return;
}
*draining = true;
}
let mut callback = match crate::compat::try_lock_recover(&self.callback) {
Some(guard) => guard,
None => {
for _ in 0..64 {
std::thread::yield_now();
if let Some(mut guard) = crate::compat::try_lock_recover(&self.callback) {
return self.finish_drain(&mut guard);
}
}
lock(&self.callback)
}
};
self.finish_drain(&mut callback);
}
fn finish_drain<D: core::ops::DerefMut<Target = SlotFn<T>>>(&self, callback: &mut D) {
loop {
let mut pending = lock(&self.pending);
if let Some(queued) = pending.pop_front() {
drop(pending);
callback(Arc::new(queued));
continue;
}
*lock(&self.draining) = false;
break;
}
}
}
struct SlotEntry<T: Clone + Send + 'static> {
slot: Option<Arc<SlotSlot<T>>>,
once: bool,
blocked: bool,
priority: Priority,
sequence: u64,
}
thread_local! {
#[allow(clippy::missing_const_for_thread_local)]
static EXECUTING_SLOTS: core::cell::RefCell<crate::compat::Vec<(usize, ConnectionHandle)>> =
core::cell::RefCell::new(crate::compat::Vec::new());
}
fn slot_running_here(identity: usize, handle: ConnectionHandle) -> bool {
EXECUTING_SLOTS.with(|set| set.borrow().contains(&(identity, handle)))
}
struct SlotRunning(usize, ConnectionHandle);
impl SlotRunning {
fn new(identity: usize, handle: ConnectionHandle) -> Self {
EXECUTING_SLOTS.with(|set| set.borrow_mut().push((identity, handle)));
Self(identity, handle)
}
}
impl Drop for SlotRunning {
fn drop(&mut self) {
EXECUTING_SLOTS.with(|set| {
let mut set = set.borrow_mut();
if let Some(pos) = set.iter().position(|entry| *entry == (self.0, self.1)) {
set.remove(pos);
}
});
}
}
struct SignalInner<T: Clone + Send + 'static> {
slots: RwLock<HashMap<ConnectionHandle, SlotEntry<T>>>,
}
impl<T: Clone + Send + 'static> SignalInner<T> {
fn new() -> Self {
Self { slots: RwLock::new(HashMap::new()) }
}
fn disconnect(&self, handle: ConnectionHandle) -> bool {
let removed = write_lock(&self.slots).remove(&handle);
let present = removed.is_some();
drop(removed);
present
}
fn block(&self, handle: ConnectionHandle) -> bool {
if let Some(entry) = write_lock(&self.slots).get_mut(&handle) {
entry.blocked = true;
true
} else {
false
}
}
fn unblock(&self, handle: ConnectionHandle) -> bool {
if let Some(entry) = write_lock(&self.slots).get_mut(&handle) {
entry.blocked = false;
true
} else {
false
}
}
fn is_blocked(&self, handle: ConnectionHandle) -> Option<bool> {
read_lock(&self.slots).get(&handle).map(|entry| entry.blocked)
}
fn set_priority(&self, handle: ConnectionHandle, priority: Priority) -> bool {
if let Some(entry) = write_lock(&self.slots).get_mut(&handle) {
entry.priority = priority;
true
} else {
false
}
}
}
#[derive(Default)]
pub struct ConnectionScope {
disconnectors: Mutex<Vec<Box<dyn FnOnce() + Send + 'static>>>,
}
impl ConnectionScope {
pub fn new() -> Self {
Self::default()
}
pub fn clear(&self) {
let pending: Vec<Box<dyn FnOnce() + Send + 'static>> =
core::mem::take(&mut *lock(&self.disconnectors));
for disconnector in pending {
disconnector();
}
}
pub fn disconnect_count(&self) -> usize {
lock(&self.disconnectors).len()
}
fn track(&self, disconnector: Box<dyn FnOnce() + Send + 'static>) {
lock(&self.disconnectors).push(disconnector);
}
}
impl Drop for ConnectionScope {
fn drop(&mut self) {
let pending: Vec<Box<dyn FnOnce() + Send + 'static>> =
core::mem::take(&mut *lock(&self.disconnectors));
for disconnector in pending {
disconnector();
}
}
}
#[derive(Clone)]
pub struct Signal<T: Clone + Send + 'static> {
inner: Arc<SignalInner<T>>,
}
impl<T: Clone + Send + 'static> Signal<T> {
pub fn new() -> Self {
Self { inner: Arc::new(SignalInner::new()) }
}
pub fn connect<F>(&self, slot: F) -> ConnectionHandle
where
F: FnMut(Arc<T>) + Send + Sync + 'static,
{
self.connect_with_priority(slot, Priority::Normal)
}
pub fn connect_with_priority<F>(&self, slot: F, priority: Priority) -> ConnectionHandle
where
F: FnMut(Arc<T>) + Send + Sync + 'static,
{
let sequence = NEXT_HANDLE.fetch_add(1, Ordering::Relaxed);
let handle = ConnectionHandle(sequence);
write_lock(&self.inner.slots).insert(
handle,
SlotEntry {
slot: Some(Arc::new(SlotSlot::new(Box::new(slot)))),
once: false,
blocked: false,
priority,
sequence,
},
);
handle
}
pub fn connect_once<F>(&self, slot: F) -> ConnectionHandle
where
F: FnMut(Arc<T>) + Send + Sync + 'static,
{
let sequence = NEXT_HANDLE.fetch_add(1, Ordering::Relaxed);
let handle = ConnectionHandle(sequence);
write_lock(&self.inner.slots).insert(
handle,
SlotEntry {
slot: Some(Arc::new(SlotSlot::new(Box::new(slot)))),
once: true,
blocked: false,
priority: Priority::Normal,
sequence,
},
);
handle
}
pub fn connect_scoped<F>(&self, owner: &ConnectionScope, slot: F) -> ConnectionHandle
where
F: FnMut(Arc<T>) + Send + Sync + 'static,
{
let handle = self.connect(slot);
self.track_owner(owner, handle);
handle
}
pub fn connect_once_scoped<F>(&self, owner: &ConnectionScope, slot: F) -> ConnectionHandle
where
F: FnMut(Arc<T>) + Send + Sync + 'static,
{
let handle = self.connect_once(slot);
self.track_owner(owner, handle);
handle
}
pub fn disconnect(&self, handle: ConnectionHandle) -> bool {
self.inner.disconnect(handle)
}
pub fn disconnect_all(&self) {
let drained = core::mem::take(&mut *write_lock(&self.inner.slots));
drop(drained);
}
pub fn block(&self, handle: ConnectionHandle) -> bool {
self.inner.block(handle)
}
pub fn unblock(&self, handle: ConnectionHandle) -> bool {
self.inner.unblock(handle)
}
pub fn is_blocked(&self, handle: ConnectionHandle) -> Option<bool> {
self.inner.is_blocked(handle)
}
pub fn is_connected(&self, handle: ConnectionHandle) -> bool {
read_lock(&self.inner.slots).contains_key(&handle)
}
pub fn identity(&self) -> usize {
Arc::as_ptr(&self.inner) as usize
}
pub fn set_priority(&self, handle: ConnectionHandle, priority: Priority) -> bool {
self.inner.set_priority(handle, priority)
}
pub fn emit(&self, value: T) {
self.emit_inner(Arc::new(value));
}
fn emit_inner(&self, arc_value: Arc<T>) {
let snapshot: Vec<(ConnectionHandle, Priority, u64)> = {
let slots = read_lock(&self.inner.slots);
slots.iter().map(|(h, e)| (*h, e.priority, e.sequence)).collect()
};
let mut snapshot = snapshot;
snapshot.sort_by_key(|a| (a.1.rank(), a.2));
let identity = self.identity();
for (handle, _priority, _sequence) in snapshot {
let slot = {
let mut slots = write_lock(&self.inner.slots);
let Some(entry) = slots.get(&handle) else {
continue;
};
if entry.blocked {
None
} else {
let slot = entry.slot.clone();
if entry.once {
slots.remove(&handle);
}
slot
}
};
let Some(slot) = slot else {
continue;
};
if slot_running_here(identity, handle) {
continue;
}
let _running = SlotRunning::new(identity, handle);
let _ = slot.deliver(arc_value.clone());
}
}
pub fn slot_count(&self) -> usize {
read_lock(&self.inner.slots).len()
}
fn track_owner(&self, owner: &ConnectionScope, handle: ConnectionHandle) {
let weak = Arc::downgrade(&self.inner);
owner.track(Box::new(move || {
if let Some(inner) = weak.upgrade() {
let _ = inner.disconnect(handle);
}
}));
}
}
impl<T: Clone + Send + 'static> Default for Signal<T> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod emit_behaviour_tests {
use super::*;
use crate::compat::lock;
use alloc::sync::Arc;
use core::sync::atomic::{AtomicUsize, Ordering};
#[derive(Default)]
struct Trace {
entries: crate::compat::Mutex<alloc::vec::Vec<&'static str>>,
}
impl Trace {
fn push(&self, label: &'static str) {
lock(&self.entries).push(label);
}
fn snapshot(&self) -> alloc::vec::Vec<&'static str> {
lock(&self.entries).clone()
}
}
#[test]
fn a_slot_may_disconnect_itself_from_inside_its_callback() {
use core::sync::atomic::AtomicU64;
let signal = Signal::<u32>::new();
let calls = Arc::new(AtomicUsize::new(0));
let shared_handle = Arc::new(AtomicU64::new(0));
let calls_self = Arc::clone(&calls);
let signal_for_self = signal.clone();
let handle_slot = Arc::clone(&shared_handle);
let handle = signal.connect(move |_| {
calls_self.fetch_add(1, Ordering::SeqCst);
let own = ConnectionHandle(handle_slot.load(Ordering::SeqCst));
assert!(
signal_for_self.disconnect(own),
"a callback must be able to find and remove its own handle"
);
});
shared_handle.store(handle.0, Ordering::SeqCst);
signal.emit(1);
assert_eq!(calls.load(Ordering::SeqCst), 1, "the slot must run exactly once");
assert!(
!signal.is_connected(handle),
"self-disconnect must stick; the removed entry must not be reinstated"
);
signal.emit(2);
assert_eq!(
calls.load(Ordering::SeqCst),
1,
"a self-disconnected slot must not be called by the next emit"
);
}
#[test]
fn a_slot_may_disconnect_a_later_slot_in_the_same_pass() {
let signal = Signal::<u32>::new();
let trace = Arc::new(Trace::default());
let target = {
let trace = Arc::clone(&trace);
signal.connect(move |_| trace.push("target"))
};
let signal_for_cutter = signal.clone();
let trace_first = Arc::clone(&trace);
signal.connect_with_priority(
move |_| {
trace_first.push("cutter");
signal_for_cutter.disconnect(target);
},
Priority::High,
);
signal.emit(1);
assert_eq!(
trace.snapshot(),
alloc::vec!["cutter"],
"the disconnected slot must be skipped in the same pass"
);
}
#[test]
fn connecting_inside_a_callback_does_not_run_in_the_same_pass() {
let signal = Signal::<u32>::new();
let trace = Arc::new(Trace::default());
let signal_for_adder = signal.clone();
let trace_adder = Arc::clone(&trace);
signal.connect(move |_| {
trace_adder.push("adder");
let trace_late = Arc::clone(&trace_adder);
signal_for_adder.connect(move |_| trace_late.push("late"));
});
signal.emit(1);
assert_eq!(
trace.snapshot(),
alloc::vec!["adder"],
"a slot connected during emit must wait for the next emit"
);
lock(&trace.entries).clear();
signal.emit(2);
let mut order = trace.snapshot();
order.sort_unstable();
assert_eq!(
order,
alloc::vec!["adder", "late"],
"both the original and the newly connected slot must run on the next emit"
);
assert_eq!(signal.slot_count(), 3);
}
#[test]
fn a_re_entrant_emit_skips_the_slot_still_on_the_stack() {
let signal = Signal::<u32>::new();
let trace = Arc::new(Trace::default());
let nested_seen = Arc::new(AtomicUsize::new(0));
{
let trace = Arc::clone(&trace);
let nested = Arc::clone(&nested_seen);
signal.connect_with_priority(
move |_| {
trace.push("observer");
nested.fetch_add(1, Ordering::SeqCst);
},
Priority::Low,
);
}
let signal_for_reentry = signal.clone();
let trace_recur = Arc::clone(&trace);
let depth = Arc::new(AtomicUsize::new(0));
let depth_inner = Arc::clone(&depth);
signal.connect_with_priority(
move |_| {
trace_recur.push("recur");
if depth_inner.fetch_add(1, Ordering::SeqCst) == 0 {
signal_for_reentry.emit(0);
}
},
Priority::High,
);
signal.emit(1);
let order = trace.snapshot();
assert_eq!(
order.iter().filter(|l| **l == "recur").count(),
1,
"the recursive slot must run once, not twice: {order:?}"
);
assert!(
order.iter().filter(|l| **l == "observer").count() >= 2,
"the re-entrant pass must still reach the other slot: {order:?}"
);
assert_eq!(
nested_seen.load(Ordering::SeqCst),
2,
"the observer must have been called by both the outer and the nested pass"
);
}
#[test]
fn a_once_slot_runs_once_and_is_removed() {
let signal = Signal::<u32>::new();
let calls = Arc::new(AtomicUsize::new(0));
let calls_once = Arc::clone(&calls);
let handle = signal.connect_once(move |_| {
calls_once.fetch_add(1, Ordering::SeqCst);
});
signal.emit(1);
assert_eq!(calls.load(Ordering::SeqCst), 1);
assert!(!signal.is_connected(handle), "a once slot must be removed after it runs");
signal.emit(2);
assert_eq!(calls.load(Ordering::SeqCst), 1, "a once slot must not run twice");
}
#[test]
fn concurrent_emits_claim_a_once_slot_only_once() {
use std::sync::Barrier;
const ROUNDS: usize = 200;
let signal = Signal::<u32>::new();
let start = Arc::new(Barrier::new(3));
let done = Arc::new(Barrier::new(3));
let mut workers = Vec::new();
for _ in 0..2 {
let worker_signal = signal.clone();
let worker_start = Arc::clone(&start);
let worker_done = Arc::clone(&done);
workers.push(std::thread::spawn(move || {
for round in 0..ROUNDS {
worker_start.wait();
worker_signal.emit(round as u32);
worker_done.wait();
}
}));
}
for _ in 0..ROUNDS {
let calls = Arc::new(AtomicUsize::new(0));
let calls_once = Arc::clone(&calls);
let handle = signal.connect_once(move |_| {
calls_once.fetch_add(1, Ordering::SeqCst);
});
start.wait();
done.wait();
assert_eq!(
calls.load(Ordering::SeqCst),
1,
"simultaneous emitters must invoke a once-slot exactly once"
);
assert!(!signal.is_connected(handle), "the once-slot must be removed");
}
for worker in workers {
worker.join().expect("emit worker must not panic");
}
}
#[test]
fn concurrent_pending_values_are_delivered_fifo() {
use std::sync::Barrier;
let signal = Signal::<u32>::new();
let entered = Arc::new(Barrier::new(2));
let release = Arc::new(Barrier::new(2));
let observed = Arc::new(std::sync::Mutex::new(Vec::new()));
let slot_entered = Arc::clone(&entered);
let slot_release = Arc::clone(&release);
let slot_observed = Arc::clone(&observed);
signal.connect(move |value| {
let value = *value;
slot_observed.lock().unwrap().push(value);
if value == 1 {
slot_entered.wait();
slot_release.wait();
}
});
let worker_signal = signal.clone();
let worker = std::thread::spawn(move || worker_signal.emit(1));
entered.wait();
signal.emit(2);
signal.emit(3);
release.wait();
worker.join().expect("the in-flight emitter must complete");
assert_eq!(*observed.lock().unwrap(), vec![1, 2, 3]);
}
#[test]
fn a_value_deferred_as_the_drainer_exits_is_still_delivered() {
use std::sync::Barrier;
let signal = Signal::<u32>::new();
let entered = Arc::new(Barrier::new(2));
let release = Arc::new(Barrier::new(2));
let observed = Arc::new(std::sync::Mutex::new(Vec::new()));
let slot_entered = Arc::clone(&entered);
let slot_release = Arc::clone(&release);
let slot_observed = Arc::clone(&observed);
signal.connect(move |value| {
let value = *value;
slot_observed.lock().unwrap().push(value);
if value == 1 {
slot_entered.wait();
slot_release.wait();
}
});
let worker_signal = signal.clone();
let worker = std::thread::spawn(move || worker_signal.emit(1));
entered.wait();
let deferred_signal = signal.clone();
let deferred = std::thread::spawn(move || deferred_signal.emit(2));
release.wait();
deferred.join().expect("the deferred emitter must complete");
worker.join().expect("the in-flight emitter must complete");
assert_eq!(
*observed.lock().unwrap(),
vec![1, 2],
"the value deferred during the drainer's exit must be delivered"
);
}
#[test]
fn a_blocked_slot_is_skipped_and_unblocking_restores_it() {
let signal = Signal::<u32>::new();
let calls = Arc::new(AtomicUsize::new(0));
let calls_inner = Arc::clone(&calls);
let handle = signal.connect(move |_| {
calls_inner.fetch_add(1, Ordering::SeqCst);
});
assert!(signal.block(handle));
signal.emit(1);
assert_eq!(calls.load(Ordering::SeqCst), 0, "a blocked slot must not run");
assert!(signal.is_connected(handle), "blocking must not disconnect");
assert!(signal.unblock(handle));
signal.emit(2);
assert_eq!(calls.load(Ordering::SeqCst), 1, "an unblocked slot must run again");
}
#[test]
fn slots_run_in_priority_order() {
let signal = Signal::<u32>::new();
let trace = Arc::new(Trace::default());
for (label, priority) in [
("low", Priority::Low),
("normal", Priority::Normal),
("high", Priority::High),
("normal2", Priority::Normal),
] {
let trace = Arc::clone(&trace);
signal.connect_with_priority(move |_| trace.push(label), priority);
}
signal.emit(1);
let order = trace.snapshot();
assert_eq!(order.first(), Some(&"high"), "High must run first");
assert_eq!(order.last(), Some(&"low"), "Low must run last");
assert_eq!(order.len(), 4, "every non-blocked slot must run exactly once, got {order:?}");
}
#[test]
fn set_priority_inside_a_callback_does_not_reorder_the_current_pass() {
let signal = Signal::<u32>::new();
let trace = Arc::new(Trace::default());
let later = {
let trace = Arc::clone(&trace);
signal.connect_with_priority(move |_| trace.push("later"), Priority::Low)
};
let signal_for_bump = signal.clone();
let trace_first = Arc::clone(&trace);
signal.connect_with_priority(
move |_| {
trace_first.push("first");
signal_for_bump.set_priority(later, Priority::High);
},
Priority::High,
);
signal.emit(1);
assert_eq!(
trace.snapshot(),
alloc::vec!["first", "later"],
"the pass order is fixed at snapshot time"
);
}
#[test]
fn disconnect_all_inside_a_callback_stops_the_rest_of_the_pass() {
let signal = Signal::<u32>::new();
let trace = Arc::new(Trace::default());
{
let trace = Arc::clone(&trace);
signal.connect_with_priority(move |_| trace.push("second"), Priority::Normal);
}
let signal_for_clear = signal.clone();
let trace_first = Arc::clone(&trace);
signal.connect_with_priority(
move |_| {
trace_first.push("first");
signal_for_clear.disconnect_all();
},
Priority::High,
);
signal.emit(1);
assert_eq!(
trace.snapshot(),
alloc::vec!["first"],
"slots after disconnect_all must be skipped"
);
assert_eq!(signal.slot_count(), 0u64 as usize, "no slots must remain");
}
#[test]
fn a_scoped_connection_is_dropped_with_its_scope() {
let signal = Signal::<u32>::new();
let calls = Arc::new(AtomicUsize::new(0));
{
let scope = ConnectionScope::new();
let calls_inner = Arc::clone(&calls);
signal.connect_scoped(&scope, move |_| {
calls_inner.fetch_add(1, Ordering::SeqCst);
});
signal.emit(1);
assert_eq!(calls.load(Ordering::SeqCst), 1, "the scoped slot must run while alive");
}
signal.emit(2);
assert_eq!(
calls.load(Ordering::SeqCst),
1,
"the scoped slot must be gone once its scope drops"
);
}
#[test]
fn equal_priority_slots_fire_in_connection_order() {
let signal = Signal::<u32>::new();
let trace = Arc::new(Trace::default());
let mut handles = alloc::vec::Vec::new();
for label in ["first", "second", "third", "fourth"] {
let trace = Arc::clone(&trace);
handles.push(signal.connect(move |_| trace.push(label)));
}
signal.emit(1);
assert_eq!(
trace.snapshot(),
alloc::vec!["first", "second", "third", "fourth"],
"same-priority slots must run in connection order"
);
signal.disconnect(handles[0]);
signal.disconnect(handles[1]);
let trace2 = Arc::new(Trace::default());
{
let trace2 = Arc::clone(&trace2);
signal.connect(move |_| trace2.push("fifth"));
}
signal.emit(2);
assert_eq!(
trace.snapshot(),
alloc::vec!["first", "second", "third", "fourth", "third", "fourth"],
"the two surviving original slots must run in connection order"
);
assert_eq!(trace2.snapshot(), alloc::vec!["fifth"], "the new slot must run once");
}
#[test]
fn a_panicking_slot_is_restored_so_later_emits_still_reach_it() {
let signal = Signal::<u32>::new();
let calls = Arc::new(AtomicUsize::new(0));
let should_panic = Arc::new(core::sync::atomic::AtomicBool::new(true));
let calls_slot = Arc::clone(&calls);
let panic_flag = Arc::clone(&should_panic);
signal.connect(move |_| {
calls_slot.fetch_add(1, Ordering::SeqCst);
if panic_flag.load(Ordering::SeqCst) {
panic!("deliberate unwind from a slot callback");
}
});
let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| signal.emit(1)));
assert!(caught.is_err(), "the callback must have unwound");
assert_eq!(signal.slot_count(), 1, "the slot must still be counted after an unwind");
should_panic.store(false, Ordering::SeqCst);
signal.emit(2);
assert_eq!(
calls.load(Ordering::SeqCst),
2,
"the slot must run on the next emit, not be left as a dead entry"
);
}
#[cfg(not(alloc_frugal))]
#[test]
fn a_concurrent_emit_is_delivered_rather_than_silently_dropped() {
use std::sync::Barrier;
let signal = Signal::<u32>::new();
let delivered = Arc::new(AtomicUsize::new(0));
let entered = Arc::new(Barrier::new(2));
let release = Arc::new(Barrier::new(2));
let delivered_slot = Arc::clone(&delivered);
let entered_slot = Arc::clone(&entered);
let release_slot = Arc::clone(&release);
signal.connect(move |_| {
delivered_slot.fetch_add(1, Ordering::SeqCst);
if delivered_slot.load(Ordering::SeqCst) == 1 {
entered_slot.wait();
release_slot.wait();
}
});
let worker_signal = signal.clone();
let worker = std::thread::spawn(move || {
worker_signal.emit(1);
});
entered.wait();
let emitter_signal = signal.clone();
let emitter = std::thread::spawn(move || {
emitter_signal.emit(2);
});
release.wait();
worker.join().expect("the worker thread must not panic");
emitter.join().expect("the emitter thread must not panic");
assert_eq!(
delivered.load(Ordering::SeqCst),
2,
"both emits must reach the connected slot; a dropped value is the silent-loss defect"
);
}
#[cfg(not(alloc_frugal))]
#[test]
fn cross_signal_forwards_across_threads_do_not_deadlock() {
use std::sync::mpsc;
use std::sync::Barrier;
use std::time::Duration;
let a = Signal::<u32>::new();
let b = Signal::<u32>::new();
let both_inside = Arc::new(Barrier::new(2));
let b_for_a = b.clone();
let barrier_a = Arc::clone(&both_inside);
a.connect(move |value: Arc<u32>| {
if *value == 1 {
barrier_a.wait();
b_for_a.emit(2);
}
});
let a_for_b = a.clone();
let barrier_b = Arc::clone(&both_inside);
b.connect(move |value: Arc<u32>| {
if *value == 1 {
barrier_b.wait();
a_for_b.emit(2);
}
});
let (done_tx_1, done_rx_1) = mpsc::channel();
let (done_tx_2, done_rx_2) = mpsc::channel();
let a1 = a.clone();
let b2 = b.clone();
let t1 = std::thread::spawn(move || {
a1.emit(1);
let _ = done_tx_1.send(());
});
let t2 = std::thread::spawn(move || {
b2.emit(1);
let _ = done_tx_2.send(());
});
assert!(
done_rx_1.recv_timeout(Duration::from_secs(5)).is_ok(),
"thread 1 must complete: a mutual cross-signal forward must not dead-lock"
);
assert!(
done_rx_2.recv_timeout(Duration::from_secs(5)).is_ok(),
"thread 2 must complete: a mutual cross-signal forward must not dead-lock"
);
t1.join().expect("thread 1 must not panic");
t2.join().expect("thread 2 must not panic");
}
}
#[cfg(all(test, not(alloc_frugal)))]
mod drop_outside_lock_tests {
use super::*;
use alloc::sync::Arc;
use std::sync::mpsc;
use std::time::Duration;
const BOUND: Duration = Duration::from_secs(5);
fn arm_reentrant_scope(signal: &Signal<u32>) -> ConnectionHandle {
let scope = Arc::new(ConnectionScope::new());
let _scoped_handle = signal.connect_scoped(&scope, |_| {});
let captured = Arc::clone(&scope);
let capture_handle = signal.connect(move |_| {
let _ = &captured;
});
drop(scope);
capture_handle
}
#[test]
fn disconnecting_a_capture_that_reenters_still_completes() {
let signal = Signal::<u32>::new();
let handle = arm_reentrant_scope(&signal);
let (done_tx, done_rx) = mpsc::channel();
let worker_signal = signal.clone();
let worker = std::thread::spawn(move || {
let removed = worker_signal.disconnect(handle);
let _ = done_tx.send(removed);
});
let removed = done_rx
.recv_timeout(BOUND)
.expect("disconnect must complete without dropping the capture under the slot lock");
assert!(removed, "the capture handle was connected, so disconnect must report true");
worker.join().expect("the disconnect worker must not panic");
assert_eq!(
signal.slot_count(),
0,
"dropping the capture also frees the scope, whose disconnector removes the scoped slot"
);
}
#[test]
fn disconnect_all_with_a_reentrant_capture_still_completes() {
let signal = Signal::<u32>::new();
let _ = arm_reentrant_scope(&signal);
let (done_tx, done_rx) = mpsc::channel();
let worker_signal = signal.clone();
let worker = std::thread::spawn(move || {
worker_signal.disconnect_all();
let _ = done_tx.send(());
});
assert!(
done_rx.recv_timeout(BOUND).is_ok(),
"disconnect_all must complete without dropping captures under the slot lock"
);
worker.join().expect("the disconnect_all worker must not panic");
assert_eq!(signal.slot_count(), 0, "every slot must be gone");
}
#[test]
fn a_once_slot_with_a_reentrant_capture_is_removed_safely() {
let signal = Signal::<u32>::new();
let scope = Arc::new(ConnectionScope::new());
let after = signal.connect_scoped(&scope, |_| {});
let scoped_signal = signal.clone();
let captured = Arc::clone(&scope);
signal.connect_once(move |_| {
let _ = &captured;
let _ = scoped_signal.is_connected(after);
});
drop(scope);
let (done_tx, done_rx) = mpsc::channel();
let worker_signal = signal.clone();
let worker = std::thread::spawn(move || {
worker_signal.emit(1);
let _ = done_tx.send(());
});
assert!(
done_rx.recv_timeout(BOUND).is_ok(),
"a once slot whose capture can re-enter must not deadlock the emit"
);
worker.join().expect("the emit worker must not panic");
}
#[test]
fn clearing_a_scope_whose_capture_reenters_clear_still_completes() {
use core::time::Duration;
use std::sync::mpsc;
struct ReentrantClear {
scope: Arc<ConnectionScope>,
}
impl Drop for ReentrantClear {
fn drop(&mut self) {
self.scope.clear();
}
}
let (done_tx, done_rx) = mpsc::channel();
let worker = std::thread::spawn(move || {
let signal = Signal::<u32>::new();
let scope = Arc::new(ConnectionScope::new());
signal.connect_scoped(&scope, |_| {});
let for_slot = Arc::clone(&scope);
signal.connect_scoped(&scope, move |_| {
let _reentrant = ReentrantClear { scope: Arc::clone(&for_slot) };
let _ = &_reentrant;
});
scope.clear();
let _ = done_tx.send(signal.slot_count());
});
let slots = done_rx
.recv_timeout(Duration::from_secs(5))
.expect("a scope clear/auto-clear must not deadlock on a re-entrant disconnector");
assert_eq!(slots, 0, "every tracked slot must be disconnected by the scope");
worker.join().expect("the scope worker must not panic");
}
#[test]
fn plain_disconnect_and_clear_still_behave() {
let signal = Signal::<u32>::new();
assert!(
!signal.disconnect(ConnectionHandle(u64::MAX)),
"an unknown handle must report false"
);
signal.disconnect_all();
assert_eq!(signal.slot_count(), 0);
let handle = signal.connect(|_| {});
assert!(signal.is_connected(handle));
assert!(signal.disconnect(handle), "a known handle must report true");
assert!(!signal.is_connected(handle));
}
}