Skip to content

Commit 7cfe50b

Browse files
committed
Add tests. Fix async problems.
1 parent 40ef803 commit 7cfe50b

7 files changed

Lines changed: 221 additions & 73 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ All notable changes to this project are documented here. The format loosely foll
3939
- **`WillAsync` / `CountDownLatch` woke only one awaiter** — the single `Option<Waker>` slot dropped all but the last-registered waker, hanging concurrent awaiters; now stores `Vec<Waker>` and wakes all (draining before waking to stay reentrancy-safe). `WillAsync::start()` now also wakes wakers registered before `start()`, so poll-before-start no longer hangs.
4040
- **`SubscriptionFunc` buffered every value forever (memory leak)** — under `for_futures`, `on_next` unconditionally pushed each delivered value into an internal `cached` stream, but that stream was open from construction (`LinkedListAsync::new()` starts `alive=true`). A callback-only subscription (the common `subscribe_fn` case) has no stream consumer, so the buffer grew without bound. The `cached` stream now starts **closed** and only begins buffering once `as_stream()` opens it — matching the documented open/close intent. Streaming (`subscribe_as_stream`) is unaffected. Regression-guarded by `test_common_subscription_func_no_buffer_until_stream_opened`.
4141
- **`BlockingQueue::stop` doc/dead-code mismatch** — the method and module docs claimed it "drops the sender side" to close the queue, but the code did `let sender = …lock().unwrap(); drop(sender);` which dropped the `MutexGuard`, not the `Sender` (which lives behind `Arc<Mutex<..>>` and cannot be dropped there). Removed the misleading no-op and corrected both docs to state the real mechanism: `stop()` flips the `alive` flag (gating later `offer`/`take`), and a consumer already blocked in `take()` stays blocked until its own `timeout`. No behavior change (verified by `test_sync_blocking_queue_is_alive_and_stop`).
42+
- **`HandlerThreadInner::post` accepted work after stop** — `Handler::stop` promised "stop accepting new jobs," but `post()` always enqueued into the inner `BlockingQueue`. A post after stop could leak forever into an un-drained queue or wake a worker blocked in `take()` and execute one more job after stop. `post()` now rejects only the stopped state (`started && !alive`), preserving pre-start queueing. Regression-guarded by `test_handler_inner_post_after_stop_is_rejected` and `test_handler_post_before_start_is_preserved`.
43+
- **`ActorAsync::stop` left mailbox handles accepting sends** — stopping an actor cleared the actor alive flag but left the mailbox `BlockingQueue` alive, so `HandleAsync::send` after stop still queued messages that would never be processed. `stop()` now also stops the mailbox queue so later sends are rejected, while still not interrupting a thread already blocked in `take()` (shutdown redesign remains deferred). Regression-guarded by `test_actor_handle_send_after_stop_is_rejected`.
44+
- **`CountDownLatch::wait` released only one blocking waiter** — `countdown()` used `Condvar::notify_one()`, so when the count reached zero only one waiting thread was guaranteed to wake. It now uses `notify_all()` at zero, matching Java `CountDownLatch` semantics and the existing async multi-waker behavior. Regression-guarded by `test_sync_countdownlatch_releases_all_waiters`.
45+
- **Reentrant waker deadlock risks** — `WillAsync`, `CountDownLatch`, and `LinkedListAsync` now release state/waker locks before invoking `wake()`/`wake_all_wakers()`/`notify_all()`, so synchronous re-polling wakers cannot block on the same non-reentrant mutexes during wakeup. Verified by existing async stream/future tests.
46+
- **`Cor::stop` vestigial no-op** — `stop()` ran `drop(self.op_ch_sender.lock().unwrap());`, which (like the old `BlockingQueue::stop`) dropped the `MutexGuard`, not the shared `Sender`, so it never closed the op channel. Removed the no-op and clarified that cooperative stop is delivered solely by the `alive` flag (a `yield_from` to a stopped `Cor` returns `None` because `receive()` early-returns). Behavior-preserving; also removes one lock held under `started_alive`. Verified by `test_cor_*` (pure + `test_runtime`).
47+
- **`Publisher::delete_observer` closed streams for non-members and duplicates too early** — stream closure happened before confirming the subscription belonged to that publisher, so deleting a non-member could close another publisher's stream. With duplicate registrations, removing one occurrence closed the shared stream while another observer still remained. `delete_observer` now closes only after removing a matching observer, and only when no matching observer remains in that publisher. Regression-guarded by `test_publisher_delete_non_member_does_not_close_subscription_stream` and `test_publisher_delete_duplicate_keeps_stream_open_until_last_observer`.
4248

