It's an interesting problem. I would like to rephrase your conclusion:
the implicit loop should only be used when the object you are creating
has a shorter (or equal) lifetime than the loop. I would also think
that the real problem with the code example is that it creates a
variable with global lifetime. That's really an antipattern,
especially when it comes to network connections. But I'm not sure we
can blame asyncio or motor for that -- it's really class A that's to
blame.

I would suggest different guidelines for libraries than for
applications: Libraries should be robust and always store their own
loop. This is how asyncio itself works and how aiohttp (and other
libraries you name) work. Their test suite (like asyncio's) should
enforce this.

But applications (and fledgling libraries may do this too) should be
allowed to assume a simpler model of the world: use coroutines (async
def, await) for everything, never store the loop, happily use
get_event_loop() when you really need it. And really, you should only
need it for run_in_executor(). If you find yourself using call_later()
you probably haven't quite figured out how to use coroutines properly.

Regarding testability of class A, I presume tests could just set a new
default loop and then re-assign A.client =
motor.motor_asyncio.AsyncIOMotorClient()? Or is that class under the
hood a singleton that just wraps more state with global lifetime? Then
that would be a problem.

There are always exceptions to such rules, but they should all fall in
the category of advanced usage, guarding against rare failures,
"productionizing" (which I would recommend against doing in too early
a stage).

--Guido

On Mon, Oct 31, 2016 at 8:16 AM, Andrew Svetlov
<andrew.svet...@gmail.com> wrote:
> Hi.
>
> As people know I prefer explicit asyncio loop and pass it everywhere.
> That's how I build libraries like aiohttp and aiopg, do writing private
> source code.
> I promote this way of code writing on conferences etc.
> Push `asyncio.set_event_loop(None)` at very begin of your code to avoid
> mysterious bugs!
>
> But I feel temptation of using implicit loop -- and users of my libraries do
> it very often.
>
> Now, after four years of working with asyncio I almost agree with it -- if
> implicit loop is used *from coroutine*.
>
> But outside of *task context* (every asyncio coroutine is executed by a
> task, you know) it's still dangerous sometimes.
>
> Let's take a close look on the following example:
>
>     import motor.motor_asyncio
>
>     class A:
>         client = motor.motor_asyncio.AsyncIOMotorClient()
>
> What's wrong with it? The code is untestable. AsyncIOMotorClient accepts
> is_loop parameter which should be event loop instance or None. If the
> parameter is None (as in our case) default event loop will be given.
>
> Then we run a test suite. Every test should have own loop for sake of test
> isolation. In practice it means that every test tool creates an event loop,
> runs test with it and closes the loop after test finishing. It's very true
> strategy for testing async code, I know no alternatives.
> But our client was coupled with *default* loop created on module import
> stage.
> As a consequence `await client['db'].find(...)` call will be never finished
> because client's loop is not iterated by test runner.
>
> It's not specific Motor's (async MongoDB client for Torrnado and Asyncio)
> problem -- aioes.Client, aiohttp.ClientSession and many others does the
> same.
>
> In opposite aiopg, asyncpg and aiomysql have no this problem -- their DB
> connections are created by `await connect(params)` coroutine call. `await`
> cannot be used in global/class namespace, thus user will always call it from
> task with proper loop.
>
> Sure, I could forbid aiohttp.ClientSession creation without current task and
> make usage of this particular class safe (it will break backward
> compatibility for inaccurate users but better to break code early, right).
>
> Asyncio could introduce `asyncio.get_current_loop()` function which will
> raise exception if implicit loop exists but there is no running task. After
> that third-party libraries may switch to this function in any case where the
> loop is required. asyncio itself could do the same.
>
> At the end.
> Accessing implicit event loop is very handy but it's safe only inside
> running task.
> Getting the loop in global namespace may lead to very strange and cryptic
> errors (most likely the next usage will just hang).
>
> Thoughts?



-- 
--Guido van Rossum (python.org/~guido)

Reply via email to