Simplify all hooks to a common `Lock.release()`

signint_saviour
Tyler Goodlet 2022-08-02 18:14:05 -04:00
parent 65540f3e2a
commit 8f1fe2376a
1 changed files with 43 additions and 59 deletions

View File

@ -68,7 +68,7 @@ class Lock:
'''
# placeholder for function to set a ``trio.Event`` on debugger exit
pdb_release_hook: Optional[Callable] = None
# pdb_release_hook: Optional[Callable] = None
# actor-wide variable pointing to current task name using debugger
local_task_in_debug: Optional[str] = None
@ -108,13 +108,7 @@ class Lock:
cls._orig_sigint_handler = None
@classmethod
def maybe_release(cls):
cls.local_task_in_debug = None
if cls.pdb_release_hook:
cls.pdb_release_hook()
@classmethod
def root_release(cls):
def release(cls):
try:
cls._debug_lock.release()
except RuntimeError:
@ -125,6 +119,7 @@ class Lock:
if owner:
raise
# actor-local state, irrelevant for non-root.
cls.global_actor_in_debug = None
cls.local_task_in_debug = None
@ -165,17 +160,17 @@ class MultiActorPdb(pdbpp.Pdb):
try:
super().set_continue()
finally:
Lock.maybe_release()
Lock.release()
def set_quit(self):
try:
super().set_quit()
finally:
Lock.maybe_release()
Lock.release()
@acm
async def _acquire_debug_lock(
async def _acquire_debug_lock_from_root_task(
uid: Tuple[str, str]
) -> AsyncIterator[trio.StrictFIFOLock]:
@ -217,7 +212,7 @@ async def _acquire_debug_lock(
# IF we received a cancel during the shielded lock entry of some
# next-in-queue requesting task, then the resumption here will
# result in that ``trio.Cancelled`` being raised to our caller
# (likely from ``_hijack_stdin_for_child()`` below)! In
# (likely from ``lock_tty_for_child()`` below)! In
# this case the ``finally:`` below should trigger and the
# surrounding caller side context should cancel normally
# relaying back to the caller.
@ -225,8 +220,6 @@ async def _acquire_debug_lock(
yield Lock._debug_lock
finally:
# if Lock.global_actor_in_debug == uid:
if (
we_acquired
and Lock._debug_lock.locked()
@ -255,20 +248,21 @@ async def _acquire_debug_lock(
@tractor.context
async def _hijack_stdin_for_child(
async def lock_tty_for_child(
ctx: tractor.Context,
subactor_uid: Tuple[str, str]
) -> str:
'''
Hijack the tty in the root process of an actor tree such that
the pdbpp debugger console can be allocated to a sub-actor for repl
bossing.
Lock the TTY in the root process of an actor tree in a new
inter-actor-context-task such that the ``pdbpp`` debugger console
can be mutex-allocated to the calling sub-actor for REPL control
without interference by other processes / threads.
NOTE: this task is invoked in the root actor-process of the actor
tree. It is meant to be invoked as an rpc-task which should be
highly reliable at cleaning out the tty-lock state when complete!
NOTE: this task must be invoked in the root process of the actor
tree. It is meant to be invoked as an rpc-task and should be
highly reliable at releasing the mutex complete!
'''
task_name = trio.lowlevel.current_task().name
@ -288,7 +282,7 @@ async def _hijack_stdin_for_child(
with (
trio.CancelScope(shield=True),
):
async with _acquire_debug_lock(subactor_uid): # as lock:
async with _acquire_debug_lock_from_root_task(subactor_uid):
# indicate to child that we've locked stdio
await ctx.started('Locked')
@ -311,14 +305,17 @@ async def wait_for_parent_stdin_hijack(
task_status: TaskStatus[trio.CancelScope] = trio.TASK_STATUS_IGNORED
):
'''
Connect to the root actor via a ctx and invoke a task which locks
a root-local TTY lock.
Connect to the root actor via a ``Context`` and invoke a task which
locks a root-local TTY lock: ``lock_tty_for_child()``; this func
should be called in a new task from a child actor **and never the
root*.
This function is used by any sub-actor to acquire mutex access to
pdb and the root's TTY for interactive debugging (see below inside
``_breakpoint()``). It can be used to ensure that an intermediate
nursery-owning actor does not clobber its children if they are in
debug (see below inside ``maybe_wait_for_debugger()``).
the ``pdb`` REPL and thus the root's TTY for interactive debugging
(see below inside ``_breakpoint()``). It can be used to ensure that
an intermediate nursery-owning actor does not clobber its children
if they are in debug (see below inside
``maybe_wait_for_debugger()``).
'''
with trio.CancelScope(shield=True) as cs:
@ -330,7 +327,7 @@ async def wait_for_parent_stdin_hijack(
# this syncs to child's ``Context.started()`` call.
async with portal.open_context(
tractor._debug._hijack_stdin_for_child,
tractor._debug.lock_tty_for_child,
subactor_uid=actor_uid,
) as (ctx, val):
@ -390,8 +387,8 @@ async def _breakpoint(
) -> None:
'''
breakpoint entry for engaging pdb machinery in the root or
a subactor.
Breakpoint entry for engaging debugger instance sync-interaction,
from async code, executing in actor runtime (task).
'''
__tracebackhide__ = True
@ -411,12 +408,17 @@ async def _breakpoint(
Lock.local_pdb_complete = trio.Event()
# TODO: need a more robust check for the "root" actor
if actor._parent_chan and not is_root_process():
if (
not is_root_process()
and actor._parent_chan # a connected child
):
if Lock.local_task_in_debug:
# Recurrence entry case: this task already has the lock and
# is likely recurrently entering a breakpoint
if Lock.local_task_in_debug == task_name:
# this task already has the lock and is
# likely recurrently entering a breakpoint
# noop on recurrent entry case
return
# if **this** actor is already in debug mode block here
@ -431,20 +433,6 @@ async def _breakpoint(
# entries/requests to the root process
Lock.local_task_in_debug = task_name
def child_release():
try:
# sometimes the ``trio`` might already be termianated in
# which case this call will raise.
Lock.local_pdb_complete.set()
finally:
# restore original sigint handler
undo_sigint()
# should always be cleared in the hijack hook aboved right?
# _local_task_in_debug = None
# assign unlock callback for debugger teardown hooks
Lock.pdb_release_hook = child_release
# this **must** be awaited by the caller and is done using the
# root nursery so that the debugger can continue to run without
# being restricted by the scope of a new task nursery.
@ -460,7 +448,7 @@ async def _breakpoint(
actor.uid,
)
except RuntimeError:
Lock.pdb_release_hook()
Lock.release()
raise
elif is_root_process():
@ -468,8 +456,8 @@ async def _breakpoint(
# we also wait in the root-parent for any child that
# may have the tty locked prior
# TODO: wait, what about multiple root tasks acquiring it though?
# root process (us) already has it; ignore
if Lock.global_actor_in_debug == actor.uid:
# re-entrant root process already has it: noop.
return
# XXX: since we need to enter pdb synchronously below,
@ -491,9 +479,6 @@ async def _breakpoint(
Lock.global_actor_in_debug = actor.uid
Lock.local_task_in_debug = task_name
# the lock must be released on pdb completion
Lock.pdb_release_hook = Lock.root_release
try:
# block here one (at the appropriate frame *up*) where
# ``breakpoint()`` was awaited and begin handling stdio.
@ -501,7 +486,7 @@ async def _breakpoint(
debug_func(actor, pdb)
except bdb.BdbQuit:
Lock.maybe_release()
Lock.release()
raise
# XXX: apparently we can't do this without showing this frame
@ -597,9 +582,8 @@ def shield_sigint(
)
# child actor that has locked the debugger
elif (
not is_root_process()
):
elif not is_root_process():
chan: Channel = actor._parent_chan
if not chan or not chan.connected():
log.warning(
@ -738,7 +722,7 @@ async def _maybe_enter_pm(err):
):
log.debug("Actor crashed, entering debug mode")
await post_mortem()
Lock.maybe_release()
Lock.release()
return True
else:
@ -754,7 +738,7 @@ async def acquire_debug_lock(
This helper is for actor's who don't actually need
to acquired the debugger but want to wait until the
lock is free in the tree root.
lock is free in the process-tree root.
'''
if not debug_mode():