fanring 0.3.6

High-throughput typed MPSC and MPMC channels with per-producer backpressure
Documentation
# fanring

Fast typed MPSC and MPMC channels built from one SPSC `yring` per producer.

Sender registration is dynamic. Each sender writes to its own ring, so
producers do not contend on a shared queue tail. MPSC drains those rings
directly; MPMC stages prefetched batches in stealable receiver-local queues.

`Deferred` teardown is the default and avoids cleanup coordination on sends.
Choose `Coordinated` to reclaim unread payloads independently of idle sender
lifetimes.

Requires Rust 1.93 or newer.

This workspace maintains two independently published crates:

- `fanring`: typed MPSC and MPMC channels.
- [`yring`]yring/README.md: bounded SPSC rings with batched publication.

Each crate has its own version and changelog.

| | Nonblocking | Blocking | Timeout | Deadline |
| --- | --- | --- | --- | --- |
| Send | `try_send` | `send` | `send_timeout` | `send_deadline` |
| Receive | `try_recv` | `recv` | `recv_timeout` | `recv_deadline` |

## Performance

Median throughput and blocking wake latency on the system named in each
chart, measured with the `performance` governor and CPU turbo disabled.
Detailed throughput heatmaps cover thread counts and payload sizes.

![Common MPSC and MPMC benchmark cases](https://raw.githubusercontent.com/paddor/fanring.rs/main/doc/charts/throughput-summary.svg)

#### Detailed MPSC

![MPSC benchmark chart](https://raw.githubusercontent.com/paddor/fanring.rs/main/doc/charts/throughput-mpsc.svg)

#### Detailed MPMC

![MPMC benchmark chart](https://raw.githubusercontent.com/paddor/fanring.rs/main/doc/charts/throughput-mpmc.svg)

#### MPSC wake latency

![MPSC wake-latency chart](https://raw.githubusercontent.com/paddor/fanring.rs/main/doc/charts/latency-mpsc.svg)

#### MPMC wake latency

![MPMC wake-latency chart](https://raw.githubusercontent.com/paddor/fanring.rs/main/doc/charts/latency-mpmc.svg)

## Teardown policies

`Deferred` is the default teardown policy: unread ring payloads may remain
alive until their sender drops. Choose `Coordinated` when queued replies,
permits, or buffers must be released independently of idle sender lifetimes:

```rust
use fanring::{mpsc, teardown::Coordinated};
let (tx, rx) = mpsc::channel_with_policy::<String, Coordinated>(256);
```

The policy is part of the endpoint types and inherited by cloned handles.
`Coordinated` teardown drains unread values; overlapping sends clean up late
publications when they resume. Receiver drop does not wait for paused producers.
The additional send coordination has a measurable cost:

![MPSC teardown policy throughput](https://raw.githubusercontent.com/paddor/fanring.rs/main/doc/charts/teardown-mpsc.svg)

![MPMC teardown policy throughput](https://raw.githubusercontent.com/paddor/fanring.rs/main/doc/charts/teardown-mpmc.svg)

The general comparison and wake-latency charts above use `Deferred` for fanring.
These policy charts compare `u64` traffic under both policies on the same system.
See [teardown contracts, reproduction, and observed behavior in other channels](doc/teardown.md).
The pinned bounded Crossbeam, Crossfire, Flume, Kanal, and Thingbuf versions all
retain unread payloads in our receiver-drop probe while senders remain alive.
This is a payload-lifetime distinction; successful send still does not guarantee
that application code processes the message.

## MPSC

```rust
use fanring::mpsc::channel;

let (mut tx0, mut rx) = channel(8);
let mut tx1 = tx0.try_clone().expect("receiver alive");

tx0.send("from sender 0").unwrap();
tx1.send("from sender 1").unwrap();

for _ in 0..2 {
    println!("{}", rx.recv().unwrap());
}

assert_eq!(tx0.send("still open"), Ok(()));
drop(rx);
assert_eq!(tx0.send("closed").unwrap_err().into_inner(), "closed");
```

`mpsc` preserves FIFO within each sender lane and relaxes order across senders.
Single-value receives batch slot release: receiving a value may leave its slot
unavailable to the sender until a later receive. Call `rx.release_consumed()`
before returning application permits or issuing completions that allow more
sends. This publishes freed slots across sender lanes and wakes blocked senders.

To receive a bounded batch into reusable storage:

```rust
let mut batch = Vec::with_capacity(32);
while rx.recv_batch_into(&mut batch, 32).is_ok() {
    for value in batch.drain(..) {
        // Process value. Its ring slot is already reusable.
    }
}
```

`recv_batch_into` appends at most the requested limit, waits only for the first
value, and releases consumed slots before returning. It returns the number
appended; a partial batch succeeds even after disconnect. Reserve enough spare
vector capacity to avoid output reallocations. A zero limit receives nothing,
releases consumed slots, and returns `Ok(0)`.

## MPMC

```rust
use fanring::mpmc::channel;

let (mut tx, mut rx0) = channel(256);
let mut rx1 = rx0.clone();

tx.send("work 0").unwrap();
tx.send("work 1").unwrap();

let a = rx0.recv().unwrap();
let b = rx1.recv().unwrap();
assert_ne!(a, b);
```

`mpmc` drains up to 64 values from a ready sender ring. With one receiver, the
remaining values stay in a private deque. Cloning publishes that deque before
the new receiver becomes visible. With multiple receivers, remaining values
enter bounded synchronized work queues, and competitors steal up to eight at a
time. Receiver drop republishes its buffered work. Ordering is relaxed. Moving
values into that second-stage queue costs more for large inline types; box large
payloads when move bandwidth dominates.

`mpmc::try_recv` may return a transient `Empty` while bounded lane maintenance
or another receiver moves work. `Disconnected` is final: all senders are gone,
sender rings and staged queues are drained, and no work publication is in
flight.

## Contract

- One bounded ring per dynamic sender; capacity is a per-sender HWM rounded up
  to a power of two.
- MPSC preserves FIFO within each sender lane. MPMC ordering is relaxed and its
  receiver staging can temporarily exceed sender-ring capacity.
- Nonblocking, blocking, timeout, and deadline operations are available.
  Blocking operations spin briefly before parking.
- Dropping the last sender preserves buffered values for receivers to drain.
  Last-receiver drop follows the chosen teardown policy. MPMC `Empty` may be
  transient while receivers move internal work; `Disconnected` is final.
- Sender hot paths and MPSC batching avoid a shared queue lock. MPMC work
  distribution and topology maintenance use mutexes; blocking operations may
  park.

## Good Fit

- Many long-lived or frequently changing producers feeding one or more
  consumers.
- MPSC callers need only per-producer FIFO; MPMC callers accept relaxed order.
- Per-producer HWM matches the desired backpressure model.
- Callers can use built-in parking or own the backoff policy around `try_*`.

## Bad Fit

- Need global FIFO or strict one-item round robin.
- Need one exact capacity shared across all producers.
- Need an exact total MPMC bound that includes receiver staging.
- Need async MPMC wakeups.

## Further reading

* More detail: [DESIGN.md]DESIGN.md
* Development and benchmark reproduction: [DEVELOPMENT.md]DEVELOPMENT.md
* Release history: [CHANGELOG.md]CHANGELOG.md

## License

[ISC](LICENSE)

## Async MPSC

Enable the optional `async` feature for runtime-independent `send_async`,
`recv_async`, and `recv_batch_into_async`. No Tokio dependency or blocking wait
is required. Successful sends publish immediately. Bulk receive waits only for
the first value and releases consumed slots before returning. Reserve the output
vector once to avoid per-value allocations. MPMC remains synchronous.

Each sender has its own capacity waker; the receiver has one data waker.
Registration is followed by a queue recheck. Canceling an incomplete send drops
its unsent value without reserving capacity. Canceling an incomplete receive
consumes nothing. Both remove retained wakers. Manual `poll_ready` and `poll_recv`
users must cancel abandoned registrations explicitly. Synchronous methods and
both teardown policies retain their existing contracts.

`Sender::try_register_bounded(max_lanes)` limits allocated rings, including
retired senders awaiting receiver drain. The ordinary registration API remains
unbounded. `registered_lanes()` reports allocated rings, rather than live senders.

Both workspace crates remain independently consumable. Fanring's versioned
yring path dependency resolves to the published yring version when packaged.