1#![forbid(unsafe_code)]
2
3use crate::cancellation::{CancellationSource, CancellationToken};
18use std::collections::HashSet;
19use std::sync::mpsc;
20use std::thread;
21use web_time::{Duration, Instant};
22
23pub type SubId = u64;
28
29pub trait Subscription<M: Send + 'static>: Send {
34 fn id(&self) -> SubId;
39
40 fn run(&self, sender: mpsc::Sender<M>, stop: StopSignal);
46}
47
48#[derive(Clone)]
55pub struct StopSignal {
56 token: CancellationToken,
57}
58
59impl StopSignal {
60 pub(crate) fn new() -> (Self, StopTrigger) {
62 let source = CancellationSource::new();
63 let signal = Self {
64 token: source.token(),
65 };
66 let trigger = StopTrigger { source };
67 (signal, trigger)
68 }
69
70 pub fn is_stopped(&self) -> bool {
72 self.token.is_cancelled()
73 }
74
75 pub fn wait_timeout(&self, duration: Duration) -> bool {
81 self.token.wait_timeout(duration)
82 }
83
84 pub fn cancellation_token(&self) -> &CancellationToken {
89 &self.token
90 }
91}
92
93pub(crate) struct StopTrigger {
97 source: CancellationSource,
98}
99
100impl StopTrigger {
101 pub(crate) fn stop(&self) {
103 self.source.cancel();
104 }
105}
106
107pub(crate) struct RunningSubscription {
109 pub(crate) id: SubId,
110 trigger: StopTrigger,
111 thread: Option<thread::JoinHandle<()>>,
112 panicked: std::sync::Arc<std::sync::atomic::AtomicBool>,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
118pub(crate) struct SubscriptionFailure {
119 pub(crate) id: SubId,
120 pub(crate) error: String,
121}
122
123const SUBSCRIPTION_STOP_JOIN_TIMEOUT: Duration = Duration::from_millis(250);
124const SUBSCRIPTION_STOP_JOIN_POLL: Duration = Duration::from_millis(1);
130
131impl RunningSubscription {
132 pub(crate) fn has_panicked(&self) -> bool {
134 self.panicked.load(std::sync::atomic::Ordering::Acquire)
135 }
136
137 pub(crate) fn signal_stop(&self) {
142 self.trigger.stop();
143 }
144
145 pub(crate) fn join_bounded(mut self) -> Option<thread::JoinHandle<()>> {
150 let handle = self.thread.take()?;
151 let start = Instant::now();
152
153 if handle.is_finished() {
155 let _ = handle.join();
156 tracing::trace!(
157 sub_id = self.id,
158 panicked = self.has_panicked(),
159 elapsed_us = start.elapsed().as_micros() as u64,
160 "subscription join (fast path)"
161 );
162 return None;
163 }
164
165 while !handle.is_finished() {
167 if start.elapsed() >= SUBSCRIPTION_STOP_JOIN_TIMEOUT {
168 tracing::warn!(
169 sub_id = self.id,
170 panicked = self.has_panicked(),
171 timeout_ms = SUBSCRIPTION_STOP_JOIN_TIMEOUT.as_millis() as u64,
172 "subscription join timed out, detaching thread"
173 );
174 return Some(handle);
175 }
176 thread::sleep(SUBSCRIPTION_STOP_JOIN_POLL);
177 }
178
179 let _ = handle.join();
180 tracing::trace!(
181 sub_id = self.id,
182 panicked = self.has_panicked(),
183 elapsed_us = start.elapsed().as_micros() as u64,
184 "subscription join (slow path)"
185 );
186 None
187 }
188
189 #[cfg_attr(not(test), allow(dead_code))]
194 pub(crate) fn stop(mut self) {
195 self.trigger.stop();
196 if let Some(handle) = self.thread.take() {
197 let start = Instant::now();
198 if handle.is_finished() {
200 let _ = handle.join();
201 tracing::trace!(
202 sub_id = self.id,
203 panicked = self.has_panicked(),
204 elapsed_us = start.elapsed().as_micros() as u64,
205 "subscription stop (fast path)"
206 );
207 return;
208 }
209 while !handle.is_finished() {
211 if start.elapsed() >= SUBSCRIPTION_STOP_JOIN_TIMEOUT {
212 tracing::warn!(
213 sub_id = self.id,
214 panicked = self.has_panicked(),
215 timeout_ms = SUBSCRIPTION_STOP_JOIN_TIMEOUT.as_millis() as u64,
216 "subscription did not stop within timeout; detaching thread"
217 );
218 return;
219 }
220 thread::sleep(SUBSCRIPTION_STOP_JOIN_POLL);
221 }
222 let _ = handle.join();
223 tracing::trace!(
224 sub_id = self.id,
225 panicked = self.has_panicked(),
226 elapsed_us = start.elapsed().as_micros() as u64,
227 "subscription stop (slow path)"
228 );
229 }
230 }
231}
232
233impl Drop for RunningSubscription {
234 fn drop(&mut self) {
235 self.trigger.stop();
236 }
238}
239
240pub(crate) struct SubscriptionManager<M: Send + 'static> {
242 active: Vec<RunningSubscription>,
243 sender: mpsc::Sender<M>,
244 receiver: mpsc::Receiver<M>,
245 failure_sender: mpsc::Sender<SubscriptionFailure>,
246 failure_receiver: mpsc::Receiver<SubscriptionFailure>,
247}
248
249impl<M: Send + 'static> SubscriptionManager<M> {
250 pub(crate) fn new() -> Self {
251 let (sender, receiver) = mpsc::channel();
252 let (failure_sender, failure_receiver) = mpsc::channel();
253 Self {
254 active: Vec::new(),
255 sender,
256 receiver,
257 failure_sender,
258 failure_receiver,
259 }
260 }
261
262 pub(crate) fn reconcile(&mut self, subscriptions: Vec<Box<dyn Subscription<M>>>) {
269 let reconcile_start = Instant::now();
270 let new_ids: HashSet<SubId> = subscriptions.iter().map(|s| s.id()).collect();
271 let active_count_before = self.active.len();
272
273 crate::debug_trace!(
274 "reconcile: new_ids={:?}, active_before={}",
275 new_ids,
276 active_count_before
277 );
278 tracing::trace!(
279 new_id_count = new_ids.len(),
280 active_before = active_count_before,
281 new_ids = ?new_ids,
282 "subscription reconcile starting"
283 );
284
285 let mut remaining = Vec::new();
287 let mut to_stop = Vec::new();
288 for running in self.active.drain(..) {
289 if new_ids.contains(&running.id) {
290 remaining.push(running);
291 } else {
292 crate::debug_trace!("stopping subscription: id={}", running.id);
293 tracing::debug!(sub_id = running.id, "Stopping subscription");
294 crate::effect_system::record_subscription_stop("subscription", running.id, 0);
295 crate::effect_system::record_dynamics_sub_stop();
296 to_stop.push(running);
297 }
298 }
299 for running in &to_stop {
301 running.signal_stop();
302 }
303 let stopped_count = to_stop.len();
304 for running in to_stop {
306 let _ = running.join_bounded();
307 }
308 self.active = remaining;
309
310 let mut started_count = 0usize;
312 let mut active_ids: HashSet<SubId> = self.active.iter().map(|r| r.id).collect();
313 for sub in subscriptions {
314 let id = sub.id();
315 if !active_ids.insert(id) {
316 continue;
317 }
318 started_count += 1;
319
320 crate::debug_trace!("starting subscription: id={}", id);
321 tracing::debug!(sub_id = id, "Starting subscription");
322 crate::effect_system::record_subscription_start("subscription", id);
323 crate::effect_system::record_dynamics_sub_start();
324 let (signal, trigger) = StopSignal::new();
325 let sender = self.sender.clone();
326 let panicked = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
327 let panicked_flag = panicked.clone();
328 let sub_id_for_thread = id;
329 let failure_sender = self.failure_sender.clone();
330
331 let thread = thread::spawn(move || {
332 let result = ftui_core::with_panic_cleanup_suppressed(|| {
336 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
337 sub.run(sender, signal);
338 }))
339 });
340 if let Err(payload) = result {
341 panicked_flag.store(true, std::sync::atomic::Ordering::Release);
342 crate::effect_system::record_dynamics_sub_panic();
343 let panic_msg = match payload.downcast_ref::<&str>() {
344 Some(s) => (*s).to_string(),
345 None => match payload.downcast_ref::<String>() {
346 Some(s) => s.clone(),
347 None => "unknown panic payload".to_string(),
348 },
349 };
350 crate::effect_system::error_effect_panic(
351 "subscription",
352 &format!("sub_id={sub_id_for_thread}: {panic_msg}"),
353 );
354 let _ = failure_sender.send(SubscriptionFailure {
355 id: sub_id_for_thread,
356 error: panic_msg,
357 });
358 }
359 });
360
361 self.active.push(RunningSubscription {
362 id,
363 trigger,
364 thread: Some(thread),
365 panicked,
366 });
367 }
368
369 let active_count_after = self.active.len();
370 let reconcile_elapsed_us = reconcile_start.elapsed().as_micros() as u64;
371 crate::effect_system::record_dynamics_reconcile(reconcile_elapsed_us);
372 crate::debug_trace!("reconcile complete: active_after={}", active_count_after);
373 tracing::trace!(
374 active_before = active_count_before,
375 active_after = active_count_after,
376 started = started_count,
377 stopped = stopped_count,
378 reconcile_us = reconcile_elapsed_us,
379 "subscription reconcile complete"
380 );
381 }
382
383 pub(crate) fn drain_messages(&self) -> Vec<M> {
385 let mut messages = Vec::new();
386 while let Ok(msg) = self.receiver.try_recv() {
387 messages.push(msg);
388 }
389 messages
390 }
391
392 pub(crate) fn drain_failures(&self) -> Vec<SubscriptionFailure> {
394 let mut failures = Vec::new();
395 while let Ok(failure) = self.failure_receiver.try_recv() {
396 failures.push(failure);
397 }
398 failures
399 }
400
401 #[inline]
403 pub(crate) fn active_count(&self) -> usize {
404 self.active.len()
405 }
406
407 pub(crate) fn stop_all(&mut self) {
416 let count = self.active.len();
417 if count == 0 {
418 return;
419 }
420 let start = Instant::now();
421
422 for running in &self.active {
424 running.signal_stop();
425 }
426
427 let signal_elapsed_us = start.elapsed().as_micros() as u64;
428 tracing::trace!(
429 target: "ftui.runtime",
430 count,
431 signal_elapsed_us,
432 "subscription stop_all phase 1 (signal) complete"
433 );
434
435 let mut panicked_count = 0_usize;
437 let mut timed_out_count = 0_usize;
438 for running in self.active.drain(..) {
439 if running.has_panicked() {
440 panicked_count += 1;
441 }
442 if running.join_bounded().is_some() {
443 timed_out_count += 1;
444 }
445 }
446
447 let shutdown_elapsed_us = start.elapsed().as_micros() as u64;
448 crate::effect_system::record_dynamics_shutdown(shutdown_elapsed_us, timed_out_count as u64);
449 tracing::debug!(
450 target: "ftui.runtime",
451 count,
452 panicked_count,
453 timed_out_count,
454 elapsed_us = shutdown_elapsed_us,
455 "subscription stop_all complete"
456 );
457 }
458}
459
460impl<M: Send + 'static> Drop for SubscriptionManager<M> {
461 fn drop(&mut self) {
462 self.stop_all();
463 }
464}
465
466pub struct Every<M: Send + 'static> {
478 id: SubId,
479 interval: Duration,
480 make_msg: Box<dyn Fn() -> M + Send + Sync>,
481}
482
483impl<M: Send + 'static> Every<M> {
484 pub fn new(interval: Duration, make_msg: impl Fn() -> M + Send + Sync + 'static) -> Self {
486 let id = interval.as_nanos() as u64 ^ 0x5449_434B; Self {
489 id,
490 interval,
491 make_msg: Box::new(make_msg),
492 }
493 }
494
495 pub fn with_id(
497 id: SubId,
498 interval: Duration,
499 make_msg: impl Fn() -> M + Send + Sync + 'static,
500 ) -> Self {
501 Self {
502 id,
503 interval,
504 make_msg: Box::new(make_msg),
505 }
506 }
507}
508
509impl<M: Send + 'static> Subscription<M> for Every<M> {
510 fn id(&self) -> SubId {
511 self.id
512 }
513
514 fn run(&self, sender: mpsc::Sender<M>, stop: StopSignal) {
515 let mut tick_count: u64 = 0;
516 crate::debug_trace!(
517 "Every subscription started: id={}, interval={:?}",
518 self.id,
519 self.interval
520 );
521 loop {
522 if stop.wait_timeout(self.interval) {
523 crate::debug_trace!(
524 "Every subscription stopped: id={}, sent {} ticks",
525 self.id,
526 tick_count
527 );
528 break;
529 }
530 tick_count += 1;
531 let msg = (self.make_msg)();
532 if sender.send(msg).is_err() {
533 crate::debug_trace!(
534 "Every subscription channel closed: id={}, sent {} ticks",
535 self.id,
536 tick_count
537 );
538 break;
539 }
540 }
541 }
542}
543
544#[cfg(test)]
545mod tests {
546 use super::*;
547
548 #[derive(Debug, Clone, PartialEq)]
549 enum TestMsg {
550 Tick,
551 Value(i32),
552 }
553
554 struct ChannelSubscription<M: Send + 'static> {
555 id: SubId,
556 receiver: mpsc::Receiver<M>,
557 poll: Duration,
558 }
559
560 impl<M: Send + 'static> ChannelSubscription<M> {
561 fn new(id: SubId, receiver: mpsc::Receiver<M>) -> Self {
562 Self {
563 id,
564 receiver,
565 poll: Duration::from_millis(5),
566 }
567 }
568 }
569
570 impl<M: Send + 'static> Subscription<M> for ChannelSubscription<M> {
571 fn id(&self) -> SubId {
572 self.id
573 }
574
575 fn run(&self, sender: mpsc::Sender<M>, stop: StopSignal) {
576 loop {
577 if stop.is_stopped() {
578 break;
579 }
580 match self.receiver.recv_timeout(self.poll) {
581 Ok(msg) => {
582 if sender.send(msg).is_err() {
583 break;
584 }
585 }
586 Err(mpsc::RecvTimeoutError::Timeout) => {}
587 Err(mpsc::RecvTimeoutError::Disconnected) => break,
588 }
589 }
590 }
591 }
592
593 fn channel_subscription(id: SubId) -> (ChannelSubscription<TestMsg>, mpsc::Sender<TestMsg>) {
594 let (tx, rx) = mpsc::channel();
595 (ChannelSubscription::new(id, rx), tx)
596 }
597
598 #[test]
599 fn stop_signal_starts_false() {
600 let (signal, _trigger) = StopSignal::new();
601 assert!(!signal.is_stopped());
602 }
603
604 #[test]
605 fn stop_signal_becomes_true_after_trigger() {
606 let (signal, trigger) = StopSignal::new();
607 trigger.stop();
608 assert!(signal.is_stopped());
609 }
610
611 #[test]
612 fn stop_signal_wait_returns_true_when_stopped() {
613 let (signal, trigger) = StopSignal::new();
614 trigger.stop();
615 assert!(signal.wait_timeout(Duration::from_millis(100)));
616 }
617
618 #[test]
619 fn stop_signal_wait_returns_false_on_timeout() {
620 let (signal, _trigger) = StopSignal::new();
621 assert!(!signal.wait_timeout(Duration::from_millis(10)));
622 }
623
624 #[test]
625 fn channel_subscription_forwards_messages() {
626 let (sub, event_tx) = channel_subscription(1);
627 let (tx, rx) = mpsc::channel();
628 let (signal, trigger) = StopSignal::new();
629
630 let handle = thread::spawn(move || {
631 sub.run(tx, signal);
632 });
633
634 event_tx.send(TestMsg::Value(1)).unwrap();
635 event_tx.send(TestMsg::Value(2)).unwrap();
636 thread::sleep(Duration::from_millis(10));
637 trigger.stop();
638 handle.join().unwrap();
639
640 let msgs: Vec<_> = rx.try_iter().collect();
641 assert_eq!(msgs, vec![TestMsg::Value(1), TestMsg::Value(2)]);
642 }
643
644 #[test]
645 fn every_subscription_fires() {
646 let sub = Every::new(Duration::from_millis(10), || TestMsg::Tick);
647 let (tx, rx) = mpsc::channel();
648 let (signal, trigger) = StopSignal::new();
649
650 let handle = thread::spawn(move || {
651 sub.run(tx, signal);
652 });
653
654 thread::sleep(Duration::from_millis(50));
656 trigger.stop();
657 handle.join().unwrap();
658
659 let msgs: Vec<_> = rx.try_iter().collect();
660 assert!(!msgs.is_empty(), "Should have received at least one tick");
661 assert!(msgs.iter().all(|m| *m == TestMsg::Tick));
662 }
663
664 #[test]
665 fn every_subscription_uses_stable_id() {
666 let sub1 = Every::<TestMsg>::new(Duration::from_secs(1), || TestMsg::Tick);
667 let sub2 = Every::<TestMsg>::new(Duration::from_secs(1), || TestMsg::Tick);
668 assert_eq!(sub1.id(), sub2.id());
669 }
670
671 #[test]
672 fn every_subscription_different_intervals_different_ids() {
673 let sub1 = Every::<TestMsg>::new(Duration::from_secs(1), || TestMsg::Tick);
674 let sub2 = Every::<TestMsg>::new(Duration::from_secs(2), || TestMsg::Tick);
675 assert_ne!(sub1.id(), sub2.id());
676 }
677
678 #[test]
679 fn subscription_manager_starts_subscriptions() {
680 let mut mgr = SubscriptionManager::<TestMsg>::new();
681 let (sub, event_tx) = channel_subscription(1);
682 let subs: Vec<Box<dyn Subscription<TestMsg>>> = vec![Box::new(sub)];
683
684 mgr.reconcile(subs);
685 event_tx.send(TestMsg::Value(42)).unwrap();
686
687 thread::sleep(Duration::from_millis(20));
689
690 let msgs = mgr.drain_messages();
691 assert_eq!(msgs, vec![TestMsg::Value(42)]);
692 }
693
694 #[test]
695 fn subscription_manager_dedupes_duplicate_ids() {
696 let mut mgr = SubscriptionManager::<TestMsg>::new();
697 let (sub_a, tx_a) = channel_subscription(7);
698 let (sub_b, tx_b) = channel_subscription(7);
699 let subs: Vec<Box<dyn Subscription<TestMsg>>> = vec![Box::new(sub_a), Box::new(sub_b)];
700
701 mgr.reconcile(subs);
702
703 tx_a.send(TestMsg::Value(1)).unwrap();
704 assert!(
705 tx_b.send(TestMsg::Value(2)).is_err(),
706 "Duplicate subscription should be dropped"
707 );
708
709 thread::sleep(Duration::from_millis(20));
710 let msgs = mgr.drain_messages();
711 assert_eq!(msgs, vec![TestMsg::Value(1)]);
712 }
713
714 #[test]
715 fn subscription_manager_stops_removed() {
716 let mut mgr = SubscriptionManager::<TestMsg>::new();
717
718 mgr.reconcile(vec![Box::new(Every::with_id(
720 99,
721 Duration::from_millis(5),
722 || TestMsg::Tick,
723 ))]);
724
725 thread::sleep(Duration::from_millis(20));
726 let msgs_before = mgr.drain_messages();
727 assert!(!msgs_before.is_empty());
728
729 mgr.reconcile(vec![]);
731
732 thread::sleep(Duration::from_millis(20));
734 let _ = mgr.drain_messages();
735
736 thread::sleep(Duration::from_millis(30));
738 let msgs_after = mgr.drain_messages();
739 assert!(
740 msgs_after.is_empty(),
741 "Should stop receiving after reconcile with empty set"
742 );
743 }
744
745 #[test]
746 fn subscription_manager_keeps_unchanged() {
747 let mut mgr = SubscriptionManager::<TestMsg>::new();
748
749 mgr.reconcile(vec![Box::new(Every::with_id(
751 50,
752 Duration::from_millis(10),
753 || TestMsg::Tick,
754 ))]);
755
756 thread::sleep(Duration::from_millis(30));
757 let _ = mgr.drain_messages();
758
759 mgr.reconcile(vec![Box::new(Every::with_id(
761 50,
762 Duration::from_millis(10),
763 || TestMsg::Tick,
764 ))]);
765
766 thread::sleep(Duration::from_millis(30));
767 let msgs = mgr.drain_messages();
768 assert!(!msgs.is_empty(), "Subscription should still be running");
769 }
770
771 #[test]
772 fn subscription_manager_stop_all() {
773 let mut mgr = SubscriptionManager::<TestMsg>::new();
774
775 mgr.reconcile(vec![
776 Box::new(Every::with_id(1, Duration::from_millis(5), || {
777 TestMsg::Value(1)
778 })),
779 Box::new(Every::with_id(2, Duration::from_millis(5), || {
780 TestMsg::Value(2)
781 })),
782 ]);
783
784 thread::sleep(Duration::from_millis(20));
785 mgr.stop_all();
786
787 thread::sleep(Duration::from_millis(20));
788 let _ = mgr.drain_messages();
789 thread::sleep(Duration::from_millis(30));
790 let msgs = mgr.drain_messages();
791 assert!(msgs.is_empty());
792 }
793
794 #[test]
799 fn stop_signal_is_cloneable() {
800 let (signal, trigger) = StopSignal::new();
801 let signal_clone = signal.clone();
802
803 assert!(!signal.is_stopped());
804 assert!(!signal_clone.is_stopped());
805
806 trigger.stop();
807
808 assert!(signal.is_stopped());
809 assert!(signal_clone.is_stopped());
810 }
811
812 #[test]
813 fn stop_signal_wait_wakes_immediately_when_already_stopped() {
814 let (signal, trigger) = StopSignal::new();
815 trigger.stop();
816
817 let start = Instant::now();
819 let stopped = signal.wait_timeout(Duration::from_secs(10));
820 let elapsed = start.elapsed();
821
822 assert!(stopped);
823 assert!(elapsed < Duration::from_millis(100));
824 }
825
826 #[test]
827 fn stop_signal_wait_is_interrupted_by_trigger() {
828 let (signal, trigger) = StopSignal::new();
829
830 let signal_clone = signal.clone();
831 let handle = thread::spawn(move || signal_clone.wait_timeout(Duration::from_secs(10)));
832
833 thread::sleep(Duration::from_millis(20));
835 trigger.stop();
836
837 let stopped = handle.join().unwrap();
838 assert!(stopped);
839 }
840
841 #[test]
842 fn channel_subscription_no_messages_without_events() {
843 let (sub, _event_tx) = channel_subscription(1);
844 let (tx, rx) = mpsc::channel();
845 let (signal, trigger) = StopSignal::new();
846
847 let handle = thread::spawn(move || {
848 sub.run(tx, signal);
849 });
850
851 thread::sleep(Duration::from_millis(10));
852 trigger.stop();
853 handle.join().unwrap();
854
855 let msgs: Vec<_> = rx.try_iter().collect();
856 assert!(msgs.is_empty());
857 }
858
859 #[test]
860 fn channel_subscription_id_is_preserved() {
861 let (sub, _tx) = channel_subscription(42);
862 assert_eq!(sub.id(), 42);
863 }
864
865 #[test]
866 fn channel_subscription_stops_on_disconnected_receiver() {
867 let (sub, event_tx) = channel_subscription(1);
868 let (tx, _rx) = mpsc::channel();
869 let (signal, _trigger) = StopSignal::new();
870
871 drop(event_tx);
872
873 let handle = thread::spawn(move || {
874 sub.run(tx, signal);
875 });
876
877 let result = handle.join();
878 assert!(result.is_ok());
879 }
880
881 #[test]
882 fn every_with_id_preserves_custom_id() {
883 let sub = Every::<TestMsg>::with_id(12345, Duration::from_secs(1), || TestMsg::Tick);
884 assert_eq!(sub.id(), 12345);
885 }
886
887 #[test]
888 fn every_stops_on_disconnected_receiver() {
889 let sub = Every::new(Duration::from_millis(5), || TestMsg::Tick);
890 let (tx, rx) = mpsc::channel();
891 let (signal, _trigger) = StopSignal::new();
892
893 drop(rx);
895
896 let handle = thread::spawn(move || {
898 sub.run(tx, signal);
899 });
900
901 let result = handle.join();
903 assert!(result.is_ok());
904 }
905
906 #[test]
907 fn every_respects_interval() {
908 let sub = Every::with_id(1, Duration::from_millis(50), || TestMsg::Tick);
909 let (tx, rx) = mpsc::channel();
910 let (signal, trigger) = StopSignal::new();
911
912 let start = Instant::now();
913 let handle = thread::spawn(move || {
914 sub.run(tx, signal);
915 });
916
917 thread::sleep(Duration::from_millis(160));
919 trigger.stop();
920 handle.join().unwrap();
921
922 let msgs: Vec<_> = rx.try_iter().collect();
923 let elapsed = start.elapsed();
924
925 assert!(
927 msgs.len() >= 2,
928 "Expected at least 2 ticks, got {}",
929 msgs.len()
930 );
931 assert!(
932 msgs.len() <= 4,
933 "Expected at most 4 ticks, got {}",
934 msgs.len()
935 );
936 assert!(elapsed >= Duration::from_millis(150));
937 }
938
939 #[test]
940 fn subscription_manager_empty_reconcile() {
941 let mut mgr = SubscriptionManager::<TestMsg>::new();
942
943 mgr.reconcile(vec![]);
945 let msgs = mgr.drain_messages();
946 assert!(msgs.is_empty());
947 }
948
949 #[test]
950 fn subscription_manager_drain_messages_returns_all() {
951 let mut mgr = SubscriptionManager::<TestMsg>::new();
952 let (sub, event_tx) = channel_subscription(1);
953 let subs: Vec<Box<dyn Subscription<TestMsg>>> = vec![Box::new(sub)];
954
955 mgr.reconcile(subs);
956 event_tx.send(TestMsg::Value(1)).unwrap();
957 event_tx.send(TestMsg::Value(2)).unwrap();
958 thread::sleep(Duration::from_millis(20));
959
960 let msgs = mgr.drain_messages();
961 assert_eq!(msgs.len(), 2);
962 assert_eq!(msgs[0], TestMsg::Value(1));
963 assert_eq!(msgs[1], TestMsg::Value(2));
964
965 let msgs2 = mgr.drain_messages();
967 assert!(msgs2.is_empty());
968 }
969
970 #[test]
971 fn subscription_manager_replaces_subscription_with_different_id() {
972 let mut mgr = SubscriptionManager::<TestMsg>::new();
973 let (sub1, tx1) = channel_subscription(1);
974
975 mgr.reconcile(vec![Box::new(sub1)]);
977 tx1.send(TestMsg::Value(1)).unwrap();
978 thread::sleep(Duration::from_millis(20));
979 let msgs1 = mgr.drain_messages();
980 assert_eq!(msgs1, vec![TestMsg::Value(1)]);
981
982 let (sub2, tx2) = channel_subscription(2);
984 mgr.reconcile(vec![Box::new(sub2)]);
985 tx2.send(TestMsg::Value(2)).unwrap();
986 thread::sleep(Duration::from_millis(20));
987 let msgs2 = mgr.drain_messages();
988 assert_eq!(msgs2, vec![TestMsg::Value(2)]);
989 }
990
991 #[test]
992 fn subscription_manager_multiple_subscriptions() {
993 let mut mgr = SubscriptionManager::<TestMsg>::new();
994 let (sub1, tx1) = channel_subscription(1);
995 let (sub2, tx2) = channel_subscription(2);
996 let (sub3, tx3) = channel_subscription(3);
997 let subs: Vec<Box<dyn Subscription<TestMsg>>> =
998 vec![Box::new(sub1), Box::new(sub2), Box::new(sub3)];
999
1000 mgr.reconcile(subs);
1001 tx1.send(TestMsg::Value(10)).unwrap();
1002 tx2.send(TestMsg::Value(20)).unwrap();
1003 tx3.send(TestMsg::Value(30)).unwrap();
1004 thread::sleep(Duration::from_millis(30));
1005
1006 let mut msgs = mgr.drain_messages();
1007 msgs.sort_by_key(|m| match m {
1008 TestMsg::Value(v) => *v,
1009 _ => 0,
1010 });
1011
1012 assert_eq!(msgs.len(), 3);
1013 assert_eq!(msgs[0], TestMsg::Value(10));
1014 assert_eq!(msgs[1], TestMsg::Value(20));
1015 assert_eq!(msgs[2], TestMsg::Value(30));
1016 }
1017
1018 #[test]
1019 fn subscription_manager_partial_update() {
1020 let mut mgr = SubscriptionManager::<TestMsg>::new();
1021
1022 mgr.reconcile(vec![
1024 Box::new(Every::with_id(1, Duration::from_millis(10), || {
1025 TestMsg::Value(1)
1026 })),
1027 Box::new(Every::with_id(2, Duration::from_millis(10), || {
1028 TestMsg::Value(2)
1029 })),
1030 Box::new(Every::with_id(3, Duration::from_millis(10), || {
1031 TestMsg::Value(3)
1032 })),
1033 ]);
1034
1035 thread::sleep(Duration::from_millis(30));
1036 let _ = mgr.drain_messages();
1037
1038 mgr.reconcile(vec![
1040 Box::new(Every::with_id(1, Duration::from_millis(10), || {
1041 TestMsg::Value(1)
1042 })),
1043 Box::new(Every::with_id(3, Duration::from_millis(10), || {
1044 TestMsg::Value(3)
1045 })),
1046 ]);
1047
1048 let _ = mgr.drain_messages();
1051
1052 thread::sleep(Duration::from_millis(30));
1054 let msgs = mgr.drain_messages();
1055
1056 let values: Vec<i32> = msgs
1058 .iter()
1059 .filter_map(|m| match m {
1060 TestMsg::Value(v) => Some(*v),
1061 _ => None,
1062 })
1063 .collect();
1064
1065 assert!(
1066 values.contains(&1),
1067 "Should still receive from subscription 1"
1068 );
1069 assert!(
1070 values.contains(&3),
1071 "Should still receive from subscription 3"
1072 );
1073 assert!(
1074 !values.contains(&2),
1075 "Should not receive from stopped subscription 2"
1076 );
1077 }
1078
1079 #[test]
1080 fn subscription_manager_drop_stops_all() {
1081 let (_signal, _) = StopSignal::new();
1082 let flag = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
1083 let flag_clone = flag.clone();
1084
1085 struct FlagSubscription {
1086 id: SubId,
1087 flag: std::sync::Arc<std::sync::atomic::AtomicBool>,
1088 }
1089
1090 impl Subscription<TestMsg> for FlagSubscription {
1091 fn id(&self) -> SubId {
1092 self.id
1093 }
1094
1095 fn run(&self, _sender: mpsc::Sender<TestMsg>, stop: StopSignal) {
1096 while !stop.is_stopped() {
1097 thread::sleep(Duration::from_millis(5));
1098 }
1099 self.flag.store(true, std::sync::atomic::Ordering::SeqCst);
1100 }
1101 }
1102
1103 {
1104 let mut mgr = SubscriptionManager::<TestMsg>::new();
1105 mgr.reconcile(vec![Box::new(FlagSubscription {
1106 id: 1,
1107 flag: flag_clone,
1108 })]);
1109
1110 thread::sleep(Duration::from_millis(20));
1111 }
1113
1114 thread::sleep(Duration::from_millis(50));
1115 assert!(
1116 flag.load(std::sync::atomic::Ordering::SeqCst),
1117 "Subscription should have stopped on drop"
1118 );
1119 }
1120
1121 #[test]
1122 fn running_subscription_stop_joins_thread() {
1123 use std::sync::atomic::{AtomicBool, Ordering};
1124
1125 let completed = std::sync::Arc::new(AtomicBool::new(false));
1126 let completed_clone = completed.clone();
1127
1128 let (signal, trigger) = StopSignal::new();
1129 let (_tx, _rx) = mpsc::channel::<TestMsg>();
1130
1131 let thread = thread::spawn(move || {
1132 while !signal.is_stopped() {
1133 thread::sleep(Duration::from_millis(5));
1134 }
1135 completed_clone.store(true, Ordering::SeqCst);
1136 });
1137
1138 let running = RunningSubscription {
1139 id: 1,
1140 trigger,
1141 thread: Some(thread),
1142 panicked: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
1143 };
1144
1145 running.stop();
1146 assert!(completed.load(Ordering::SeqCst));
1147 }
1148
1149 #[test]
1150 fn running_subscription_stop_times_out_for_uncooperative_thread() {
1151 use std::sync::atomic::{AtomicBool, Ordering};
1152
1153 let completed = std::sync::Arc::new(AtomicBool::new(false));
1154 let completed_clone = completed.clone();
1155
1156 let (_signal, trigger) = StopSignal::new();
1157 let thread = thread::spawn(move || {
1158 thread::sleep(Duration::from_millis(500));
1159 completed_clone.store(true, Ordering::SeqCst);
1160 });
1161
1162 let running = RunningSubscription {
1163 id: 7,
1164 trigger,
1165 thread: Some(thread),
1166 panicked: std::sync::Arc::new(AtomicBool::new(false)),
1167 };
1168
1169 let start = Instant::now();
1170 running.stop();
1171 assert!(
1172 start.elapsed() < Duration::from_millis(400),
1173 "stop() should not block behind an uncooperative subscription thread"
1174 );
1175
1176 thread::sleep(Duration::from_millis(550));
1177 assert!(completed.load(Ordering::SeqCst));
1178 }
1179
1180 #[test]
1181 fn every_id_stable_across_instances() {
1182 let sub1 = Every::<TestMsg>::new(Duration::from_millis(100), || TestMsg::Tick);
1184 let sub2 = Every::<TestMsg>::new(Duration::from_millis(100), || TestMsg::Tick);
1185 let sub3 = Every::<TestMsg>::new(Duration::from_millis(100), || TestMsg::Value(1));
1186
1187 assert_eq!(sub1.id(), sub2.id());
1188 assert_eq!(sub2.id(), sub3.id()); }
1190
1191 #[test]
1192 fn drain_messages_preserves_order() {
1193 let mut mgr = SubscriptionManager::<TestMsg>::new();
1194
1195 struct OrderedSubscription {
1197 values: Vec<i32>,
1198 }
1199
1200 impl Subscription<TestMsg> for OrderedSubscription {
1201 fn id(&self) -> SubId {
1202 999
1203 }
1204
1205 fn run(&self, sender: mpsc::Sender<TestMsg>, _stop: StopSignal) {
1206 for v in &self.values {
1207 let _ = sender.send(TestMsg::Value(*v));
1208 thread::sleep(Duration::from_millis(1));
1209 }
1210 }
1211 }
1212
1213 mgr.reconcile(vec![Box::new(OrderedSubscription {
1214 values: vec![1, 2, 3, 4, 5],
1215 })]);
1216
1217 thread::sleep(Duration::from_millis(30));
1218 let msgs = mgr.drain_messages();
1219
1220 let values: Vec<i32> = msgs
1221 .iter()
1222 .filter_map(|m| match m {
1223 TestMsg::Value(v) => Some(*v),
1224 _ => None,
1225 })
1226 .collect();
1227
1228 assert_eq!(values, vec![1, 2, 3, 4, 5]);
1229 }
1230
1231 #[test]
1232 fn subscription_manager_new_is_empty() {
1233 let mgr = SubscriptionManager::<TestMsg>::new();
1234 let msgs = mgr.drain_messages();
1235 assert!(msgs.is_empty());
1236 }
1237
1238 #[test]
1250 fn contract_stop_signal_resilient_to_thread_panics() {
1251 let (signal, trigger) = StopSignal::new();
1252 let signal_clone = signal.clone();
1253
1254 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1256 assert!(!signal_clone.is_stopped());
1257 panic!("intentional panic while holding signal clone");
1258 }));
1259 assert!(result.is_err());
1260
1261 assert!(
1263 !signal.is_stopped(),
1264 "signal should still report not-stopped"
1265 );
1266 trigger.stop();
1267 assert!(
1268 signal.is_stopped(),
1269 "signal should report stopped after trigger"
1270 );
1271 assert!(
1272 signal.wait_timeout(Duration::from_millis(10)),
1273 "wait_timeout should return true when stopped"
1274 );
1275 }
1276
1277 #[test]
1280 fn contract_stop_signal_exposes_cancellation_token() {
1281 let (signal, trigger) = StopSignal::new();
1282 let token = signal.cancellation_token();
1283 assert!(!token.is_cancelled(), "token should start uncancelled");
1284 trigger.stop();
1285 assert!(token.is_cancelled(), "token should be cancelled after stop");
1286 }
1287
1288 #[test]
1292 fn contract_stop_all_bounded_time_with_uncooperative_subscriptions() {
1293 let mut mgr = SubscriptionManager::<TestMsg>::new();
1294
1295 struct UncooperativeSub {
1297 id: SubId,
1298 }
1299
1300 impl Subscription<TestMsg> for UncooperativeSub {
1301 fn id(&self) -> SubId {
1302 self.id
1303 }
1304
1305 fn run(&self, _sender: mpsc::Sender<TestMsg>, _stop: StopSignal) {
1306 thread::sleep(Duration::from_secs(5));
1308 }
1309 }
1310
1311 mgr.reconcile(vec![
1312 Box::new(UncooperativeSub { id: 100 }),
1313 Box::new(UncooperativeSub { id: 200 }),
1314 ]);
1315
1316 thread::sleep(Duration::from_millis(20)); let start = Instant::now();
1319 mgr.stop_all();
1320 let elapsed = start.elapsed();
1321
1322 assert!(
1324 elapsed < Duration::from_millis(800),
1325 "stop_all took {elapsed:?}, expected < 800ms for 2 uncooperative subscriptions"
1326 );
1327 }
1328
1329 #[test]
1332 fn contract_reconcile_deduplicates_by_id_not_identity() {
1333 let mut mgr = SubscriptionManager::<TestMsg>::new();
1334 let counter = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
1335
1336 struct CountingSub {
1337 id: SubId,
1338 counter: std::sync::Arc<std::sync::atomic::AtomicUsize>,
1339 }
1340
1341 impl Subscription<TestMsg> for CountingSub {
1342 fn id(&self) -> SubId {
1343 self.id
1344 }
1345
1346 fn run(&self, _sender: mpsc::Sender<TestMsg>, stop: StopSignal) {
1347 self.counter
1348 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1349 while !stop.is_stopped() {
1350 thread::sleep(Duration::from_millis(5));
1351 }
1352 }
1353 }
1354
1355 mgr.reconcile(vec![Box::new(CountingSub {
1357 id: 42,
1358 counter: counter.clone(),
1359 })]);
1360 thread::sleep(Duration::from_millis(20));
1361 assert_eq!(
1362 counter.load(std::sync::atomic::Ordering::SeqCst),
1363 1,
1364 "first reconcile should start exactly 1 thread"
1365 );
1366
1367 mgr.reconcile(vec![Box::new(CountingSub {
1369 id: 42,
1370 counter: counter.clone(),
1371 })]);
1372 thread::sleep(Duration::from_millis(20));
1373 assert_eq!(
1374 counter.load(std::sync::atomic::Ordering::SeqCst),
1375 1,
1376 "second reconcile with same ID must not start another thread"
1377 );
1378
1379 mgr.stop_all();
1380 }
1381
1382 #[test]
1386 fn contract_buffered_messages_available_after_subscription_stopped() {
1387 let mut mgr = SubscriptionManager::<TestMsg>::new();
1388
1389 struct BurstSub;
1390
1391 impl Subscription<TestMsg> for BurstSub {
1392 fn id(&self) -> SubId {
1393 77
1394 }
1395
1396 fn run(&self, sender: mpsc::Sender<TestMsg>, stop: StopSignal) {
1397 for i in 0..10 {
1399 let _ = sender.send(TestMsg::Value(i));
1400 }
1401 while !stop.is_stopped() {
1403 thread::sleep(Duration::from_millis(5));
1404 }
1405 }
1406 }
1407
1408 mgr.reconcile(vec![Box::new(BurstSub)]);
1409 thread::sleep(Duration::from_millis(30));
1410
1411 mgr.reconcile(vec![]);
1413
1414 let msgs = mgr.drain_messages();
1416 let values: Vec<i32> = msgs
1417 .iter()
1418 .filter_map(|m| match m {
1419 TestMsg::Value(v) => Some(*v),
1420 _ => None,
1421 })
1422 .collect();
1423
1424 assert!(
1425 values.len() >= 5,
1426 "Expected at least 5 buffered messages after stop, got {}",
1427 values.len()
1428 );
1429 }
1430
1431 #[test]
1434 fn contract_active_count_tracks_running_subscriptions() {
1435 let mut mgr = SubscriptionManager::<TestMsg>::new();
1436
1437 assert_eq!(mgr.active_count(), 0, "empty manager");
1438
1439 mgr.reconcile(vec![
1440 Box::new(Every::with_id(1, Duration::from_millis(50), || {
1441 TestMsg::Tick
1442 })),
1443 Box::new(Every::with_id(2, Duration::from_millis(50), || {
1444 TestMsg::Tick
1445 })),
1446 ]);
1447 assert_eq!(mgr.active_count(), 2, "after starting 2");
1448
1449 mgr.reconcile(vec![Box::new(Every::with_id(
1450 1,
1451 Duration::from_millis(50),
1452 || TestMsg::Tick,
1453 ))]);
1454 assert_eq!(mgr.active_count(), 1, "after removing 1");
1455
1456 mgr.stop_all();
1457 assert_eq!(mgr.active_count(), 0, "after stop_all");
1458 }
1459
1460 #[test]
1464 fn contract_every_id_derived_from_interval_only() {
1465 let sub_a = Every::<TestMsg>::new(Duration::from_millis(100), || TestMsg::Tick);
1466 let sub_b = Every::<TestMsg>::new(Duration::from_millis(100), || TestMsg::Value(999));
1467 assert_eq!(
1468 sub_a.id(),
1469 sub_b.id(),
1470 "Every ID must depend only on interval, not message factory"
1471 );
1472
1473 let sub_c = Every::<TestMsg>::new(Duration::from_millis(200), || TestMsg::Tick);
1474 assert_ne!(
1475 sub_a.id(),
1476 sub_c.id(),
1477 "Different intervals must produce different IDs"
1478 );
1479 }
1480
1481 #[test]
1484 fn contract_every_id_formula_is_stable() {
1485 let interval = Duration::from_millis(100);
1486 let expected_id = interval.as_nanos() as u64 ^ 0x5449_434B;
1487 let sub = Every::<TestMsg>::new(interval, || TestMsg::Tick);
1488 assert_eq!(
1489 sub.id(),
1490 expected_id,
1491 "Every ID formula must be: interval.as_nanos() as u64 ^ 0x5449_434B"
1492 );
1493 }
1494
1495 #[test]
1498 fn contract_drop_triggers_stop_all() {
1499 use std::sync::atomic::{AtomicUsize, Ordering};
1500
1501 let stop_count = std::sync::Arc::new(AtomicUsize::new(0));
1502
1503 struct StopCountingSub {
1504 id: SubId,
1505 counter: std::sync::Arc<AtomicUsize>,
1506 }
1507
1508 impl Subscription<TestMsg> for StopCountingSub {
1509 fn id(&self) -> SubId {
1510 self.id
1511 }
1512
1513 fn run(&self, _sender: mpsc::Sender<TestMsg>, stop: StopSignal) {
1514 while !stop.is_stopped() {
1515 thread::sleep(Duration::from_millis(5));
1516 }
1517 self.counter.fetch_add(1, Ordering::SeqCst);
1518 }
1519 }
1520
1521 {
1522 let mut mgr = SubscriptionManager::<TestMsg>::new();
1523 mgr.reconcile(vec![
1524 Box::new(StopCountingSub {
1525 id: 1,
1526 counter: stop_count.clone(),
1527 }),
1528 Box::new(StopCountingSub {
1529 id: 2,
1530 counter: stop_count.clone(),
1531 }),
1532 Box::new(StopCountingSub {
1533 id: 3,
1534 counter: stop_count.clone(),
1535 }),
1536 ]);
1537 thread::sleep(Duration::from_millis(20));
1538 }
1540
1541 thread::sleep(Duration::from_millis(400));
1543 assert_eq!(
1544 stop_count.load(std::sync::atomic::Ordering::SeqCst),
1545 3,
1546 "all 3 subscription threads must have observed stop signal on drop"
1547 );
1548 }
1549
1550 #[test]
1553 fn contract_stop_join_timeout_is_250ms() {
1554 assert_eq!(
1555 SUBSCRIPTION_STOP_JOIN_TIMEOUT,
1556 Duration::from_millis(250),
1557 "join timeout must be 250ms"
1558 );
1559 assert_eq!(
1560 SUBSCRIPTION_STOP_JOIN_POLL,
1561 Duration::from_millis(1),
1562 "join poll interval must be 1ms (bd-1f2aw)"
1563 );
1564 }
1565
1566 #[test]
1576 fn lifecycle_panic_in_subscription_is_caught() {
1577 use std::sync::atomic::Ordering;
1578
1579 let mut mgr = SubscriptionManager::<TestMsg>::new();
1580
1581 struct PanickingSub;
1582
1583 impl Subscription<TestMsg> for PanickingSub {
1584 fn id(&self) -> SubId {
1585 0xDEAD
1586 }
1587
1588 fn run(&self, _sender: mpsc::Sender<TestMsg>, _stop: StopSignal) {
1589 panic!("intentional test panic in subscription");
1590 }
1591 }
1592
1593 mgr.reconcile(vec![Box::new(PanickingSub)]);
1594
1595 thread::sleep(Duration::from_millis(50));
1597
1598 assert_eq!(
1600 mgr.active_count(),
1601 1,
1602 "panicked sub still tracked as active"
1603 );
1604
1605 assert!(
1607 mgr.active[0].panicked.load(Ordering::Acquire),
1608 "panicked flag should be set after subscription panic"
1609 );
1610
1611 mgr.stop_all();
1613 assert_eq!(mgr.active_count(), 0);
1614 }
1615
1616 #[test]
1619 fn lifecycle_panic_does_not_affect_sibling_subscriptions() {
1620 let mut mgr = SubscriptionManager::<TestMsg>::new();
1621
1622 struct PanickingSub;
1623 impl Subscription<TestMsg> for PanickingSub {
1624 fn id(&self) -> SubId {
1625 0xBAD
1626 }
1627 fn run(&self, _sender: mpsc::Sender<TestMsg>, _stop: StopSignal) {
1628 panic!("boom");
1629 }
1630 }
1631
1632 mgr.reconcile(vec![
1633 Box::new(PanickingSub),
1634 Box::new(Every::with_id(42, Duration::from_millis(10), || {
1635 TestMsg::Tick
1636 })),
1637 ]);
1638
1639 thread::sleep(Duration::from_millis(100));
1642
1643 let msgs = mgr.drain_messages();
1644 assert!(
1645 !msgs.is_empty(),
1646 "sibling subscription should still deliver messages after a panic in another sub"
1647 );
1648
1649 mgr.stop_all();
1650 }
1651
1652 #[test]
1655 fn lifecycle_stop_all_parallel_shutdown() {
1656 use std::sync::atomic::{AtomicUsize, Ordering};
1657
1658 let stop_count = std::sync::Arc::new(AtomicUsize::new(0));
1659 let sub_count = 4;
1660
1661 struct SlowStopSub {
1662 id: SubId,
1663 counter: std::sync::Arc<AtomicUsize>,
1664 }
1665
1666 impl Subscription<TestMsg> for SlowStopSub {
1667 fn id(&self) -> SubId {
1668 self.id
1669 }
1670
1671 fn run(&self, _sender: mpsc::Sender<TestMsg>, stop: StopSignal) {
1672 while !stop.is_stopped() {
1674 thread::sleep(Duration::from_millis(5));
1675 }
1676 thread::sleep(Duration::from_millis(50));
1677 self.counter.fetch_add(1, Ordering::SeqCst);
1678 }
1679 }
1680
1681 let mut mgr = SubscriptionManager::<TestMsg>::new();
1682 let subs: Vec<Box<dyn Subscription<TestMsg>>> = (0..sub_count)
1683 .map(|i| -> Box<dyn Subscription<TestMsg>> {
1684 Box::new(SlowStopSub {
1685 id: 1000 + i,
1686 counter: stop_count.clone(),
1687 })
1688 })
1689 .collect();
1690
1691 mgr.reconcile(subs);
1692 thread::sleep(Duration::from_millis(20));
1693
1694 let start = Instant::now();
1695 mgr.stop_all();
1696 let elapsed = start.elapsed();
1697
1698 assert!(
1703 elapsed < Duration::from_millis(150),
1704 "parallel stop_all took {elapsed:?}, expected < 150ms \
1705 (sequential would be ~{expected_sequential}ms)",
1706 expected_sequential = sub_count * 50
1707 );
1708
1709 thread::sleep(Duration::from_millis(20));
1711 assert_eq!(
1712 stop_count.load(Ordering::SeqCst),
1713 sub_count as usize,
1714 "all subscriptions should have completed their cleanup"
1715 );
1716 }
1717
1718 #[test]
1721 fn lifecycle_reconcile_removal_uses_parallel_stop() {
1722 use std::sync::atomic::{AtomicUsize, Ordering};
1723
1724 let stop_count = std::sync::Arc::new(AtomicUsize::new(0));
1725
1726 struct SlowStopSub {
1727 id: SubId,
1728 counter: std::sync::Arc<AtomicUsize>,
1729 }
1730
1731 impl Subscription<TestMsg> for SlowStopSub {
1732 fn id(&self) -> SubId {
1733 self.id
1734 }
1735
1736 fn run(&self, _sender: mpsc::Sender<TestMsg>, stop: StopSignal) {
1737 while !stop.is_stopped() {
1738 thread::sleep(Duration::from_millis(5));
1739 }
1740 thread::sleep(Duration::from_millis(40));
1741 self.counter.fetch_add(1, Ordering::SeqCst);
1742 }
1743 }
1744
1745 let mut mgr = SubscriptionManager::<TestMsg>::new();
1746 mgr.reconcile(vec![
1747 Box::new(SlowStopSub {
1748 id: 2000,
1749 counter: stop_count.clone(),
1750 }),
1751 Box::new(SlowStopSub {
1752 id: 2001,
1753 counter: stop_count.clone(),
1754 }),
1755 Box::new(SlowStopSub {
1756 id: 2002,
1757 counter: stop_count.clone(),
1758 }),
1759 ]);
1760 thread::sleep(Duration::from_millis(20));
1761
1762 let start = Instant::now();
1764 mgr.reconcile(vec![]);
1765 let elapsed = start.elapsed();
1766
1767 assert!(
1769 elapsed < Duration::from_millis(100),
1770 "reconcile removal took {elapsed:?}, expected < 100ms with parallel stop"
1771 );
1772
1773 thread::sleep(Duration::from_millis(20));
1774 assert_eq!(stop_count.load(Ordering::SeqCst), 3);
1775 }
1776
1777 #[test]
1779 fn lifecycle_has_panicked_tracks_state() {
1780 let panicked = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
1781 let panicked_flag = panicked.clone();
1782
1783 let (signal, trigger) = StopSignal::new();
1784 let thread = thread::spawn(move || {
1785 signal.wait_timeout(Duration::from_secs(10));
1786 });
1787
1788 let running = RunningSubscription {
1789 id: 999,
1790 trigger,
1791 thread: Some(thread),
1792 panicked,
1793 };
1794
1795 assert!(!running.has_panicked(), "should not be panicked initially");
1796
1797 panicked_flag.store(true, std::sync::atomic::Ordering::Release);
1799 assert!(running.has_panicked(), "should reflect panicked state");
1800
1801 running.stop();
1802 }
1803
1804 #[test]
1807 fn lifecycle_signal_then_join_works() {
1808 use std::sync::atomic::{AtomicBool, Ordering};
1809
1810 let completed = std::sync::Arc::new(AtomicBool::new(false));
1811 let completed_clone = completed.clone();
1812
1813 let (signal, trigger) = StopSignal::new();
1814 let thread = thread::spawn(move || {
1815 while !signal.is_stopped() {
1816 thread::sleep(Duration::from_millis(5));
1817 }
1818 completed_clone.store(true, Ordering::SeqCst);
1819 });
1820
1821 let running = RunningSubscription {
1822 id: 888,
1823 trigger,
1824 thread: Some(thread),
1825 panicked: std::sync::Arc::new(AtomicBool::new(false)),
1826 };
1827
1828 running.signal_stop();
1829 let leftover = running.join_bounded();
1830 assert!(
1831 leftover.is_none(),
1832 "cooperative thread should join within timeout"
1833 );
1834 assert!(
1835 completed.load(Ordering::SeqCst),
1836 "thread should have completed"
1837 );
1838 }
1839
1840 #[test]
1842 fn lifecycle_join_bounded_returns_handle_for_uncooperative() {
1843 use std::sync::atomic::AtomicBool;
1844
1845 let (_signal, trigger) = StopSignal::new();
1846 let thread = thread::spawn(move || {
1847 thread::sleep(Duration::from_millis(500));
1848 });
1849
1850 let running = RunningSubscription {
1851 id: 777,
1852 trigger,
1853 thread: Some(thread),
1854 panicked: std::sync::Arc::new(AtomicBool::new(false)),
1855 };
1856
1857 running.signal_stop();
1858 let start = Instant::now();
1859 let leftover = running.join_bounded();
1860 let elapsed = start.elapsed();
1861
1862 assert!(
1863 leftover.is_some(),
1864 "uncooperative thread should not join within timeout"
1865 );
1866 assert!(
1867 elapsed < Duration::from_millis(400),
1868 "join_bounded should respect the 250ms timeout, took {elapsed:?}"
1869 );
1870 }
1871
1872 #[test]
1875 fn lifecycle_restart_after_stop() {
1876 let mut mgr = SubscriptionManager::<TestMsg>::new();
1877
1878 mgr.reconcile(vec![Box::new(Every::with_id(
1880 300,
1881 Duration::from_millis(10),
1882 || TestMsg::Tick,
1883 ))]);
1884 thread::sleep(Duration::from_millis(30));
1885 let msgs = mgr.drain_messages();
1886 assert!(!msgs.is_empty(), "should receive ticks");
1887
1888 mgr.reconcile(vec![]);
1890 thread::sleep(Duration::from_millis(20));
1891 let _ = mgr.drain_messages();
1892 thread::sleep(Duration::from_millis(30));
1893 let msgs = mgr.drain_messages();
1894 assert!(msgs.is_empty(), "should stop receiving after removal");
1895
1896 mgr.reconcile(vec![Box::new(Every::with_id(
1898 300,
1899 Duration::from_millis(10),
1900 || TestMsg::Value(99),
1901 ))]);
1902 thread::sleep(Duration::from_millis(30));
1903 let msgs = mgr.drain_messages();
1904 assert!(
1905 !msgs.is_empty(),
1906 "should receive messages again after restart"
1907 );
1908 assert!(
1909 msgs.iter().any(|m| matches!(m, TestMsg::Value(99))),
1910 "restarted sub should use the new message factory"
1911 );
1912
1913 mgr.stop_all();
1914 }
1915
1916 #[test]
1924 fn lifecycle_non_interference_with_manager_state() {
1925 use std::sync::atomic::{AtomicUsize, Ordering};
1926
1927 let msg_count = std::sync::Arc::new(AtomicUsize::new(0));
1928
1929 struct CountingSub {
1930 id: SubId,
1931 counter: std::sync::Arc<AtomicUsize>,
1932 }
1933
1934 impl Subscription<TestMsg> for CountingSub {
1935 fn id(&self) -> SubId {
1936 self.id
1937 }
1938
1939 fn run(&self, sender: mpsc::Sender<TestMsg>, stop: StopSignal) {
1940 while !stop.is_stopped() {
1941 if sender.send(TestMsg::Tick).is_err() {
1942 break;
1943 }
1944 self.counter.fetch_add(1, Ordering::SeqCst);
1945 thread::sleep(Duration::from_millis(5));
1946 }
1947 }
1948 }
1949
1950 let mut mgr = SubscriptionManager::<TestMsg>::new();
1951
1952 mgr.reconcile(vec![
1954 Box::new(CountingSub {
1955 id: 400,
1956 counter: msg_count.clone(),
1957 }),
1958 Box::new(CountingSub {
1959 id: 401,
1960 counter: msg_count.clone(),
1961 }),
1962 ]);
1963
1964 thread::sleep(Duration::from_millis(50));
1965
1966 assert_eq!(mgr.active_count(), 2);
1968 let drained = mgr.drain_messages();
1969 let sent_count = msg_count.load(Ordering::SeqCst);
1970 assert!(sent_count > 0, "subscriptions should have sent messages");
1971 assert!(
1972 drained.len() <= sent_count,
1973 "drained {} but only {} sent",
1974 drained.len(),
1975 sent_count
1976 );
1977
1978 mgr.stop_all();
1980 assert_eq!(mgr.active_count(), 0);
1981
1982 let remaining = mgr.drain_messages();
1984 let total_drained = drained.len() + remaining.len();
1985 let final_sent = msg_count.load(Ordering::SeqCst);
1986 assert!(
1987 total_drained <= final_sent,
1988 "total drained ({total_drained}) must not exceed total sent ({final_sent})"
1989 );
1990 }
1991
1992 #[test]
1996 fn lifecycle_shutdown_signal_ordering() {
1997 use std::sync::atomic::{AtomicU64, Ordering};
1998
1999 let signal_times =
2000 std::sync::Arc::new([AtomicU64::new(0), AtomicU64::new(0), AtomicU64::new(0)]);
2001 let epoch = Instant::now();
2002
2003 struct TimingStopSub {
2004 id: SubId,
2005 index: usize,
2006 signal_times: std::sync::Arc<[AtomicU64; 3]>,
2007 epoch: Instant,
2008 }
2009
2010 impl Subscription<TestMsg> for TimingStopSub {
2011 fn id(&self) -> SubId {
2012 self.id
2013 }
2014
2015 fn run(&self, _sender: mpsc::Sender<TestMsg>, stop: StopSignal) {
2016 while !stop.is_stopped() {
2017 thread::sleep(Duration::from_millis(1));
2018 }
2019 let elapsed_us = self.epoch.elapsed().as_micros() as u64;
2020 self.signal_times[self.index].store(elapsed_us, Ordering::SeqCst);
2021 }
2022 }
2023
2024 let mut mgr = SubscriptionManager::<TestMsg>::new();
2025 mgr.reconcile(vec![
2026 Box::new(TimingStopSub {
2027 id: 500,
2028 index: 0,
2029 signal_times: signal_times.clone(),
2030 epoch,
2031 }),
2032 Box::new(TimingStopSub {
2033 id: 501,
2034 index: 1,
2035 signal_times: signal_times.clone(),
2036 epoch,
2037 }),
2038 Box::new(TimingStopSub {
2039 id: 502,
2040 index: 2,
2041 signal_times: signal_times.clone(),
2042 epoch,
2043 }),
2044 ]);
2045 thread::sleep(Duration::from_millis(20));
2046
2047 mgr.stop_all();
2048
2049 let t0 = signal_times[0].load(Ordering::SeqCst);
2053 let t1 = signal_times[1].load(Ordering::SeqCst);
2054 let t2 = signal_times[2].load(Ordering::SeqCst);
2055
2056 assert!(
2057 t0 > 0 && t1 > 0 && t2 > 0,
2058 "all subs should have recorded stop time"
2059 );
2060
2061 let max_t = t0.max(t1).max(t2);
2062 let min_t = t0.min(t1).min(t2);
2063 let spread_us = max_t - min_t;
2064
2065 assert!(
2066 spread_us < 10_000, "stop signal spread should be < 10ms for parallel signaling, got {spread_us}us \
2068 (t0={t0}, t1={t1}, t2={t2})"
2069 );
2070 }
2071}