-
-
Notifications
You must be signed in to change notification settings - Fork 38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
asyncio loop patching fails on 3.7 #23
Comments
Oh ick, it's very unfortunate we missed this :-(. That's a real problem in our testing... it looks like currently Travis is testing 3.7-dev, but Travis's normal idea of "3.7-dev" is some snapshot from January, which may predate this change, and if you want to actually test 3.7 or 3.8-dev then you need to jump through some poorly-documented hoops. We should do that pretty urgently, given how dependent this project is on asyncio development. As for what to do now... I'm actually not sure what our options are. Going to CC @1st1. The problem is that we need to somehow override We could...
Yury, any thoughts? |
Gaah. Sorry that I entirely missed this. Travis needs convincing that updating this kind of critical testing infrastructure more often is necessary. Personally I'm all for the third solution: Store the current asyncio loop in a contextvar. Could somebody look into creating a patch for this? I'm unfortunately swamped with (a) work (b) craftsmen (c) sorting my partner's belongings into my home (d) more work … |
First we'd want to find out whether Yury would be willing to accept such a patch :-) |
That is the first step of "looking into". :-P |
I think I need more details to understand the problem you're trying to solve here. BTW, have you tried using
I tried storing the current event loop in a ContextVar and it didn't work. We'd have to add ContextVar on top of the current code and that's a lot of extra complexity. Besides, I'd be cautious about exposing some internal context var as part of asyncio public API. |
Short: we're re-implementing the asyncio mainloop on top of Trio. Thus when we run a callback (including a Future's completion routine) we need to restore asyncio's idea of the current event loop ourselves. The sensible way to do this is via contextvars, so I store the current loop in the current Trio task's context and monkeypatch
Trio-asyncio also monkeypatches
Why not?
Umm, well, the contextvar wouldn't be exposed – it'd be carried over seamlessly as part of the contextvar support in Trio and Trio-asyncio. We'd set it once, via |
@1st1 We're implementing an asyncio event loop. Our loop object is associated with a specific Trio nursery, that it uses to schedule callbacks and so forth. For code that's running inside this nursery, we want And then maybe we have another nursery somewhere else, inside another task, and it has a different asyncio event loop associated with it... and for code inside this nursery, we want And for code that's inside trio, but not inside any asyncio event loop, we'd really prefer that async def function_that_uses_an_asyncio_library():
async with trio_asyncio.open_loop() as loop:
assert asyncio.get_event_loop() is loop
async def main():
async with trio.open_nursery() as nursery:
# running two of those at the same time, both assertions should pass
# (even though the loop objects are different)
nursery.start_soon(function_that_uses_an_asyncio_library) In asyncio, the event loop is a global thing, shared across the whole program, and the event loop lets you spawn background tasks. But Trio's whole thing is that background tasks can't be globally scoped. How can those be reconciled? Well, this is a compromise: we make the event loop "global" within a branch of the task tree, so from the inside it feels like asyncio, but from the outside we're still enforcing trio's normal restrictions. |
Wasn't it the case before #13 that trio-asyncio had its own EventLoopPolicy? Would it be a solution to bring that back (but maybe require to explicitly install it rather than automatically on import? |
Well I mean, #13 was that messing with the |
No, though that's rather easy to change. (5b6496d (untested)) |
We can certainly try whether reverting #13 would help here. That would imply that we stop supporting any asyncio>trio usecases on Python >3.6, but as NB: Are |
The new tests are probably going to fail horribly because of python-triogh-23
The new tests are probably going to fail horribly because of python-triogh-23
This issue prompted me to go back and review why we want I think there are two reasons: Reason 1 is: originally, we were fumbling around trying to figure out how seamlessly we could get trio and asyncio code to work together, and even considering whether we could make it totally seamless (i.e. supporting straight-up Reason 2 is: we were originally thinking about converting code like: loop = asyncio.get_event_loop()
loop.run_until_complete(some_fn())
loop.run_until_complete(another_fn())
loop.run_forever() So we were imagining you'd be doing on-trivial work with the asyncio loop, from outside the asyncio context. And in particular, These two things – that we've moved more towards a strong trio-mode vs asyncio-mode separation, and that asyncio has moved more towards (@smurfix I guess this question is mostly for you, since you're probably the person who has the most actual experience using trio-asyncio, while I am just speculating from my armchair over here.) The main thing that worries me here is the question of when loops are created versus reused. Right now it's very clear: Like, if you do I suppose creating a new loop would be the most consistent. But you do need a way to re-use the outer loop, for when the Trio code is a plugin inside a larger asyncio program and you need to call back into that larger program, so if we did it this way we'd need some way to re-use the existing loop as well. Maybe Or, we could make it so the inner Actually "traiohttp" is kind of an interesting case... you probably use it like: # This is literally just aiohttp's API
async with traiohttp.ClientSession() as session:
async with session.get(url) as response:
print(response.text()) The interesting thing here is that we're doing multiple operations, on multiple different objects, that all have to run on the same asyncio loop. But traiohttp wants to masquerade as a plain ordinary trio library that only happens to use trio-asyncio as an invisible internal implementation detail. To do this it will actually have to create a loop object at the same time the In fact, it doesn't even need a "loop" object in the sense of asyncio – the only operation it needs is # Explicitly opening a portal and using it:
async with trio_asyncio.open_asyncio_portal() as asyncio_portal:
...
# Convenience function, similar to asyncio.run
async def run_in_asyncio(fn, *args):
async with trio_asyncio.open_asyncio_portal() as asyncio_portal:
return await asyncio_portal.run(fn, *args)
# And there's a global trio_asyncio.run_in_trio for getting back into trio
# Because there's only one Trio state, we don't need to keep track of multiple loop objects
# And trio's API is already composable, with no globals or causality violation, so basically
# introducing a loop object wouldn't do anything.
await asyncio_trio.run_in_trio(fn, *args) Does that logic make sense? And, @smurfix , @miracle2k , anyone else who's tried using trio_asyncio for solving actual problems... how convenient or burdensome would this API be for your code? |
In most use cases you're not simply calling one Thus, creating a new loop in As to passing the loop explicitly vs. storing it in the context: usually you don't get to decide what arguments your callback gets, so you need to store the loop somewhere. The context's advantage is that you can't access a dead loop – when the context exits, the loop's gone, no chance of re-using it by accident. |
@smurfix Hmm, ok, I hear that. And what do you think about calling sync asyncio APIs from trio-mode code – is that something you end up doing in practice? Actually, I think I want to split that into two questions: (1) do you call loop methods directly? (2) do you call functions that implicitly do Can you point me to any repos with ordinary everyday trio-asyncio code in them, like your homeassistant plugins or whatever? (Sorry for all the questions :-)) |
Well, as soon as you're in sync code there's no longer any visible distinction between trio and asyncio modes, so why bother? Switching into asyncio is somewhat expensive (queue the call, wait for the asyncio mainloop task to process it), you wouldn't want to do that for sync calls if it can be avoided. |
@smurfix But does it come up that you're in trio mode and then are like "oh, I need to call this asyncio API that happens to be synchronous", or does that mostly only happen when you're already in asyncio mode? And the reason to bother is, well, this issue: currently on 3.7 we do not have any way to support calling synchronous asyncio APIs when we're in trio mode, and we need to decide what to do about that. Restoring that functionality might only be possible with heroic efforts. So I'm trying to figure out how important it is. |
@njsmith Suppose you need to talk to an asyncio protocol. So you override the dataReveived callback to queue the data to to your Trio task, that's easy and reasonably cheap. You then need to send something back, so you call yourProtocol..transport.send() … oops. In actual usage, this is a not that big an issue because self-respecting protocols save their loop when they're set up and pass it to anything that might need it. At setup time the loop variable should be easily accessible. in other words, just pass the loop into the future-or-whatever and things work again:
IMHO the way forward is to remove compatibility mode (or at least the guarantee thereof), i.e. force people to us If that still causes problems, well, teaching people to pass the loop around in Trio mode is not too much of a burden – you only need it when setting things up from Trio code (always assuming that the |
If we made it so that either this worked (if
That was the original way asyncio was supposed to work. Then experience (and critiques from curio and trio!) convinced them that it was a bad approach, and now they've moved much more strongly towards implicit loop access... plus, well, not every library is well-written, but people still want to use them. So I think we shouldn't count on asyncio libraries tracking their loops manually, and I definitely don't want to end up telling people "you can call most sync-colored APIs, except the ones you can't, but probably you won't run into those very often, so don't worry about it".
That's fine with me – as far as I'm concerned the only reason we ever supported working But I don't think this next bit is right :-(
When you call
The problem is that neither of this is actually what we want. And sure enough, when we tried that, someone filed a bug (#13) saying that they were trying to run vanilla-asyncio or uvloop-asyncio in one thread and trio-asyncio in a second thread and the event loop policies were colliding and breaking stuff. We could reduce these issues by being cleverer. For example, we could wait until the first time someone creates a trio-asyncio loop to install our policy, and we could make sure that when our policy detects that it's being called outside of the trio-asyncio thread, then it defers to whatever policy the user set before that. That might even be good enough to get us through the 3.7 cycle, until we could get something better? But it's still going to create really arcane frustrating bugs. |
I haven't followed the discussion in detail, sorry, but I just wanted to say that there's zero chance of us changing how |
@1st1 That kindof goes without saying. I do wonder, though, whether there's a reason the loop policy isn't thread-specific, besides the obvious answer of "we didn't think there'd be a usecase for that"? |
I suggest you to soften your communication style. It really comes out as unnecessarily snarky and makes me unmotivated to continue the discussion here. |
The obvious answer is that asyncio wasn't designed to work in multithreaded environments the way Trio wants it to. And IIRC Guido didn't like the idea of using many event loops in multiple OS threads in one process. The API was explicitly and consciously designed to use a single global asyncio policy for the entire process, as managing many of them in multiple threads manually would be too cumbersome for pure asyncio programs. It's also the first time I hear this feature to be proposed (which by itself doesn't mean that the idea is bad, it only means that it's not that common). The ship for making policies thread-specific has sailed though, so we'll have to work around the existing API and its limitations. I understand that Trio wants to have a very robust and unbreakable mechanism, but, unfortunately, I don't see how we can do that besides monkey-patching asyncio's |
On 30.07.2018 17:50, Yury Selivanov wrote:
@1st1 <https://github.com/1st1> That kindof goes without saying.
I do wonder, though, whether there's a reason the loop policy
isn't thread-specific, besides the obvious answer of "we didn't
think there'd be a usecase for that"?
I suggest you to soften your communication style. It really comes out
as unnecessarily snarky and makes me unmotivated to continue the
discussion here.
I am sorry. That wasn't intentional – in fact I have been on the
receiving end of that sort of comment often enough to take this sort of
thing with a large grain of levity – and my awareness of the fact that
other people might perceive differently is, unfortunately, somewhat limited.
…--
-- Matthias Urlichs
|
NP, I think I actually overreacted a bit. FWIW I'm super interested in Trio and pretty open to fixing asyncio when there's a clear win for both projects. So please continue the discussion, I'm trying to follow it. |
That's actually an interesting idea I hadn't considered. We could make our own "smart" loop policy that checks whether it's in a trio thread, and if not then passes on the request to whatever policy the user had registered. And then we could monkeypatch It'd still probably be good to get out of this monkeypatching business eventually, but that could restore current functionality on 3.7, at least. |
0.8.0 implements a per-thread loop policy. This should allow trio-asyncio (in thread A) to interoperate seamlessly with pure asyncio (thread B) and/or uvloop (thread C). Getting out of the monkeypatching business requires asyncio support: we'd need a per-thread policy. |
...Maybe asyncio should work this way, but it still seems like a surprise that trio-asyncio would make asyncio's loop policy thread-local? And unnecessary for our purposes...? |
done @ v0.8.2 |
says:
This seems to be related to stuff now written in C. A naked
Future()
call does not pick up the trio loop.If in asyncio/futures.py I go to the end and change this code:
to make sure
_asyncio
is not imported, my program works.The text was updated successfully, but these errors were encountered: