Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
152: Cancellation and Timeouts in Async Code
One of the things that trips people up when moving from languages like Go or JavaScript to Rust is how we handle "stopping" something. In other languages, you might pass a context object or a cancellation token around and manually check if (ctx.Done()) inside your loops. In Rust, we don't really do that. Cancellation in Rust is implicit: if you stop polling a future and drop it, the task is cancelled. It's that simple, and yet, it's where most of the subtle bugs in async code hide.
The manual signal approach
Imagine you're building a service that fetches a user's profile from a flaky third-party API. You want to make sure that if the API takes longer than two seconds, you give up and return a cached version of the profile. A naive approach—one I see a lot from developers coming from other ecosystems—is to spawn a separate "timer" task that sends a signal over a channel to tell the fetch task to stop.
// The "Naive" way: Manual signaling
let (tx, rx) = mpsc::channel(1);
let timeout_tx = tx.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(2)).await;
let _ = timeout_tx.send(StopSignal).await;
});
// Inside the fetch logic...
loop {
tokio::select! {
res = api_call() => return res,
_ = rx.recv() => return Err(Error::TimedOut),
}
}
This looks reasonable, but it's overkill. You're managing channels, spawning extra tasks, and manually wiring together signals. More importantly, if the api_call() is a single, monolithic .await point (like a standard reqwest call), your select! block won't even check the receiver until the API call finishes or yields. You aren't actually cancelling the request; you're just deciding to ignore the result after the fact.
Leveraging the Drop mechanism
The "Rust way" is to realize that Futures are just state machines. If you stop polling them, they stop executing. The tokio::time::timeout function is a perfect wrapper for this. It doesn't send a signal; it simply creates a new future that polls your original future, but returns an error the moment the timer expires.
use tokio::time::{timeout, Duration};
let result = timeout(Duration::from_secs(2), fetch_user_profile(user_id)).await;
match result {
Ok(profile) => println!("Got profile: {:?}", profile),
Err(_) => println!("The request timed out and was dropped immediately"),
}
When timeout expires, it simply drops the fetch_user_profile future. Because that future is dropped, any internal resources it was holding (like the network socket) are also dropped and closed. This is incredibly efficient because there's no background "manager" task keeping track of the timeout—it's all baked into the polling logic of the executor.
The price of instant death: Cancellation Safety
Now, here is the part where I have to warn you. This "drop-to-cancel" mechanism is powerful, but it's dangerous if you aren't thinking about cancellation safety. When a future is dropped, it stops exactly where it was. It doesn't get to run a "cleanup" block or a finally clause.
Consider this scenario: you're reading a chunk of data from a socket into a buffer, and then you're processing that buffer. If your future is cancelled (via a timeout or a select!) right after the read completes but before the processing starts, that data is simply gone. You've pulled bytes out of the TCP stream, and since the future was dropped, those bytes are lost to the ether. If you were using a shared buffer or a stateful protocol, you've just corrupted your stream.
I usually tell my teammates: if you're using tokio::select! or timeout, ask yourself, "If this future is killed at any .await point, will I leave the system in an inconsistent state?" If the answer is yes, you can't just drop the future. You'll need to use a CancellationToken from the tokio_util crate to signal a graceful shutdown, or wrap the critical section in a way that it can't be interrupted until it's safe to do so.
📋 Practical Task
Exercise: Implementing a Fast-Response Mirror Race
You are building a high-availability system that fetches a configuration file from two different mirror servers (Mirror A and Mirror B). To minimize latency, you want to start requests to both mirrors simultaneously and use the result from whoever responds first. However, to prevent the system from hanging, the entire operation must time out after 500ms.
Requirements:
- Implement a function
fetch_fastest_config(). - Use
tokio::select!to race two simulated async functions:fetch_from_mirror_a()andfetch_from_mirror_b(). - Wrap the entire racing logic in a
tokio::time::timeoutof 500ms. - The simulated mirror functions should use
tokio::time::sleepwith random durations (between 100ms and 1000ms) to simulate network jitter. - If a mirror wins, return its value. If the timeout hits before either mirror responds, return a
TimeoutError.
There are no comments for now.