How can I do this in Rust with async/await?


#1

I wanna make a process supervisor app.
It has two basic components: a control api service and the main supervisor service.
And I wanna roll it on a single-threaded model with async/await approach.
I can easily model the main sketch in python or any other PL with GC like this:

import asyncio
import random


class App:
    def __init__(self):
        self.api = ControlService(self)
        self.supervisor = SupervisorService()

    async def serve(self):
        await asyncio.wait([
            self.api.serve(),
            self.supervisor.serve()
        ])


class ControlService:
    def __init__(self, app):
        self.app = app

    async def serve(self):
        while True:
            print('[ControlService   ] Waiting user request...')
            cmd, arg = await simulate_user_request()
            print("[ControlService   ] Received user cmd: {}".format([cmd, arg]))
            if cmd == "set":
                self.app.supervisor.set_value(arg)
            else:
                pass


class SupervisorService:
    def __init__(self):
        self._value = 0

    def set_value(self, value):
        self._value = value

    async def serve(self):
        while True:
            print('[SupervisorService] Business value: {}'.format(self._value))
            await asyncio.sleep(1)


async def simulate_user_request():
    await asyncio.sleep(2)
    return "set", random.randint(0, 10)


if __name__ == '__main__':
    app = App()
    loop = asyncio.get_event_loop()
    loop.run_until_complete(app.serve())
    loop.close()

But with Rust I have problem:
If the control service and supervisor serve both serving, then the supervisor must be borrowed and be held in the async fn serve(self: &SupervisorService). So, The control service is impossible to mutate the supervisor service’s parameter.
So, How can I solve the problem?
What’s the best practice of making this kind of app in Rust?


#2

Have you looked into using a data structure with shared ownership and interior mutability like Rc<RefCell<T>>? There’s a lot of useful info in the book if you need a quick reference to the what-and-why:


#3

@parasyte Thanks for your suggestion.

I have considered Rc<RefCell<T>>. But there are still some worries.

async fn serve(self: Rc<RefCell<SupervisorService>>) {
    loop {
        await!(do_something_async(self));
        await!(do_another_thing_async(self.borrow().member)); // may cause runtime panic here.
        // ...
    }
}
  1. I have to implement all these async functions with parameter Rc<RefCell<T>>, I consider it is a hell.
  2. If awaiting a member’s async operation inside, I think it will be easy to trigger a runtime panic.

#4

You can model this with futures channels - ControlService sends a Message over a channel to the SupervisorService that’s reading the channel, and it can then handle the update. This means the control and supervisor don’t have direct references, but rather communicate over a channel.


#5

@vitalyd
But then the same problem will appear inside the SuperiorService again. Sounds like it just moved the ControlService into the SupervisorService.


#6

You probably will still need the RefCell (or Cell, depending on what the mutable state is), but you shouldn’t need the Rc. There may be a way to avoid it with async/await + Pin, but I’m not fully up to speed on that yet.

Maybe you can sketch out some Rust code that we can talk about in particular?


#7

I was thinking more along the lines of each of ControlService and SupervisorService sharing ownership of a third struct that contains the mutable state. Using custom self types for this seems very strange to me. Using channels would be a better model for mutating state between the two services, though.


#8

Ok, I’ll sketch out some Rust code once the a new version of futures-preview being landed.
It is lacking stuffs to make codes compilable right now.