Skip to content

Commit e50d323

Browse files
Keruspeclaude
andcommitted
promise: replace AtomicWaker with event-listener via Listener
Drop the atomic-waker dependency. Shared<T> now holds a Listener for the notify side; Promise<T> holds a cloned Listener (sharing the same Arc<Event>) for the arm/poll side. poll() follows the same loop pattern as Consumer::poll_next. The manual RefUnwindSafe impl is kept since event-listener uses UnsafeCell internally. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent dc56dd3 commit e50d323

4 files changed

Lines changed: 122 additions & 28 deletions

File tree

AGENTS.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Commands
6+
7+
```bash
8+
# Build and type-check (--all-features requires system deps for openssl; omit locally if Perl unavailable)
9+
cargo check --all --bins --examples --tests --all-features
10+
11+
# Run all tests (requires a running RabbitMQ instance)
12+
cargo test
13+
14+
# Run a single test file
15+
cargo test --test tokio # tokio integration tests
16+
cargo test --test smol # smol integration tests
17+
cargo test --test runtime_isolation --features=tokio
18+
19+
# Lint
20+
cargo clippy --all-features -- -W clippy::all
21+
22+
# Format check
23+
cargo fmt --all -- --check
24+
25+
# Format (apply)
26+
cargo fmt --all
27+
28+
# Docs
29+
RUSTDOCFLAGS=-D warnings cargo doc --no-deps --document-private-items --all-features
30+
31+
# Regenerate protocol code (after changing templates/ or lapin.json)
32+
./regen-code.sh
33+
```
34+
35+
Tests require RabbitMQ running locally (default port 5672). The CI uses a Docker `rabbitmq:latest` service.
36+
37+
## Architecture
38+
39+
Lapin is an async AMQP 0.9.1 client. The core abstraction is a `Connection` (one TCP socket) that multiplexes many lightweight `Channel`s. All I/O runs on a single background thread (the IO loop); user code interacts with it via lock-free channels.
40+
41+
### Key layers
42+
43+
**Connection & Channel (`connection.rs`, `channel.rs`)**
44+
- `Connection::connect()` / `Connection::connect_with_runtime()` are the entry points.
45+
- `connection.create_channel()` creates logical channels on the same socket.
46+
- `channel.rs` (~44 KB, hand-written) implements every AMQP operation (basic_publish, basic_consume, queue_declare, …). It delegates the heavy lifting to the generated layer.
47+
- `src/generated/channel.rs` (~73 KB, **do not edit by hand**) is produced by the codegen system and contains all option structs and low-level method implementations.
48+
49+
**IO Loop (`io_loop.rs`)**
50+
- Spawned once per connection as a background thread via `runtime.rs`.
51+
- Owns the socket, read/write buffers, and the heartbeat timer.
52+
- Implements automatic reconnection with exponential backoff (`backon` crate).
53+
- State machine: `Initial → Connected → Stop`.
54+
55+
**Internal RPC (`internal_rpc.rs`)**
56+
- `InternalRPCHandle` (cloneable `flume` sender) lets channels submit commands (Ack, Nack, Reject, CreateChannel, CloseChannel, …) to the IO loop without shared mutable state.
57+
- `InternalRPC` (receiver side) processes the command queue inside the IO loop.
58+
59+
**Frame handling (`frames.rs`, `parsing.rs`)**
60+
- `Frames` manages the outbound frame queue and tracks expected replies.
61+
- `ExpectedReply` associates an outgoing frame with a promise resolver so callers can `await` the server response.
62+
- Frame serialization/deserialization is done by the `amq-protocol` crate.
63+
64+
**Promise system (`promise.rs`)**
65+
- Thin async primitive used instead of `oneshot` channels for RPC-style request/response throughout the library.
66+
- `Shared<T>` holds the result behind a `Mutex<Option<Result<T>>>` (for ownership transfer on take) and a `Listener` (notify side only) for waking the waiting task.
67+
- `Promise<T>` holds its own `Listener` (cloned from `Shared` at construction, sharing the same `Arc<Event>`) for the arm/poll side.
68+
- `poll` uses the arm-then-recheck loop to close the lost-wakeup race window; the resolver drops the data lock before calling `notify()`.
69+
70+
**Listener (`listener.rs`)**
71+
- `Listener` wraps `event_listener::{Event, EventListener}` and is used by `Promise`, `Consumer`'s stream impl, and `Notifier`.
72+
- Usage pattern: call `arm()` before checking the guarded condition, `disarm()` if it is met, otherwise `poll()` to register the waker; `notify()` wakes all clones sharing the same `Event`.
73+
74+
**Topology & recovery (`topology.rs`, `channel_recovery_context.rs`)**
75+
- `Topology` tracks declared exchanges, queues, and bindings so the IO loop can replay them after a reconnect.
76+
- `ConnectionProperties::enable_auto_recover()` enables this behavior.
77+
78+
### Code generation
79+
80+
Templates live in `templates/channel.rs` (Handlebars) and `templates/lapin.json` (extra metadata). Running `./regen-code.sh` sets `LAPIN_CODEGEN_DIR=src/generated`, invokes `cargo build --features=codegen-internal`, and then formats the output. The generated file is committed; `build.rs` only regenerates it when the `codegen-internal` feature is active.
81+
82+
When the AMQP spec or method signatures need to change, edit `templates/` and re-run `./regen-code.sh`, then commit both.
83+
84+
### Feature flags
85+
86+
| Category | Flags |
87+
|----------|-------|
88+
| Runtime (pick one) | `tokio` (default), `smol`, `async-global-executor` |
89+
| TLS | `rustls` (default), `native-tls`, `openssl`, `rustls-platform-verifier`, `rustls-native-certs`, `rustls-webpki-roots-certs` |
90+
| Rustls crypto | `rustls--aws_lc_rs` (default), `rustls--ring` (more portable) |
91+
| DNS | `hickory-dns` |
92+
| Codegen | `codegen` (user-facing), `codegen-internal` (build.rs only) |
93+
94+
MSRV is **1.88.0** (Rust 2024 edition).

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ optional = true
4949
[dependencies]
5050
async-trait = "^0.1.42"
5151
cfg-if = "^1.0"
52-
atomic-waker = "^1.0"
5352
event-listener = "^5.0"
5453
futures-core = "^0.3"
5554
futures-io = "^0.3"

src/promise.rs

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
use crate::{Error, Result};
2-
use atomic_waker::AtomicWaker;
1+
use crate::{Error, Result, listener::Listener};
32
use std::{
43
fmt,
54
future::Future,
@@ -13,6 +12,7 @@ use tracing::{Level, level_enabled, trace};
1312
#[must_use = "Promise should be used or you can miss errors"]
1413
pub(crate) struct Promise<T> {
1514
shared: Arc<Shared<T>>,
15+
listener: Listener,
1616
}
1717

1818
impl<T> fmt::Debug for Promise<T> {
@@ -32,17 +32,19 @@ impl<T> Drop for Promise<T> {
3232

3333
impl<T> Promise<T> {
3434
pub(crate) fn new(marker: &str) -> (Self, PromiseResolver<T>) {
35-
let promise = Self {
36-
shared: Shared::new(None, marker),
37-
};
35+
let promise = Self::build(marker, None);
3836
let resolver = promise.resolver();
3937
(promise, resolver)
4038
}
4139

4240
pub(crate) fn new_with_data(marker: &str, data: Result<T>) -> Self {
43-
Self {
44-
shared: Shared::new(Some(data), marker),
45-
}
41+
Self::build(marker, Some(data))
42+
}
43+
44+
fn build(marker: &str, data: Option<Result<T>>) -> Self {
45+
let shared = Shared::new(data, marker);
46+
let listener = shared.listener.clone();
47+
Self { shared, listener }
4648
}
4749

4850
pub(crate) fn try_wait(&self) -> Option<Result<T>> {
@@ -59,17 +61,17 @@ impl<T> Promise<T> {
5961
impl<T> Future for Promise<T> {
6062
type Output = Result<T>;
6163

62-
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
63-
// Fast path: already resolved.
64-
if let Some(data) = self.shared.take() {
65-
return Poll::Ready(data);
66-
}
67-
// Register the waker before the second check so we don't miss a
68-
// wakeup that arrives between the two take() calls.
69-
self.shared.waker.register(cx.waker());
70-
match self.shared.take() {
71-
Some(data) => Poll::Ready(data),
72-
None => Poll::Pending,
64+
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
65+
loop {
66+
self.listener.arm();
67+
if let Some(data) = self.shared.take() {
68+
self.listener.disarm();
69+
return Poll::Ready(data);
70+
}
71+
match self.listener.poll(cx) {
72+
Poll::Ready(()) => {}
73+
Poll::Pending => return Poll::Pending,
74+
}
7375
}
7476
}
7577
}
@@ -122,21 +124,21 @@ impl<T> Cancelable for PromiseResolver<T> {
122124

123125
struct Shared<T> {
124126
data: Mutex<Option<Result<T>>>,
125-
waker: AtomicWaker,
127+
listener: Listener,
126128
marker: Option<String>,
127129
}
128130

129-
// AtomicWaker uses UnsafeCell internally, which opts out of RefUnwindSafe by
130-
// default. The only panic vector inside AtomicWaker is Waker::clone(); if that
131-
// panics, the waker's atomic state machine gets stuck. This is not a broke
132-
// invariant that could cause further unsoundness in code that catches the unwind.
131+
// Listener wraps event-listener::Event which uses UnsafeCell internally, opting
132+
// out of RefUnwindSafe by default. The only panic vector is Waker::wake() inside
133+
// notify(); if it panics the waiting task is not woken, but the data is already
134+
// written before notify() is called so a subsequent poll will find it.
133135
impl<T> RefUnwindSafe for Shared<T> where Result<T>: RefUnwindSafe {}
134136

135137
impl<T> Shared<T> {
136138
fn new(data: Option<Result<T>>, marker: &str) -> Arc<Self> {
137139
Arc::new(Self {
138140
data: Mutex::new(data),
139-
waker: AtomicWaker::new(),
141+
listener: Listener::default(),
140142
marker: if level_enabled!(Level::TRACE) {
141143
Some(marker.into())
142144
} else {
@@ -152,7 +154,7 @@ impl<T> Shared<T> {
152154
// Release the lock before waking to avoid the woken task
153155
// immediately blocking on it.
154156
drop(lock);
155-
self.waker.wake();
157+
self.listener.notify();
156158
}
157159
}
158160

0 commit comments

Comments
 (0)