Your First #[async_drop]
Here's the whole thing, and then we'll take it apart slowly:
#[redrop::async_drop]
impl LogShipper {
async fn async_drop(&mut self) {
// pretend this sends self.buffered_lines somewhere over the network
tokio::time::sleep(std::time::Duration::from_millis(15)).await;
println!("shipper #{}: flushed {} lines", self.id, self.buffered_lines);
}
}
That's it. No impl Drop, no channel to wire up by hand, no explicit call anywhere. Once this
is in place, plain old:
drop(LogShipper { id: 0, buffered_lines: 12 });
is enough to have the flush happen, asynchronously, without blocking whatever called drop.
What the macro actually generates
It helps to know what #[redrop::async_drop] expands to, because it's not magic — it's a fairly
small, ordinary piece of code. Roughly:
impl std::ops::Drop for LogShipper {
fn drop(&mut self) {
let owned: LogShipper = std::mem::take(self);
redrop::enqueue(async move {
let mut owned = owned;
owned.async_drop().await;
// (redrop finalizes `owned` here so its fields drop normally afterwards)
});
}
}
Two things are doing the real work:
std::mem::take(self). Drop::drop only ever gives you &mut self — a reference, not
ownership. But the async cleanup needs to run after drop returns, possibly much later, on a
background task. To do that, it needs to actually own the data, not just borrow it briefly.
mem::take solves this: it moves the real value out into owned, and leaves a fresh
LogShipper::default() behind in self's place. This is exactly why we needed
#[derive(Default)] back in the last chapter — mem::take needs something valid to leave
behind, and Default::default() is what it uses.
redrop::enqueue(...). Instead of running the cleanup future right here and now (which
would make drop async, which — as established — isn't allowed), it's handed to redrop's
collector, which will run it later, respecting a concurrency limit so a burst of drops doesn't
overwhelm anything. That's the subject of the next chapter.
Try it
If you're following along in the showcase/ crate:
cargo run -p redrop-showcase
You'll see each LogShipper get "flushed" with its buffered line count, and — if you look
closely at the order things print in — you'll notice several flushes overlapping rather than
running one at a time. That overlap is deliberate, and it's what the next chapter is about.