testcontainers-rs
testcontainers-rs copied to clipboard
Use "drop-notifier" pattern for to asynchronously shut down containers with `Http` client
Not sure if this is documented anywhere but I've since learned about this very nifty pattern for doing async
work in a Drop
implementation:
- Have a
Void
async, oneshot channel - Store the
Sender
in the struct you intend to eventuallyDrop
- Listen on the
receiver
in a task - Once you get
oneshot::Canceled
, you know that the struct has been dropped - Perform the async work
To utilize this, we need to store the Sender
in ContainerAsync
and the Receiver
in Http
. The client (Http
), needs to create one task per container that it starts and shut it down as soon as it receives oneshot::Canceled
.
This should allow us to drop the executor
feature on futures
.
Can you please help me understand the idea? I'm failing to come up with a possible implementation for a good half of what you wrote :D
To execute some async work — we'd need to spawn an async task, and to do that — we need an executor. Also, to perform a "proper" drop because of the threads and stuff, we'd need to do a join
on all the spawned threads inside the Http
's Drop...
What am I missing? Do you have an example of the implementation?
So when writing something up, I realized that actually utilizing the drop notifier here is a bit pointless :D
I'll write it up anyway because I think it is a really interested pattern.
Pattern
What this does not do is synchronize the drop with the async code that executes, i.e. Drop
will complete before the async work gets done.
- Return a
DropListener
in addition toContainterAsync
fromHttp::run
. -
DropListener
would implementFuture
and probably just store aBoxFuture
inside. - The
BoxFuture
would be constructed from something like:
let client = Http {
inner: self.inner.clone(),
};
Box::pin(async move {
match listener.await {
Ok(void) => void::unreachable(void),
Err(Canceled {}) => {
client.rm(id).await;
}
}
});
Returning a dedicated future makes us executor agnostic. What users would have to do is:
let (container, drop_listener) = client.run(HelloWorld {}).await;
tokio::spawn(drop_listener);
This is quite a klunky API though and with testcontainers
being targeted at making tests ergonomic, this is quite the deal breaker.
There is an alternative to this where we define an Executor
trait and users you can pass in an instance when you initially construct Http
. That would allow us to spawn tasks onto the user's executor. However, if we do that, we might as well just call
executor.spawn(async move {
client.rm(id).await;
})
from the container's Drop
impl directly, no need to use the drop notifier pattern.