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
Tyler Goodlet 2020-08-01 13:39:05 -04:00
parent f9ef3fc5de
commit ebb21b9ba3
1 changed files with 44 additions and 31 deletions

View File

@ -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,6 +140,7 @@ 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:
try:
async with tractor._portal.open_portal( async with tractor._portal.open_portal(
actor._parent_chan, actor._parent_chan,
start_msg_loop=False, start_msg_loop=False,
@ -160,16 +161,27 @@ def _breakpoint(debug_func) -> Awaitable[None]:
# trigger cancellation of remote stream # trigger cancellation of remote stream
break 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):