4349
---
4450

‎src/actor.rs‎

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
//!
88
//! # Shutdown behavior
99
//!
10-
//! [`ActorAsync::stop`] sets the alive flag only. The consumer thread is **not**
11-
//! joined; commented-out [`join`](std::thread::JoinHandle::join) code shows
12-
//! join-based shutdown was deferred. A thread blocked in [`take`](crate::sync::BlockingQueue::take)
13-
//! is not interrupted. README/examples that use `thread::sleep` after `stop` are
14-
//! demo synchronization only—not a supported shutdown contract.
10+
//! [`ActorAsync::stop`] clears the actor alive flag and stops the mailbox queue
11+
//! from accepting later sends. The consumer thread is **not** joined;
12+
//! commented-out [`join`](std::thread::JoinHandle::join) code shows join-based
13+
//! shutdown was deferred. A thread already blocked in
14+
//! [`take`](crate::sync::BlockingQueue::take) is not interrupted.
1515
1616
use std::collections::HashMap;
1717
use std::sync::{
@@ -176,7 +176,8 @@ where
176176
alive.load(Ordering::SeqCst)
177177
}
178178

179-
/// Clears alive flag only; does not join the mailbox thread (see module docs).
179+
/// Clears the actor alive flag and stops the mailbox accepting later sends;
180+
/// does not join the mailbox thread (see module docs).
180181
pub fn stop(&mut self) {
181182
{
182183
let started_alive = self.started_alive.lock().unwrap();
@@ -190,6 +191,9 @@ where
190191
}
191192
alive.store(false, Ordering::SeqCst);
192193
}
194+
195+
self.queue.stop();
196+
193197
// NOTE: Kill thread <- OS depending
194198
// let mut join_handle = self.join_handle.lock().unwrap();
195199
// join_handle
@@ -591,6 +595,27 @@ fn test_actor_handle_id_and_no_parent() {
591595
assert_eq!(true, actor.get_handle_child("missing").is_none());
592596
}
593597

598+
#[test]
599+
fn test_actor_handle_send_after_stop_is_rejected() {
600+
let mut actor = ActorAsync::new(
601+
|_this: &mut ActorAsync<i32, i32>, _msg: i32, _ctx: &mut HashMap<String, i32>| {},
602+
);
603+
let mut handle = actor.get_handle();
604+
605+
{
606+
let started_alive = actor.started_alive.lock().unwrap();
607+
let (started, alive) = &*started_alive;
608+
started.store(true, Ordering::SeqCst);
609+
alive.store(true, Ordering::SeqCst);
610+
}
611+
612+
actor.stop();
613+
handle.send(7);
614+
615+
let mut queue = handle.queue.clone();
616+
assert_eq!(None, queue.poll());
617+
}
618+
594619
#[test]
595620
fn test_actor_context_accumulates_state() {
596621
use super::common::LinkedListAsync;

‎src/common.rs‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,9 @@ impl<T> LinkedListAsync<T> {
178178
if alive.load(Ordering::SeqCst) {
179179
self.inner.lock().unwrap().push_back(input);
180180
}
181-
182-
self.wake();
183181
}
182+
183+
self.wake();
184184
}
185185

