empicano / aiomqtt

The idiomatic asyncio MQTT client
https://aiomqtt.bo3hm.com
BSD 3-Clause "New" or "Revised" License
427 stars 75 forks source link

creating Client outside async starts a loop? #335

Closed ofer-pd closed 2 weeks ago

ofer-pd commented 2 months ago

Hello. This might be my own ignorance - I just started learning async Python in order to use aiomqtt. (Latest version: 2.3.0)

This example demonstrates the problem:

import aiomqtt
import asyncio

async def main_async( client ):
    async with client:
        await client.subscribe( "temperature/#" )
        async for message in client.messages:
            print( f"{ message.topic }" )

client = aiomqtt.Client( "test.mosquitto.org" )
asyncio.run( main_async( client ) )

Which produces an error like: "RuntimeError: Task <Task pending name='Task-1' coro=<main_async() running at /home/ofer/src/foo.py:5> cb=[_run_until_complete_cb() at /home/ofer/.pyenv/versions/3.12.0/lib/python3.12/asyncio/base_events.py:180]> got Future attached to a different loop"

But moving the creation of the client inside the async function solves it:

import aiomqtt
import asyncio

async def main_async():
    client = aiomqtt.Client( "test.mosquitto.org" )
    async with client:
        await client.subscribe( "temperature/#" )
        async for message in client.messages:
            print( f"{ message.topic }" )

asyncio.run( main_async() )

Is this normal / to-be-expected? Might it be worth noting in the docs for noobs?

PS - I got the pattern of creating the client separate from using it from the example on how to handle reconnections: https://aiomqtt.felixboehm.dev/reconnection.html

PPS - This started because I was trying to create a class wrapper around my use of aiomqtt where the client gets created in def __init__( self ) but doesn't get used until async def run( self ) is called.

empicano commented 2 weeks ago

Hi there, thanks for the detailed issue and sorry for my late reply! 🙂

What happens is that Client.__init__ calls asyncio.get_event_loop() which returns the running event loop if it exists and the result of get_event_loop_policy().get_event_loop() when it doesn't. When we initialize the client before asyncio.run in your example, there is no running event loop yet, and we thus fail later on.

asyncio.get_event_loop() is deprecated since Python 3.12, so I've changed it to asyncio.get_running_loop(). This means that your first example will fail directly when initializing the Client. This is similar to how e.g. aiohttp handles it.


For reference, your first example will now throw RuntimeError: no running event loop. For anyone stumbling upon this issue, the solution is to put the client initialization somewhere inside async context.

Hope that helps!