goodboy / tractor

A distributed, structured concurrent runtime for Python (and friends)
GNU Affero General Public License v3.0
270 stars 12 forks source link

`IPython` integration 😎 #306

Open goodboy opened 2 years ago

goodboy commented 2 years ago

Like it sounds, with more refinements to come!

This is a POC and seems to do about what I'd like; would appreciate lurker feedback 😉

ping @mikenerone, original author of the base gists for this:

TODO:

goodboy commented 1 year ago

Turns out there might be a better solution for what we actually want (await <blah> inside any REPL, but mostly our debugger UX).


from @smurfix on gitter:

import trio
import greenback as gb
AW=gb.await_

async def wat():
    await trio.sleep(0.1)
    return "yes"

async def run():
    await gb.ensure_portal()
    breakpoint()
    pass

trio.run(run)

which can be called from repl like:

(Pdb) AW(wat())
'yes'

with follow up from @oremanj actually using pdb++ 💥 :

$ python3.8 -c "import trio, pdb, greenback; trio.run(greenback.with_portal_run_sync, lambda: breakpoint())"
(Pdb++) greenback.await_(trio.sleep(1))
<1sec delay>

also a further extended example 🏄🏼

$ python3.8 -c "import trio, greenback, code; trio.run(greenback.with_portal_run_sync, code.interact)"
>>> import trio
>>> from greenback import await_ as aw, async_context as acm
>>> async def task(interval, msg):
...   while True:
...     await trio.sleep(interval)
...     print(msg)
...
>>> with acm(trio.open_nursery()) as nursery:
...   nursery.start_soon(task, 2, "every two seconds")
...   nursery.start_soon(task, 0.75, "every 3/4 second")
...   aw(task(1.7, "every 1.7 seconds"))
...
every 3/4 second
every 3/4 second
every 1.7 seconds
every two seconds
every 3/4 second
every 3/4 second
every 1.7 seconds
every 3/4 second
every two seconds
^CTraceback (most recent call last):
  File "/usr/lib/python3.8/code.py", line 90, in runcode
    exec(code, self.locals)
  File "<console>", line 4, in <module>
  File "/usr/lib/python3.8/site-packages/greenback/_impl.py", line 653, in await_
    raise exception_from_greenbacked_function
  File "<console>", line 3, in task
  File "/usr/lib/python3.8/site-packages/trio/_timeouts.py", line 75, in sleep
    await sleep_until(trio.current_time() + seconds)
  File "/usr/lib/python3.8/site-packages/trio/_timeouts.py", line 56, in sleep_until
    await sleep_forever()
  File "/usr/lib/python3.8/site-packages/trio/_timeouts.py", line 40, in sleep_forever
    await trio.lowlevel.wait_task_rescheduled(lambda _: trio.lowlevel.Abort.SUCCEEDED)
  File "/usr/lib/python3.8/site-packages/trio/_core/_traps.py", line 166, in wait_task_rescheduled
    return (await _async_yield(WaitTaskRescheduled(abort_func))).unwrap()
  File "/usr/lib/python3.8/site-packages/outcome/_sync.py", line 111, in unwrap
    raise captured_error
  File "/usr/lib/python3.8/site-packages/trio/_core/_run.py", line 1178, in raise_cancel
    raise KeyboardInterrupt
KeyboardInterrupt
>>>
smurfix commented 1 year ago

To make this work anywhere we should teach Trio's task creation code to unconditionally create a Greenback portal in every new task; otherwise debugging at random breakpoints won't work.

goodboy commented 1 year ago

@smurfix agreed, this is already an issue with debugging single task crashes within a nursery as well.

Currently on a task crash, you can't breakpoint() at the (specific task's) error stack frame and instead always end up at the nursery exit; so, an extended nursery is needed for both of these cases:

Further, this probably plays best with the idea of a OCONursery (one-cancels-one) style nursery where every task has an associated individual cancel scope that can be managed independently of other tasks such that respawns can happen on a per-task-failure scenario.