Search code examples
pythonpython-asyncioaiohttp

Automatically close an aiohttp session at exit python


I want to be able to automatically close an aiohttp session using atexit but I'm having trouble figuring out why this isn't working.

I had code that previously worked but once I defined the event loop outside the module and inside the file the module was being used in it breaks.

Simplified version of code:

Works as intended but public functions aren't asynchronous

# main.py
import module

client = module.Client()
client.foo()

# module/client.py
import asyncio
import atexit
import aiohttp

class Client:
    def __init__(self, loop=None):
        self.loop = asyncio.get_event_loop() if loop is None else loop
        atexit.register(self.close)
        self._session = aiohttp.ClientSession(loop=self.loop)

    def _run(self, future):
        return self.loop.run_until_complete(future)

    def close(self):
        self._run(self._session.close())

    def foo(self):
        ...
        self._run(...)
        ...

Does not work as intended, public functions are asynchronous and the loop is defined in main.py

# main.py
import module
import asyncio

async def main():
    client = module.Client()
    await client.foo()

asyncio.run(main())

# module/client.py
import asyncio
import atexit
import aiohttp

class Client:
    def __init__(self, loop=None):
        self.loop = asyncio.get_event_loop() if loop is None else loop
        atexit.register(self.close)
        self._session = aiohttp.ClientSession(loop=self.loop)

    def _run(self, future):
        return self.loop.run_until_complete(future)

    def close(self):
        self._run(self._session.close())

    async def foo(self):
        ...
        await ...
        ...

The second code segment raises the error Event loop is closed.

Solutions I've tried based on similar questions on stackoverflow give the errors the event loop is already running or there is no current event loop in thread.

Is there a solution to be able to automatically close the ClientSession when the event loop is created somewhere else? And does it matter if the public functions are asynchronous if not everything inside them is awaited?

Any help would be appreciated, thanks!


Solution

  • I'm pretty sure the atexit isn't working because your async code doesn't get called until the loop has been closed, you may want a with object by adding

    async def __aexit__(self, *error_details): 
        # await but don't return, if exit returns truethy value it suppresses exceptions.
        await self._run(self._session.close())
    async def __aenter__(self):
        return self
    

    to the Client class and then the main code can use async with to ensure the client gets closed within the main function:

    async def main():
        async with module.Client() as client:
            await client.foo()
    

    this makes the __aexit__ method get called when the with block finishes to ensure the client is closed similar to how files are closed etc using with statements.

    Otherwise you want .close() to return the awaitable so you can call it in the main function and wait for the close to finish before closing the event loop:

    async def main():
        client = module.Client()
        await client.foo()
        await client.close() # requires .close() to return the awaitable