redrop

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.

← Setting Up Handling Load: Concurrency and the Background Daemon →