pythonasynchronousasync-awaitdaemon

Can I use a python script to run as a daemon with async libs or do I need to use subprocess?


I need to run a quick experiment with python. However, I am stuck with an async issue. I am not very familiar with python async programming, so please bear with me.

What I want is to run a script which starts different daemons at different ports, and then waits for connections to be established to it.

I used basically the code below (which does not run, because I wanted to simplify for the question, but it should be enough to make the case. If not, please let me know).

The issue I have is that this code (probably obvious to the experts) stalls at the first sleep_forever. In other words, only one daemon gets started.

Can this be done at all from the same script? Or do I need subprocesses (or something else completely) to start? I tried removing the await from self.__start(), but that results in an error saying __start() was not awaited. My thinking was that then the async function would indeed not be awaited and the script would move on, while the network stuff would be initiated and then it would wait. Looks like it doesn't work like that. I also tried starting a task, but then the task needs to be awaited as well?

import trio

class Daemon:
    port: int

    @classmethod
    async def new(cls, port):
        self = cls()
        self.port = port
        #assume this to return an `AbstractAsyncContextManager`
        self.server = create_server() `
        await self.__start()

    async def __start(self):
        print("starting with port...".format(port=self.port))
        # init network socket
        async with self.server.start(port=self.port), trio.open_nursery() as nursery:
            #setup network objects
            print("server ok")
            # ->> WAIT FOR INCOMING Connections 
            await trio.sleep_forever()

        print("exiting.")


async def run():
    d_list = []
    num = 3
    for i in range(num):
        d = await Daemon.new(5000+i)
        d_list.append(d)

if __name__ == "__main__":
    trio.run(run)

Solution

  • What you've written is a single threaded program with no concurrency at all. It will create each Daemon in turn and wait for that Daemon to finish, before moving on to the next iteration of the loop. This isn't what you want at all.

    You need to use the feature of asyncio that provide concurrency. And that feature is a Task. A task will continue executing until it finds an await statement that cannot complete immediately. Such as a sleep() or waiting for a socket to connect.

    It is vitally important that you keep a reference to tasks that have not yet completed. Failing to do so may (will) mean the task will not run to completion. As you're already using trio, use its nurseries to take care of that for you.

    To using tasks in your program is a trivial switch.

    async def run():
        num = 3
        async with trio.open_nursery() as nursery:
            for i in range(num):
                nursery.start_soon(Daemon.new, 5000+i)
    
        print('all tasks in the nursery will have stopped after the with block')
        # however, as your daemons never return, this print statement is never executed