-
-
Notifications
You must be signed in to change notification settings - Fork 344
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
Should we have a conventional way to "spawn a process into a nursery"? #1104
Comments
I know I've proposed some other options in the past, but currently I like |
#1109 affects this some – first because if we end up needing an |
I propose filling out docs for "Interacting with a process as it runs" with some examples to give users a sane starting point. This would have saved me many hours. From the experience I had, the most important pieces are 1) don't defer parent exceptions while the subprocess is running, and 2) have the parent do something (e.g. exit) when the subprocess exits unexpectedly. Roughly: async with trio.Process(['my_executable']) as proc,
trio.open_nursery() as nursery:
try:
@nursery.start_soon
async def _subprocess_monitor():
# do something (abort shown here) if subprocess exits unexpectedly
if await proc.wait() != 0:
logger.error("subprocess failed")
sys.exit(1)
# code for subprocess interaction goes here...
except BaseException:
# on local error, terminate subprocess to avoid it blocking task exit
proc.terminate() |
Another example of a real user being confused by |
Sorry for being suuuuuper late to respond to the mention (I got a new puppy! She's adorable! It's a lot of work! etc etc). But I just wanted to hop in and say two quick things. First and foremost:
is a solid encapsulation of the root of the discussion! 🎉 Second: I'm not sure that
is entirely correct. I mean, you certainly can do this, but sometimes you want nursery semantics and return values, and then you end up wrapping things with something like this: import functools
import trio
class BoundTrioPartial:
def __init__(self, coro, *args, **kwargs):
self.return_flag = trio.Event()
@functools.wraps(coro)
async def wrapper():
self._return_value = await coro(*args, **kwargs)
self.return_flag.set()
self.wrapper = wrapper
def __call__(self):
return self.wrapper()
async def get_return_value(self):
await self.return_flag.wait()
return self._return_value to be used something like this: async def outer():
async with trio.open_nursery() as nursery:
foo = BoundTrioPartial(inner, foo='foo')
bar = BoundTrioPartial(inner, bar='bar')
nursery.start_soon(trio.sleep, 1)
nursery.start_soon(foo)
nursery.start_soon(bar)
foo_result = await foo.get_return_value()
print(f'foo result: {foo_result}')
bar_result = await bar.get_return_value()
print(f'bar result: {bar_result}')
async def inner(*args, **kwargs):
return (args, kwargs) which, as you'd expect, works like this: In [25]: trio.run(test)
foo result: ((), {'foo': 'foo'})
bar result: ((), {'bar': 'bar'}) if the answer to the question posed in the issue title is "Yes", (ie, if this gets implemented) then for the sake of consistency I think it would be nice for the solution to also support those kinds of patterns. |
I see how that's something you want to be able to do in general, but is that something you want for The intermediate result from |
That's a good point! Just having the |
Yes I know, I can always start a task to capture stderr separately, but having that built-in would still be helpful IMHO. |
This ties in nice with discourse #189 Trio with pexpect (gatttool). If I understand correctly, expect and pexpect (python version of expect) is a fancy way to have interactive conversation with a spawned process. You can send_cmd() and expect(keyStr) answers. the interprocess communications happen behind that expect(keyStr) that fills buffers Before, and After() the keyStr requested. (you can also run() a process which lets it run to completion and then returns result.) https://kite.com/python/docs/pexpect |
Thinking about this more: the current So maybe a good plan would be:
|
As part of this, one of the questions we have to answer is how to propagate cancellation into a subprocess for One idea: let the user provide an arbitrary async function that's invoked if the subprocess gets cancelled. The semantics would be:
Then our default implementation could be something like: async def default_process_cancel(process):
try:
process.send_signal(signal.SIGTERM)
await trio.sleep(5)
warnings.warn(RuntimeWarning("process ignored SIGTERM for 5 seconds, escalating to SIGKILL"))
process.send_signal(signal.SIGKILL)
except OSError:
warnings.warn(RuntimeWarning("can't kill process!")) Usually, the And if users wanted to tweak this somehow (close stdin? change the sleep value? ...), then that's easy to do – I think this formalism makes it quite easy to implement complex cancellation logic. #1209 also has an example where a user wanted a custom way to cancel subprocesses. |
The customizable cancellation behavior was added in #1525. Over in #1521, we're talking about adding a concept of a "service task". The reason this is relevant here, is that if we decide to go with Cross-ref: #1521 (comment) |
Quoting from upthread, the arguments against giving
Yep, it is kinda weird.
On further thought, I'm not convinced this is true. If a process exits with a non-zero result, and
This was already defunct, because of #1109. So really the only issue here is how "natural" it feels, and I dunno... making |
...Though in an interesting example of how every design question is related to every other design question: if we do go with |
To |
We have
run_process
, which is a convenient way to run a process like it's a regular subroutine, with error checking (check=
), cancellation support, automatic waiting, etc. And we havetrio.Process
, which is a fully general way to spawn a process and then do whatever with it.In the discussion in #872, @Badg brought up some dissatisfaction with both of these options. I think I mostly get the issue now, so I'll try to rephrase in my own words:
In some ways, when a task spawns a subprocess it's very similar to spawning a new task: the subprocess is a chunk of code that runs simultaneously with the Trio task, and they can potentially interact.
Trio has some strong and convenient conventions for spawning tasks:
It would be nice to be able to get this combination of features for subprocesses too. But we don't currently have any simple way to do that.
The closest thing we have is to use
async with
on aProcess
object. But that's stuck between two conflicting sets of conventions. TheProcess
API mostly treats processes as independent objects, and ourasync with process
mostly follows the general conventions forasync with <resource>
: it never cancels the body of thewith
(even if the process crashes). It doesn't care whether the body of thewith
block completed with an exception or not. And it doesn't do any particular checks on the subprocess's return code. This is also reasonable and what you'd expect for like,with file_obj
. OTOH, the conventions above are specific to how Trio handles code, and really only show up with nurseries and nursery-like objects. Which aProcess
... kind of is, and kind of isn't.I don't think it makes sense to try to wedge all those nursery semantics into
async with process_obj
. In particular, it would be very weird forasync with process_obj
to suddenly enable the equivalent ofrun_process
'scheck=True
. And then we'd need some way to allow it to be overridden withcheck=False
on a case-by-case basis, and there's no obvious way to spell that. And if we usecheck=True
and the subprocess fails and the body fails, do we raise aMultiError
? In multierror v2, does that mean we need to wrap all outgoing exceptions inMultiError
? It would involve a lot of weirdness.Another idea that's been proposed is to extend
run_process
so that it can be used like:This has several attractive aspects:
run_process
's job is to "domesticate" a subprocess so it acts like a subroutine – for example, thecheck=
argument translates error-reporting from subprocess-style to subroutine-style. So here we're reusing that, plus a nursery's general ability to take any subroutine and push it off into a parallel task. You get nursery semantics because you used a nursery. Elegant!It's also kinda weird. It's effectively a totally different 'mode' of using
run_process
: normallyrun_process
treats theprocess_obj
as hidden, and the return value as important; this flips that around completely, so you can mess with theprocess_obj
, and never see the return value. Normally, thecapture_*
arguments are a major part of why people userun_process
; here, they're completely useless, and would probably need to be forbidden. The use ofstart
is sorta gratuitous:the actual process startup is synchronous, so you could just as well have a synchronous version. The(never mind, see #1109)start
is just a trick to start a task and get a return value at the same time.Are there other options to consider? Brainstorming:
The text was updated successfully, but these errors were encountered: