Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,14 @@ jobs:
- run: cargo build --all-features --verbose
- run: cargo test --all
- run: cargo test --all --all-features

msrv:
runs-on: ubuntu-latest
strategy:
matrix:
features: ["--no-default-features", "--all-features"]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.85.0
# Dev targets use current stable; verify the consumer library graph here.
- run: cargo check -p auto_pool --lib ${{ matrix.features }}
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ edition = "2021"
authors = ["Sild <silddev@icloud.com"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/sild/libs_rs"
rust-version = "1.81"
rust-version = "1.85"
publish = false

[profile.bench]
Expand Down
51 changes: 51 additions & 0 deletions auto_pool/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# auto_pool maintainer guide

- This public crate owns caller-supplied object storage and borrowed RAII
checkout. Keep `Mutex<Vec<T>>`, `pool::AutoPool`, `config::AutoPoolConfig`,
`config::PickStrategy`, and `pool_object::PoolObject` as the canonical paths.
No factories, background workers, unsafe storage, or alternate public APIs.
- Every successful checkout removes exactly one item. Drop returns its mutated
value exactly once; `release()` removes it permanently. The wrapper borrows
the pool. Never hold a storage guard across an await or user work.
- Async waiters register then recheck storage. Each return sends an additional
notification; cancellation must forward an unconsumed notification. Sync
and async consumers can race for items, with no promised waiter fairness.
- Use one deadline across retries. Zero attempts immediately; an unrepresentable
deadline means unlimited waiting. Document scheduling/short mutex limitations.
Do not reintroduce polling via the ignored public `lock_duration` or
`sleep_duration` fields or remove these fields in a compatible release.
- The `async` feature is opt-in and executor-independent. Do not add a self
dev-dependency that enables it. Gate async examples, docs, and tests explicitly.
- Library MSRV is 1.85 because rand 0.10 requires it; dev targets use current
stable (Criterion needs 1.86+). Keep the workspace manifest and README aligned.
Cargo.lock is intentionally ignored. Release-plz owns version changes.
- Extend existing config/methods only for a demonstrated contract. Public fields,
paths, bounds and feature names are compatibility commitments. Update README,
rustdoc, examples, tests and this guide together when contracts change.
- Fast checks: `cargo test -p auto_pool --no-default-features` and
`cargo test -p auto_pool --no-default-features --features async`.
- Full affected checks, from the workspace root:
```sh
cargo test --workspace
cargo test --workspace --all-features
cargo test -p auto_pool --examples --all-features
cargo run -p auto_pool --example main
cargo run -p auto_pool --example main --features async
cargo +nightly fmt --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
RUSTDOCFLAGS="-D warnings" cargo doc -p auto_pool --no-deps --all-features
RUSTDOCFLAGS="-D warnings" cargo doc -p auto_pool --no-deps --no-default-features
cargo +1.85.0 check -p auto_pool --lib --no-default-features
cargo +1.85.0 check -p auto_pool --lib --all-features
cargo package -p auto_pool --list
cargo package -p auto_pool
git diff --check
```
- For dependency/package changes, extract `target/package/auto_pool-*.crate`
into a temporary directory outside the workspace. Run a consumer on Rust
1.85 with default dependency features, `default-features = false`, and that
setting plus `features = ["async"]`. Exercise struct literals, checkout,
mutation, drop, add and release. Inspect the normalized manifest and package
inventory. Remove the temporary consumer afterward.
- Require one independent review for substantive concurrency/dependency changes.
Benchmarks in BENCHMARKS.md are scoped measurements, not universal speed claims.
59 changes: 59 additions & 0 deletions auto_pool/BENCHMARKS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Benchmark workloads

Run a bounded local sample from the workspace root:

```sh
cargo bench -p auto_pool --bench multithread_push_pop --all-features -- \
--warm-up-time 0.2 --measurement-time 0.5 --sample-size 10 --noplot
```

- `uncontended/{lifo,random,mutex_stack,allocate_1k}` measures checkout, a
mutation of a reused 1 KiB byte buffer, and return. Pools have 64 objects.
The mutex stack takes two locks and has no waiting or RAII wrapper. Allocation
includes zero-initialization, mutation and deallocation of a fresh buffer.
- `contention/auto_pool/{1,4}` uses four synchronized workers. One item forces
consumers to compete for availability; four items isolate storage contention
without item exhaustion. Each measured round is 400 operations (100 per
worker). The mutex-stack baseline has four objects; allocation has no shared
state. Thread creation and join are excluded from the returned measurement.
Start and finish barrier overhead is included once per sample.
- `exhaustion/try_empty` measures an unsuccessful zero-budget checkout.
`exhaustion/wait_1ms` includes the configured timeout and scheduler latency.
- `async/handoff_from_thread` registers an empty-pool future before requesting
an item from a producer thread. It sums the interval from just before `add()`
to consumer resumption and `release()`. Request-channel delay, listener setup,
and thread creation/join are excluded. It uses smol's local `block_on`, not
a loaded multi-task executor, and has no configured timeout.

The harness uses `std::hint::black_box`. Grow-on-demand third-party pools are
not compared because their exhaustion behavior differs. Compare only matching
workloads and report the feature set, machine, toolchain and sample settings.
These measurements do not establish a universal speedup, waiter fairness,
tail latency under load, or superiority over other pool implementations.
`swap_remove` is a readability simplification, not a measured optimization.

## Local sample: 2026-09-10

Apple M3 Max, macOS arm64, rustc 1.97.1, `--all-features`, Criterion 0.8.2;
10 samples, 0.2 s warmup and 0.5 s measurement per workload. Point estimates
from bounded runs (contention rerun after adding the untimed readiness
barrier; not a before/after comparison):

| Workload | Estimate |
| --- | ---: |
| Uncontended LIFO | 32.6 ns / operation |
| Uncontended random | 37.8 ns / operation |
| Uncontended mutex stack | 4.94 ns / operation |
| Uncontended allocate 1 KiB | 25.5 ns / operation |
| Four workers, one pooled item | 57.3 us / 400 operations |
| Four workers, four pooled items | 59.8 us / 400 operations |
| Four workers, mutex stack | 4.50 us / 400 operations |
| Four workers, allocation | 17.6 us / 400 operations |
| Empty, zero budget | 39.6 ns / attempt |
| Empty, 1 ms budget | 1.25 ms / attempt |
| Async handoff from a thread | 2.84 us / handoff |

The minimal stack and allocation baselines are cheaper in this deliberately
small workload. They omit waiting, notifications and borrowed RAII semantics.
The one-item and four-item cases also differ in scheduling and availability;
these results do not predict performance for a particular application.
6 changes: 2 additions & 4 deletions auto_pool/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,18 @@ rust-version.workspace = true
publish = true

[features]
async = ["dep:smol"]
async = ["dep:smol", "dep:event-listener"]

[dependencies]
parking_lot = "0.12"
event-listener = { version = "5.4", optional = true }
smol = { version = "2.0", optional = true }
rand = "0.10.0"

[dev-dependencies]
criterion = { version = "0.8" }
lockfree-object-pool = "0.1.6"
object-pool = "0.6.0"
anyhow = "1.0"
tokio = { version = "1.43.0", features = ["rt", "macros"] }
auto_pool = {path = "", features = ["async"]}

[[bench]]
name = "multithread_push_pop"
Expand Down
126 changes: 90 additions & 36 deletions auto_pool/README.md
Original file line number Diff line number Diff line change
@@ -1,47 +1,101 @@
# auto_pool

This pool automatically manages the return of objects.
A small, thread-safe pool of caller-supplied objects. `AutoPool<T>` stores a
`Mutex<Vec<T>>`; checkout returns a borrowed `PoolObject<'_, T>` that implements
`Deref` and `DerefMut`. Dropping the wrapper returns the object, including any
mutations. Calling `release()` takes ownership permanently instead.

The primary difference from competitors is the interface - implementation follows the RAII pattern, meaning the user must provide objects when creating the pool.
```toml
[dependencies]
auto_pool = "0.3.3"
# Enable asynchronous checkout when needed:
# auto_pool = { version = "0.3.3", features = ["async"] }
```

The `Pool` class has only three public methods:
- Constructor `with_config(config: Config, items)`: Creates a new pool with a custom configuration.
- Constructor `new(items)`: An alias for `with_config` that uses the default configuration.
- `take(&self)`: Extracts an object from the pool.
```rust
use auto_pool::config::AutoPoolConfig;
use auto_pool::pool::AutoPool;
use std::time::Duration;

The configuration allows you to set the wait duration for the `take` method (default is `Duration::MAX`, which essentially means "Wait until you receive it").
# fn main() -> Result<(), Box<dyn std::error::Error>> {
let pool = AutoPool::new_with_config(
AutoPoolConfig {
wait_duration: Duration::from_millis(20),
..Default::default()
},
[String::with_capacity(1024)],
);
{
let mut buffer = pool.get().ok_or("pool exhausted")?;
buffer.push_str("reused allocation");
} // the buffer returns here
let buffer = pool.get().ok_or("pool exhausted")?.release();
assert_eq!(buffer, "reused allocation");
assert_eq!(pool.size(), 0);
pool.add(buffer);
# Ok(())
# }
```

`AutoPool::new(items)` uses the default configuration. Constructors are
infallible. `add(item)` supplies or returns an item; `size()` counts only
available items; `shrink_to_fit()` shrinks available storage, not checked-out
objects. Items must be `Send + 'static`. The wrapper borrows its pool, so the
pool must outlive every checkout. Share the pool across threads with `Arc` or
scoped threads. The pool never creates replacement objects.

Examples:
```rust
## Waiting and selection

fn main() -> Result<(), autoreturn_pool::Error> {
// basic usage
let pool = autoreturn_pool::Pool::new([1, 2])?;
let item = pool.take()?.unwrap();

// with custom config
let config = autoreturn_pool::Config {
wait_duration: std::time::Duration::from_millis(5),
};
let pool = autoreturn_pool::Pool::with_config(config, [1, 2])?;
let item = pool.take()?.unwrap();
}
```
`get()` waits up to `wait_duration` and returns `None` on timeout. Retries use
one overall deadline, including initial synchronous mutex acquisition.
`Duration::ZERO` performs an immediate attempt; `Duration::MAX` (and other
values that overflow the platform's `Instant`) means an unlimited wait.

Timeouts are not hard real-time guarantees: scheduling and mutex reacquisition
can delay completion. An object available when a waiter resumes may win a race
with its deadline. There is no FIFO fairness guarantee between consumers.
Avoid holding all objects while requesting another with an unlimited wait.

`PickStrategy::LIFO` is the default. `PickStrategy::RANDOM` selects from the
available items using a random index; it changes object selection, not waiter
priority.

## Async feature

The optional `async` feature adds `get_async()`. It works on any executor;
finite deadlines use smol's timer. Exhausted pools suspend through
[event-listener](https://docs.rs/event-listener/5.4.2/event_listener/struct.Event.html)
notifications, with no blocking item wait or sleep polling. Checkout still
briefly locks the storage mutex, as do `add()` and wrapper drop. Avoid expensive
pool maintenance on latency-sensitive executor threads.

```rust
// with custom object:
#[derive(Default)]
struct MyObject {
value: i32,
}
fn main() -> Result<(), autoreturn_pool::Error> {
let pool_objects = [
MyObject::default(),
MyObject::default()
];
let pool = autoreturn_pool::Pool::new(pool_objects)?;
let mut item = pool.take()?.unwrap();
Ok(())
}
# #[cfg(feature = "async")]
# fn main() -> Result<(), Box<dyn std::error::Error>> {
# smol::block_on(async {
use auto_pool::pool::AutoPool;
let pool = AutoPool::new([vec![0u8; 1024]]);
let mut buffer = pool.get_async().await.ok_or("pool exhausted")?;
buffer[0] = 42;
drop(buffer); // also wakes waiting consumers
# Ok(())
# })
# }
# #[cfg(not(feature = "async"))]
# fn main() {}
```

Dropping a pending checkout cancels it without consuming an item. Sync and
async consumers can use the same pool. `lock_duration` and `sleep_duration`
remain public for source compatibility but are ignored; `wait_duration` is
the only waiting setting. Their old defaults remain unchanged.

## Toolchains and development

The library requires **Rust 1.85**, matching its existing `rand 0.10`
dependency. The old 1.81 declaration was inaccurate. Development tests and
benchmarks use current stable Rust (Criterion 0.8 requires at least 1.86), and
formatting uses nightly. Release-plz manages crate versions.

See [AGENTS.md](AGENTS.md) for maintainer checks and
[BENCHMARKS.md](BENCHMARKS.md) for benchmark workloads and measurement limits.
Loading
Loading