#378·actix

Async/await interop suggestion

Author: DiggseyCreated Apr 14, 2020Updated Jul 18, 2024

I wrote a small crate for using async/await syntax with actix. It also makes the "AtomicResponse" type obsolete by providing a more ergonomic mechanism to achieve the same thing. I think it would be nice to have as part of actix itself.

The basic idea is to use scoped TLS (at least until custom future contexts are supported) to store the actor and its context.

There are three functions:

  • FutureInterop::interop_actor()

    This is equivalent to the into_actor() method except that it populates the TLS slots with the actor and the context before polling the inner future. There's also a boxed variant of this function.

  • with_ctx

    This takes a closure and immediately calls it, passing in mutable references to the actor and context obtained from the TLS slots to that closure.

  • critical_section

    This takes a future/async block and uses ctx.wait() to make sure it has exclusive access to the actor state while it is running. It returns a future itself so you can seamlessly demarcate blocks of code within an async/await block as being critical sections.

Example:

rust

impl Handler<Request> for MyActor {
    type Result = ResponseActFuture<Self, Result<Response, ()>>;

    fn handle(&mut self, msg: Request, _ctx: &mut Context<Self>) -> Self::Result {
        async move {
            // Modify the actor state in some way
            with_ctx(|a: &mut Self, _| a.mutate());

            ... <some async code> ...

            // Run some code with exclusive access to the actor
            let result = critical_section::<Self, _>(async {
                // Put the actor into an inconsistent state.
                // We don't want this visible outside the critical section.
                with_ctx(|a: &mut Self, _| a.begin_mutation());

                ... <some async code> ...

                // Make everything consistent again
                Ok(with_ctx(|a: &mut Self, _| a.end_mutation()))
            }).await;

            ... <some async code> ...

            // Return a result
            result
        }.interop_actor_boxed(self)
    }
}

Some things which are very hard to do right now are made much easier in this pattern (for example, when only half of a message handler needs to run atomically). Also, all of the normal futures combinators can be used, rather than the more limited set exposed under actix::fut.

Currently the crate relies on a patch to scoped_thread_local which hasn't been merged yet, which is why I haven't published it. However, except for scoped-tls this crate itself does not require any unsafe code at all.