Skip to content

Commit 18a96af

Browse files
Harden shared_thread_pool singleton and fix rustdoc links in default build
Replace static mut + transmute in shared_thread_pool() with AtomicPtr published via Once/Release-Acquire (edition-2024 safe, MSRV unchanged). Fix broken intra-doc links that only resolved under for_futures, add a pure cargo doc gate to CI, and correct BlockingQueue/Cor/README docs to match the code. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 7cfe50b commit 18a96af

8 files changed

Lines changed: 183 additions & 66 deletions

File tree

.github/workflows/ci.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,11 @@ jobs:
5656
- uses: actions/checkout@v4
5757
- uses: dtolnay/rust-toolchain@stable
5858
- uses: Swatinem/rust-cache@v2
59-
- name: cargo doc
59+
- name: cargo doc (default / pure - matches docs.rs)
60+
env:
61+
RUSTDOCFLAGS: -D warnings
62+
run: cargo doc --no-deps
63+
- name: cargo doc (test_runtime)
6064
env:
6165
RUSTDOCFLAGS: -D warnings
6266
run: cargo doc --no-deps --features=test_runtime

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ All notable changes to this project are documented here. The format loosely foll
1717
- README CI status badge (renders live once the workflow runs on the default branch); README now states edition 2021 / MSRV 1.56.
1818
- [docs/PROJECT_NOTES.md](docs/PROJECT_NOTES.md) — project origins (three-era history), cross-language family (`fpGo`/`fpEs`), and a catalog of less-known/"leaked" public APIs (`observe_on`/`subscribe_on` scheduler routing + its no-op trap, `subscribe_blocking_queue` push→pull bridge, `do_m_pattern!`, `map_insert!`, coroutine macros) with scenarios and anti-patterns. README gained a "Less-known / advanced capabilities" section pointing to it.
1919
- Two runnable examples for previously undemonstrated "leaked" capabilities: `examples/scheduler.rs` (`MonadIO::observe_on`/`subscribe_on` thread-hopping, with the `subscribe_on`-alone no-op trap proven by asserting the delivering `ThreadId`) and `examples/publisher_queue.rs` (`Publisher::as_blocking_queue` push→pull bridge). `examples/cor.rs` gained comments documenting the sync/async `yield_from` deadlock invariant.
20+
- **Cargo.toml**`[package.metadata.docs.rs]` with `features = ["for_futures"]` so docs.rs documents the async/Rx surface (`Future`/`Stream` impls, `*_as_future`, `MonadIO::to_future`, `Publisher` stream helpers) instead of only the default `pure` API.
21+
- **docs/PROJECT_NOTES.md** — validated and corrected against git/API ground truth via a parallel multi-lens review (evidence auditor, provenance historian, leaked-API hunter, scenarios/usability, strategic synthesis). Corrected the commit count (204→206); tagged every provenance attribution as **commit-cited** (Cor←Go `28f8178`, `WillAsync`←JavaFuture `24060c2`, `Reduce`←`dtolnay/reduce` `0a53ac6`, Handler/`RawFunc`←Stack Overflow `1f15ba1`/`93a36f8`, GoF Observer←`eliovir/rust-examples` source comment since `9932452`, Akka-style ask `d173df7`) vs **naming-only [INFERENCE]** (Haskell, Java `java.util.concurrent`, Android `Handler` loop, RxJava schedulers); disambiguated the "leaked" homonym (undocumented-feature `b2e17e5` vs resource-leak `6d801ad`/`40ef803`); split the cross-language family into observed parity intent (`maybe.rs:10`) vs inferred sibling APIs; documented five additional easy-to-miss public APIs (`monadio::of`/`From`, `Publisher::map` side-effect, `Cor::yield_from` cross-type, `LinkedListAsync` `Stream`, `RawFunc`/`RawReceiver`); added a standalone `BlockingQueue` scenario (with the `unbounded`/non-interruptible anti-pattern) and corrected the Publisher scenario (stream style is doctest/test-only).
2022

2123
### Changed
2224

@@ -30,9 +32,11 @@ All notable changes to this project are documented here. The format loosely foll
3032
- Removed legacy `.travis.yml`.
3133
- **Cargo.toml** — adopted **edition 2021** and pinned **MSRV `rust-version = "1.56"`**; removed the dead commented `tokio` dependency and feature line.
3234
- **CI** — added `rustfmt --check` and `rustdoc -D warnings` gates, an MSRV (1.56) job, and a feature-combination build matrix; `clippy.sh` now fails on warnings (`-D warnings`); `publish.sh` refuses a dirty working tree (dropped `--allow-dirty`).
35+
- **CI** — the `doc` job now also builds `cargo doc --no-deps` (default `pure`, matching docs.rs) under `-D warnings`, not just `--features=test_runtime`, so broken intra-doc links in the default build fail CI.
3336
- **src/fp.rs** — reimplemented `compose!`, `pipe!`, and `compose_two` from first principles, removing a CC BY-SA-licensed StackOverflow snippet (semantics unchanged).
3437
- **src/lib.rs** — crate-level docs now list the full 8-module `pure` stack (previously mis-stated as `fp` + `maybe` only).
3538
- Deleted seven vestigial `thread::sleep` synchronization hacks from tests (each was already gated by a latch/flag/waker/queue); mechanical clippy idiom cleanup across `actor`, `common`, `maybe`, `publisher`.
39+
- **src/common.rs** — hardened the `shared_thread_pool()` lazy singleton (`for_futures`): replaced `static mut` + `mem::transmute::<Box<_>, *const _>` with an `AtomicPtr` published by a `Release` store inside `Once::call_once` and read by an `Acquire` load. This removes the `Box`→pointer `transmute` and the `static mut` (a hard error under edition 2024), narrowing the `unsafe` block to the single unavoidable leaked-pointer deref. Behavior, public API, and MSRV (1.56) are unchanged (`AtomicPtr`/`Box::into_raw` predate it); guarded by `test_common_shared_thread_pool_concurrent_init_is_singleton`.
3640