186186
#[cfg(not(feature = "for_futures"))]
@@ -197,8 +197,8 @@ impl<T> LinkedListAsync<T> {
197197
#[cfg(feature = "for_futures")]
198198
#[inline]
199199
fn wake(&self) {
200-
let mut waker = self.waker.lock().unwrap();
201-
if let Some(waker) = waker.take() {
200+
let waker = { self.waker.lock().unwrap().take() };
201+
if let Some(waker) = waker {
202202
waker.wake();
203203
}
204204
}
@@ -214,9 +214,9 @@ impl<T> LinkedListAsync<T> {
214214
{
215215
let alive = self.alive.lock().unwrap();
216216
alive.store(false, Ordering::SeqCst);
217-
218-
self.wake()
219217
}
218+
219+
self.wake();
220220
}
221221
}
222222

‎src/cor.rs‎

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -356,26 +356,24 @@ impl<RETURN: Send + Sync + 'static, RECEIVE: Send + Sync + 'static> Cor<RETURN,
356356
alive.load(Ordering::SeqCst)
357357
}
358358

359-
/// Cooperative stop: `yield_from` targeting this coroutine returns `None`.
359+
/// Cooperative stop: clears the `alive` flag so a `yield_from` targeting this
360+
/// coroutine returns `None` (its `receive` early-returns without queueing).
360361
///
361-
/// Does not join an async worker thread; shutdown redesign is deferred.
362+
/// Does not join an async worker thread; shutdown redesign is deferred. The
363+
/// op channel is not closed here: the shared `Sender` lives behind
364+
/// `Arc<Mutex<..>>` and cannot be dropped from one handle, so cooperative
365+
/// stop is delivered by the `alive` flag alone.
362366
pub fn stop(&mut self) {
363-
{
364-
let started_alive = self.started_alive.lock().unwrap();
365-
let (started, alive) = &*started_alive;
366-
367-
if !started.load(Ordering::SeqCst) {
368-
return;
369-
}
370-
if !alive.load(Ordering::SeqCst) {
371-
return;
372-
}
373-
alive.store(false, Ordering::SeqCst);
367+
let started_alive = self.started_alive.lock().unwrap();
368+
let (started, alive) = &*started_alive;
374369

375-
{
376-
drop(self.op_ch_sender.lock().unwrap());
377-
}
370+
if !started.load(Ordering::SeqCst) {
371+
return;
372+
}
373+
if !alive.load(Ordering::SeqCst) {
374+
return;
378375
}
376+
alive.store(false, Ordering::SeqCst);
379377
}
380378

381379
fn receive(

‎src/handler.rs‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pub trait Handler: Send + Sync + 'static {
3030
/// `true` while the worker loop may still execute jobs.
3131
fn is_alive(&mut self) -> bool;
3232

33-
/// Starts (or resumes) the worker loop.
33+
/// Starts the worker loop once; later calls are no-ops after `is_started` is true.
3434
fn start(&mut self);
3535

3636
/// Stops accepting new jobs. Does not join the worker thread or interrupt a
@@ -197,6 +197,14 @@ impl Handler for HandlerThreadInner {
197197
}
198198

199199
fn post(&mut self, func: RawFunc) {
200+
// Preserve pre-start queueing, but once a handler has been started and
201+
// then stopped it must reject new work. Otherwise post-after-stop can
202+
// either leak into an un-drained queue or wake a worker blocked in
203+
// take() and execute one more job after stop().
204+
if self.started.load(Ordering::SeqCst) && !self.alive.load(Ordering::SeqCst) {
205+
return;
206+
}
207+
200208
let q = Arc::make_mut(&mut self.q);
201209

202210
q.put(func);
@@ -277,6 +285,27 @@ fn test_handler_stop_before_start_is_noop() {
277285
assert_eq!(false, h.is_alive());
278286
}
279287

288+
#[test]
289+
fn test_handler_inner_post_after_stop_is_rejected() {
290+
let mut inner = HandlerThreadInner::new();
291+
inner.started.store(true, Ordering::SeqCst);
292+
inner.alive.store(false, Ordering::SeqCst);
293+
294+
inner.post(RawFunc::new(|| panic!("stopped handler accepted work")));
295+
296+
let mut q = inner.q.as_ref().clone();
297+
assert_eq!(true, q.poll().is_none());
298+
}
299+
300+
#[test]
301+
fn test_handler_post_before_start_is_preserved() {
302+
let mut inner = HandlerThreadInner::new();
303+
inner.post(RawFunc::new(|| {}));
304+
305+
let mut q = inner.q.as_ref().clone();
306+
assert_eq!(true, q.poll().is_some());
307+
}
308+
280309
#[test]
281310
fn test_handler_post_executes_job() {
282311
use super::sync::CountDownLatch;

‎src/publisher.rs‎

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -157,18 +157,22 @@ impl<X: Send + Sync + 'static> Observable<X, SubscriptionFunc<X>> for Publisher<
157157
self.observers.push(observer);
158158
}
159159
fn delete_observer(&mut self, observer: Arc<SubscriptionFunc<X>>) {
160-
let id;
161-
{
162-
id = observer.get_id();
163-
164-
#[cfg(feature = "for_futures")]
165-
observer.as_ref().clone().close_stream();
166-
}
160+
let id = observer.get_id();
167161

168162
for (index, obs) in self.observers.iter().enumerate() {
169163
if obs.get_id() == id {
170164
// println!("delete_observer({});", observer);
165+
#[cfg(feature = "for_futures")]
166+
{
167+
let removed = self.observers.remove(index);
168+
if !self.observers.iter().any(|obs| obs.get_id() == id) {
169+
removed.as_ref().clone().close_stream();
170+
}
171+
}
172+
173+
#[cfg(not(feature = "for_futures"))]
171174
self.observers.remove(index);
175+
172176
return;
173177
}
174178
}
@@ -533,6 +537,43 @@ fn test_publisher_delete_observer_directly() {
533537
assert_eq!(1, count.load(Ordering::SeqCst));
534538
}
535539

540+
#[cfg(feature = "for_futures")]
541+
#[test]
542+
fn test_publisher_delete_non_member_does_not_close_subscription_stream() {
543+
use super::common::SubscriptionFunc;
544+
545+
let mut owner = Publisher::new();
546+
let mut stranger = Publisher::new();
547+
let subscription = Arc::new(SubscriptionFunc::new(|_x: Arc<i32>| {}));
548+
let stream = owner.subscribe_as_stream(subscription.clone());
549+
550+
// Removing from a publisher that never owned the subscription must not
551+
// close the stream attached to the real publisher.
552+
stranger.delete_observer(subscription);
553+
554+
owner.publish(9);
555+
assert_eq!(Some(Arc::new(9)), stream.pop_front());
556+
}
557+
558+
#[cfg(feature = "for_futures")]
559+
#[test]
560+
fn test_publisher_delete_duplicate_keeps_stream_open_until_last_observer() {
561+
use super::common::SubscriptionFunc;
562+
563+
let mut publisher = Publisher::new();
564+
let subscription = Arc::new(SubscriptionFunc::new(|_x: Arc<i32>| {}));
565+
let stream = publisher.subscribe_as_stream(subscription.clone());
566+
publisher.subscribe(subscription.clone());
567+
568+
publisher.delete_observer(subscription.clone());
569+
publisher.publish(1);
570+
assert_eq!(Some(Arc::new(1)), stream.pop_front());
571+
572+
publisher.delete_observer(subscription);
573+
publisher.publish(2);
574+
assert_eq!(None, stream.pop_front());
575+
}
576+
536577
#[test]
537578
fn test_publisher_default_has_no_observers() {
538579
use std::sync::Arc;

0 commit comments

Comments
 (0)