On Wed, Apr 25, 2018 at 3:17 AM, Antoine Pitrou email@example.com wrote:
On Wed, 25 Apr 2018 02:24:15 -0700 Nathaniel Smith firstname.lastname@example.org wrote:
I just posted another essay on concurrent API design:
This is the one that finally gets at the core reasons why Trio exists; I've been trying to figure out how to write it for at least a year now. I hope you like it.
My experience is indeed that something like the nursery construct would make concurrent programming much more robust in complex cases. This is a great explanation why.
API note: I would expect to be able to use it this way:
def __init__(self): self._nursery = open_nursery() # Lots of behaviour methods that can put new tasks in the nursery def close(self): self._nursery.close()
You might expect to be able to use it that way, but you can't! The 'async with' part of 'async with open_nursery()' is mandatory. This is what I mean about it forcing you to rethink things, and why I think there is room for genuine controversy :-). (Just like there was about goto -- it's weird to think that it could have turned out differently in hindsight, but people really did have valid concerns...)
I think the pattern we're settling on for this particular case is:
class MyEndpoint: def __init__(self, nursery, ...): self._nursery = nursery # methods here that use nursery
@asynccontextmanager async def open_my_endpoint(...): async with trio.open_nursery() as nursery: yield MyEndpoint(nursery, ...)
Then most end-users do 'async with open_my_endpoint() as endpoint:' and then use the 'endpoint' object inside the block; or if you have some special reason why you need to have multiple endpoints in the same nursery (e.g. you have an unbounded number of endpoints and don't want to have to somehow write an unbounded number of 'async with' blocks in your source code), then you can call MyEndpoint() directly and pass an explicit nursery. A little bit of extra fuss, but not too much.
So that's how you handle it. Why do we make you jump through these hoops?
The problem is, we want to enforce that each nursery object's lifetime is bound to the lifetime of a calling frame. The point of the 'async with' in 'async with open_nursery()' is to perform this binding. To reduce errors, open_nursery() doesn't even return a nursery object – only open_nursery().__aenter__() does that. Otherwise, if a task in the nursery has an unhandled error, we have nowhere to report it (among other issues).
Of course this is Python, so you can always do gross hacks like calling __aenter__ yourself, but then you're responsible for making sure the context manager semantics are respected. In most systems you'd expect this kind of thing to syntactically enforced as part of the language; it's actually pretty amazing that Trio is able to makes things work as well as it can as a "mere library". It's really a testament to how much thought has been put into Python -- other languages don't really have any equivalent to with or Python's generator-based async/await.
Also perhaps more finegrained shutdown routines such as:
wait for all tasks to join, cancel the remaining ones after the given timeout
Hmm, I've never needed that particular pattern, but it's actually pretty easy to express. I didn't go into it in this writeup, but: because nurseries need to be able to cancel their contents in order to unwind the stack during exception propagation, they need to enclose their contents in a cancel scope. And since they have this cancel scope anyway, we expose it on the nursery object. And cancel scopes allow you to adjust their deadline. So if you write:
async with trio.open_nursery() as nursery: ... blah blah ... # Last line before exiting the block and triggering the implicit join(): nursery.cancel_scope.deadline = trio.current_time() + TIMEOUT
then it'll give you the semantics you're asking about. There could be more sugar for this if it turns out to be useful. Maybe a .timeout attribute on cancel scopes that's a magic property always equal to (self.deadline - trio.current_time()), so you could do 'nursery.cancel_scope.timeout = TIMEOUT'?