mirror of
https://github.com/leonardomso/rust-skills.git
synced 2026-09-14 19:33:21 +08:00
0016d5cfb2
Includes rules for: - Ownership and borrowing patterns - Error handling with thiserror/anyhow - Memory management and allocation - API design following Rust guidelines - Async/Tokio patterns - Performance optimization - Naming conventions - Type safety - Testing strategies - Documentation standards - Project structure - Linting configuration - Common anti-patterns to avoid
4.5 KiB
4.5 KiB
async-bounded-channel
Use bounded channels to apply backpressure and prevent unbounded memory growth
Why It Matters
Unbounded channels grow without limit when producers outpace consumers. In production, this leads to memory exhaustion. Bounded channels apply backpressure—producers wait when the channel is full, naturally throttling the system. This prevents OOM and makes resource usage predictable.
Bad
use tokio::sync::mpsc;
// Unbounded channel - can grow forever
let (tx, mut rx) = mpsc::unbounded_channel::<Message>();
// Fast producer, slow consumer = unbounded memory growth
tokio::spawn(async move {
loop {
let msg = generate_message();
tx.send(msg).unwrap(); // Never blocks, never fails (until OOM)
}
});
tokio::spawn(async move {
while let Some(msg) = rx.recv().await {
slow_process(msg).await; // Can't keep up
}
});
// Memory grows unboundedly until crash
Good
use tokio::sync::mpsc;
// Bounded channel - backpressure when full
let (tx, mut rx) = mpsc::channel::<Message>(100); // Max 100 items
// Producer waits when channel full
tokio::spawn(async move {
loop {
let msg = generate_message();
// Blocks if channel is full - natural backpressure
tx.send(msg).await.unwrap();
}
});
tokio::spawn(async move {
while let Some(msg) = rx.recv().await {
slow_process(msg).await;
}
});
// Memory usage capped at ~100 messages
Choosing Buffer Size
// Too small: frequent blocking, reduced throughput
let (tx, rx) = mpsc::channel::<Item>(1);
// Too large: delayed backpressure, memory waste
let (tx, rx) = mpsc::channel::<Item>(1_000_000);
// Guidelines:
// - Start with expected burst size
// - Measure actual usage in production
// - Err on the smaller side initially
// Small items, high throughput
let (tx, rx) = mpsc::channel::<u64>(1000);
// Large items, moderate throughput
let (tx, rx) = mpsc::channel::<LargeStruct>(100);
// Low latency requirement
let (tx, rx) = mpsc::channel::<Command>(10);
Handling Full Channel
use tokio::sync::mpsc;
use tokio::time::{timeout, Duration};
let (tx, mut rx) = mpsc::channel::<Message>(100);
// Option 1: Wait indefinitely (default)
tx.send(msg).await?;
// Option 2: Try send, fail if full
match tx.try_send(msg) {
Ok(()) => println!("Sent"),
Err(TrySendError::Full(msg)) => {
println!("Channel full, dropping message");
}
Err(TrySendError::Closed(msg)) => {
println!("Receiver dropped");
}
}
// Option 3: Timeout
match timeout(Duration::from_secs(1), tx.send(msg)).await {
Ok(Ok(())) => println!("Sent"),
Ok(Err(_)) => println!("Channel closed"),
Err(_) => println!("Timeout - channel full for too long"),
}
// Option 4: send with permit reservation
let permit = tx.reserve().await?;
permit.send(msg); // Guaranteed to succeed
Channel Types
// mpsc: many producers, single consumer
let (tx, rx) = mpsc::channel::<Message>(100);
let tx2 = tx.clone(); // Can clone sender
// oneshot: single value, one producer, one consumer
let (tx, rx) = oneshot::channel::<Response>();
tx.send(response); // Can only send once
// broadcast: multiple consumers, each gets all messages
let (tx, _) = broadcast::channel::<Event>(100);
let mut rx1 = tx.subscribe();
let mut rx2 = tx.subscribe();
// watch: single latest value, multiple consumers
let (tx, rx) = watch::channel::<State>(initial);
// Receivers see latest value, not all values
Worker Pool Pattern
async fn process_with_workers(items: Vec<Item>) -> Vec<Result> {
let (tx, rx) = mpsc::channel(100);
let rx = Arc::new(Mutex::new(rx));
// Spawn worker pool
let workers: Vec<_> = (0..4).map(|_| {
let rx = rx.clone();
tokio::spawn(async move {
loop {
let item = {
let mut rx = rx.lock().await;
rx.recv().await
};
match item {
Some(item) => process(item).await,
None => break,
}
}
})
}).collect();
// Send items
for item in items {
tx.send(item).await.unwrap();
}
drop(tx); // Signal workers to stop
futures::future::join_all(workers).await;
}
See Also
- async-mpsc-queue - Multi-producer patterns
- async-oneshot-response - Request-response pattern
- async-watch-latest - Latest-value broadcasting