3741
### Fixed
3842

@@ -43,8 +47,11 @@ All notable changes to this project are documented here. The format loosely foll
4347
- **`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`.
4448
- **`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`.
4549
- **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`).
50+
- **`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`. The module-level doc still claimed cooperative stop "closes channels"; corrected it to match (the shared op channel is not closed). Verified by `test_cor_*` (pure + `test_runtime`).
4751
- **`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`.
52+
- **Broken rustdoc intra-doc links in the default (`pure`) build**`LinkedListAsync`'s `Stream` mention, and `sync` module/`WillAsync`/`CountDownLatch`/`BlockingQueue` links to `Future` and `poll_result_as_future`/`take_result_as_future`, referenced `for_futures`-only items and so failed to resolve when that feature was off. Because docs.rs builds default features and the CI `doc` job only checked `--features=test_runtime`, this shipped undetected: `cargo doc` (and docs.rs) rendered eight broken links. Links to always-available items now use a fully-qualified path (`std::future::Future`); links to `for_futures`-only items are plain code spans. `RUSTDOCFLAGS='-D warnings' cargo doc --no-deps` (pure) now passes.
53+
- **README claimed `cor_yield_from!` documents the sync-deadlock invariant, but it didn't** — the README ("Known behavior") stated that `cor_start!`, `set_async`, **and** `cor_yield_from!` document the sync↔sync `yield_from` deadlock in `src/cor.rs`, yet only the first two carried the note. Since `cor_yield_from!` is the operation that actually deadlocks and the macro users call, added the invariant to its rustdoc (pointing at `cor_start!` / `set_async`). The `cor_start!` macro doc-links use the explicit `crate::cor_start` path so they resolve regardless of textual position relative to the macro definition.
54+
- **`BlockingQueue` struct doc contradicted the code (claimed "bounded")** — the rustdoc summary read "Thread-safe **bounded** channel wrapper (Java `BlockingQueue`-like)", but the queue is backed by `mpsc::channel()` (unbounded) and its own `put` note says "there's no maximum size", and `docs/PROJECT_NOTES.md` correctly calls it unbounded. Corrected the summary to state it is **unbounded** and that, unlike a typical bounded Java `BlockingQueue`, `put`/`offer` never block on capacity. Doc-only; verified by `RUSTDOCFLAGS='-D warnings' cargo doc` (pure/for_futures/test_runtime) and the `sync` test suite.
4855

4956
---
5057

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ keywords = ["functional","rx","monad","optional","pubsub"]
1616
[badges.maintenance]
1717
status = "passively-maintained"
1818

19+
# docs.rs builds default features only; enable `for_futures` so the async/Rx
20+
# surface (Future/Stream impls, *_as_future, to_future, stream helpers) is
21+
# documented on docs.rs instead of being hidden.
22+
[package.metadata.docs.rs]
23+
features = ["for_futures"]
24+
1925
[lib]
2026
name = "fp_rust"
2127
path = "src/lib.rs"

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ Some public APIs are easy to miss. See [docs/PROJECT_NOTES.md](docs/PROJECT_NOTE
5151
- **Push→pull bridge**`Publisher::as_blocking_queue` / `subscribe_blocking_queue` forward published values into a `BlockingQueue` for deterministic pulling; `Publisher::subscribe_on` delivers off-thread.
5252
- **Sync↔async bridge**`BlockingQueue::{take,poll}_result_as_future` (`for_futures`) `.await` a blocking queue.
5353
- **Pattern do-notation**`do_m_pattern!` extends `do_m!` with typed `let`, reassignment, `exec`, and `ret`.
54-
- **Utility macros**`map_insert!` (bulk `HashMap` fill), `cor_newmutex_and_start!` / `cor_yield!` (coroutine building), `contains!`, `reverse!`.
54+
- **Value lifting**`monadio::of` and `From<Y>` for `MonadIO::just` / `MonadIO::from(val)`.
55+
- **Publisher side-effect map**`Publisher::map` runs a callback per publish (return value discarded; unlike `MonadIO::map`).
56+
- **Cross-type coroutine yield**`Cor::yield_from` can bridge coroutines with different yield/receive type parameters.
57+
- **`LinkedListAsync` streams** — implements `futures::Stream` (`for_futures`); used by `subscribe_as_stream` and `actor_ask`.
58+
- **Utility macros**`map_insert!` (bulk `HashMap` fill; brace `k: v`, bracket `k => v`, and comma forms), `cor_newmutex!` / `cor_newmutex_and_start!` / `cor_yield!`, `contains!`, `reverse!`.
5559

5660
### Deferred / non-goals
5761

0 commit comments

Comments
 (0)