forked from goodboy/tractor
Support re-entrant breakpoints
Keep an actor local (bool) flag which determines if there is already a running debugger instance for the current process. If another task tries to enter in this case, simply ignore it since allowing entry may result in a deadlock where the new task will be sync waiting on the parent stdio lock (a case that will never arrive due to the current debugger's active use of it). In the future we may want to allow FIFO queueing of local tasks where instead of ignoring re-entrant breakpoints we allow tasks to async wait for debugger release, though not sure the implications of that since you'd likely want to support switching the debugger to the new task and that could cause deadlocks where tasks are inter-dependent. It may be more sane to just error on multiple breakpoint requests within an actor.debug_tests
parent
f9ef3fc5de
commit
ebb21b9ba3
|
@ -36,8 +36,8 @@ class TractorConfig(pdbpp.DefaultConfig):
|
||||||
"""
|
"""
|
||||||
sticky_by_default = True
|
sticky_by_default = True
|
||||||
|
|
||||||
def teardown(self, _pdb):
|
def teardown(self):
|
||||||
_pdb_release_hook(_pdb)
|
_pdb_release_hook()
|
||||||
|
|
||||||
|
|
||||||
class PdbwTeardown(pdbpp.Pdb):
|
class PdbwTeardown(pdbpp.Pdb):
|
||||||
|
@ -50,11 +50,11 @@ class PdbwTeardown(pdbpp.Pdb):
|
||||||
# since that'll cause deadlock for us.
|
# since that'll cause deadlock for us.
|
||||||
def set_continue(self):
|
def set_continue(self):
|
||||||
super().set_continue()
|
super().set_continue()
|
||||||
self.config.teardown(self)
|
self.config.teardown()
|
||||||
|
|
||||||
def set_quit(self):
|
def set_quit(self):
|
||||||
super().set_quit()
|
super().set_quit()
|
||||||
self.config.teardown(self)
|
self.config.teardown()
|
||||||
|
|
||||||
|
|
||||||
# TODO: will be needed whenever we get to true remote debugging.
|
# TODO: will be needed whenever we get to true remote debugging.
|
||||||
|
@ -140,36 +140,48 @@ def _breakpoint(debug_func) -> Awaitable[None]:
|
||||||
|
|
||||||
# TODO: need a more robust check for the "root" actor
|
# TODO: need a more robust check for the "root" actor
|
||||||
if actor._parent_chan:
|
if actor._parent_chan:
|
||||||
async with tractor._portal.open_portal(
|
try:
|
||||||
actor._parent_chan,
|
async with tractor._portal.open_portal(
|
||||||
start_msg_loop=False,
|
actor._parent_chan,
|
||||||
shield=True,
|
start_msg_loop=False,
|
||||||
) as portal:
|
shield=True,
|
||||||
# with trio.fail_after(1):
|
) as portal:
|
||||||
agen = await portal.run(
|
# with trio.fail_after(1):
|
||||||
'tractor._debug',
|
agen = await portal.run(
|
||||||
'_hijack_stdin_relay_to_child',
|
'tractor._debug',
|
||||||
subactor_uid=actor.uid,
|
'_hijack_stdin_relay_to_child',
|
||||||
)
|
subactor_uid=actor.uid,
|
||||||
async with aclosing(agen):
|
)
|
||||||
async for val in agen:
|
async with aclosing(agen):
|
||||||
assert val == 'Locked'
|
async for val in agen:
|
||||||
task_status.started()
|
assert val == 'Locked'
|
||||||
with trio.CancelScope(shield=True):
|
task_status.started()
|
||||||
await do_unlock.wait()
|
with trio.CancelScope(shield=True):
|
||||||
|
await do_unlock.wait()
|
||||||
# trigger cancellation of remote stream
|
|
||||||
break
|
|
||||||
|
|
||||||
|
# trigger cancellation of remote stream
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
log.debug(f"Exiting debugger for actor {actor}")
|
||||||
|
actor.statespace['_in_debug'] = False
|
||||||
log.debug(f"Child {actor} released parent stdio lock")
|
log.debug(f"Child {actor} released parent stdio lock")
|
||||||
|
|
||||||
def unlock(_pdb):
|
|
||||||
do_unlock.set()
|
|
||||||
|
|
||||||
global _pdb_release_hook
|
|
||||||
_pdb_release_hook = unlock
|
|
||||||
|
|
||||||
async def _bp():
|
async def _bp():
|
||||||
|
"""Async breakpoint which schedules a parent stdio lock, and once complete
|
||||||
|
enters the ``pdbpp`` debugging console.
|
||||||
|
"""
|
||||||
|
in_debug = actor.statespace.setdefault('_in_debug', False)
|
||||||
|
|
||||||
|
if in_debug:
|
||||||
|
log.warning(f"Actor {actor} already has a debug lock, skipping...")
|
||||||
|
return
|
||||||
|
|
||||||
|
# assign unlock callback for debugger teardown hooks
|
||||||
|
global _pdb_release_hook
|
||||||
|
_pdb_release_hook = do_unlock.set
|
||||||
|
|
||||||
|
actor.statespace['_in_debug'] = True
|
||||||
|
|
||||||
# this **must** be awaited by the caller and is done using the
|
# this **must** be awaited by the caller and is done using the
|
||||||
# root nursery so that the debugger can continue to run without
|
# root nursery so that the debugger can continue to run without
|
||||||
# being restricted by the scope of a new task nursery.
|
# being restricted by the scope of a new task nursery.
|
||||||
|
@ -179,7 +191,8 @@ def _breakpoint(debug_func) -> Awaitable[None]:
|
||||||
# ``breakpoint()`` was awaited and begin handling stdio
|
# ``breakpoint()`` was awaited and begin handling stdio
|
||||||
debug_func(actor)
|
debug_func(actor)
|
||||||
|
|
||||||
return _bp() # user code **must** await this!
|
# user code **must** await this!
|
||||||
|
return _bp()
|
||||||
|
|
||||||
|
|
||||||
def _set_trace(actor):
|
def _set_trace(actor):
|
||||||
|
|
Loading…
Reference in New Issue