Handling Load: Concurrency and the Background Daemon
Go back to chapter 1 for a moment: the reason a naive "just spawn a task per drop" approach falls short isn't just about losing work on shutdown — it's also about what happens when hundreds or thousands of values drop in a short burst. Think of a service restarting with a few thousand open connections, or a batch job finishing and tearing down a large pool of resources all at once.
redrop's collector is built around exactly this situation.
The queue and the pump
Every async_drop() future produced by the macro goes into a single, shared, unbounded queue.
So far that's just deferring the problem — an unbounded queue can still grow without limit.
What actually bounds the work is how the queue gets drained: a loop (redrop calls it the "pump")
that keeps at most N cleanups running at once, where N is a number you control:
redrop::set_concurrency_limit(8);
Set it once, anywhere before (or even during) a burst of drops, and the pump respects it
immediately — it re-checks the limit on every iteration, so you can even change it while
cleanup is actively in progress. The default is 32, which is a reasonable starting point for a
lot of workloads, but the right number really depends on what your async_drop() talks to: a
handful of outbound HTTP calls tolerate more concurrency than, say, connections to a
rate-limited database.
If you ran the showcase in the last chapter and saw overlapping flushes, that overlap was
bounded by exactly this limit (8, by default, in showcase/redrop.toml — more on why it lives
in that file rather than in the code in the next chapter).
Two ways the queue gets drained
This is where the tokio feature from chapter 2 matters.
Without it, nothing drains the queue on its own. Cleanup futures just accumulate until you
call and .await redrop::shutdown() — at which point the pump runs, bounded by your
concurrency limit, until everything queued (and everything that was already running) has
finished. This is deliberately simple and works with any executor, not just Tokio.
With it, redrop additionally spawns a small background task — as soon as it gets a chance to
(the first time an async_drop fires, or shutdown() is called, while inside a running Tokio
runtime) — that keeps draining the queue continuously, at the same bounded concurrency, for as
long as your program runs. Drops get cleaned up as they happen rather than piling up until you
explicitly ask.
Either way, shutdown() still means the same thing: it's your guarantee that every queued
and in-flight cleanup has actually finished. With the background daemon, calling it just closes
the queue and waits for the daemon to finish draining what's left; without it, it drains the
queue itself. Call it once, as the very last thing before your program exits — we'll cover
exactly why "last" matters in the final chapter.