forked from goodboy/tractor
1
0
Fork 0

Fix `.devx.maybe_wait_for_debugger()` polling deats

When entered by the root actor avoid excessive polling cycles by,
- blocking on the `Lock.no_remote_has_tty: trio.Event` and breaking
  *immediately* when set (though we should really also lock
  it from the root right?) to avoid extra loops..
- shielding the `await trio.sleep(poll_delay)` call to avoid any local
  cancellation causing the (presumably root-actor task) caller to move
  on (possibly to cancel its children) and instead to continue
  poll-blocking until the lock is actually released by its user.
- `break` the poll loop immediately if no remote locker is detected.
- use `.pdb()` level for reporting lock state changes.

Also add a #TODO to handle calls by non-root actors as it pertains to
modden_spawn_from_client_req
Tyler Goodlet 2024-02-20 15:39:45 -05:00
parent 114ec36436
commit df50d78042
1 changed files with 86 additions and 53 deletions

View File

@ -1,18 +1,19 @@
# tractor: structured concurrent "actors". # tractor: structured concurrent "actors".
# Copyright 2018-eternity Tyler Goodlet. # Copyright 2018-eternity Tyler Goodlet.
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or
# it under the terms of the GNU Affero General Public License as published by # modify it under the terms of the GNU Affero General Public License
# the Free Software Foundation, either version 3 of the License, or # as published by the Free Software Foundation, either version 3 of
# (at your option) any later version. # the License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, # This program is distributed in the hope that it will be useful, but
# but WITHOUT ANY WARRANTY; without even the implied warranty of # WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# GNU Affero General Public License for more details. # Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public
# along with this program. If not, see <https://www.gnu.org/licenses/>. # License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
""" """
Multi-core debugging for da peeps! Multi-core debugging for da peeps!
@ -43,6 +44,7 @@ from types import FrameType
import pdbp import pdbp
import tractor import tractor
import trio import trio
from trio.lowlevel import current_task
from trio_typing import ( from trio_typing import (
TaskStatus, TaskStatus,
# Task, # Task,
@ -50,6 +52,7 @@ from trio_typing import (
from ..log import get_logger from ..log import get_logger
from .._state import ( from .._state import (
current_actor,
is_root_process, is_root_process,
debug_mode, debug_mode,
) )
@ -238,7 +241,7 @@ async def _acquire_debug_lock_from_root_task(
to the ``pdb`` repl. to the ``pdb`` repl.
''' '''
task_name: str = trio.lowlevel.current_task().name task_name: str = current_task().name
we_acquired: bool = False we_acquired: bool = False
log.runtime( log.runtime(
@ -323,8 +326,7 @@ async def lock_tty_for_child(
highly reliable at releasing the mutex complete! highly reliable at releasing the mutex complete!
''' '''
task_name = trio.lowlevel.current_task().name task_name: str = current_task().name
if tuple(subactor_uid) in Lock._blocked: if tuple(subactor_uid) in Lock._blocked:
log.warning( log.warning(
f'Actor {subactor_uid} is blocked from acquiring debug lock\n' f'Actor {subactor_uid} is blocked from acquiring debug lock\n'
@ -407,11 +409,13 @@ async def wait_for_parent_stdin_hijack(
assert val == 'Locked' assert val == 'Locked'
async with ctx.open_stream() as stream: async with ctx.open_stream() as stream:
# unblock local caller
try: try:
# unblock local caller
assert Lock.local_pdb_complete assert Lock.local_pdb_complete
task_status.started(cs) task_status.started(cs)
# wait for local task to exit and
# release the REPL
await Lock.local_pdb_complete.wait() await Lock.local_pdb_complete.wait()
finally: finally:
@ -468,7 +472,7 @@ def shield_sigint_handler(
uid_in_debug: tuple[str, str] | None = Lock.global_actor_in_debug uid_in_debug: tuple[str, str] | None = Lock.global_actor_in_debug
actor = tractor.current_actor() actor = current_actor()
# print(f'{actor.uid} in HANDLER with ') # print(f'{actor.uid} in HANDLER with ')
def do_cancel(): def do_cancel():
@ -613,7 +617,7 @@ def _set_trace(
shield: bool = False, shield: bool = False,
): ):
__tracebackhide__: bool = True __tracebackhide__: bool = True
actor: tractor.Actor = actor or tractor.current_actor() actor: tractor.Actor = actor or current_actor()
# start 2 levels up in user code # start 2 levels up in user code
frame: FrameType | None = sys._getframe() frame: FrameType | None = sys._getframe()
@ -683,9 +687,9 @@ async def pause(
''' '''
# __tracebackhide__ = True # __tracebackhide__ = True
actor = tractor.current_actor() actor = current_actor()
pdb, undo_sigint = mk_mpdb() pdb, undo_sigint = mk_mpdb()
task_name = trio.lowlevel.current_task().name task_name: str = trio.lowlevel.current_task().name
if ( if (
not Lock.local_pdb_complete not Lock.local_pdb_complete
@ -836,7 +840,7 @@ async def pause(
# runtime aware version which takes care of all . # runtime aware version which takes care of all .
def pause_from_sync() -> None: def pause_from_sync() -> None:
print("ENTER SYNC PAUSE") print("ENTER SYNC PAUSE")
actor: tractor.Actor = tractor.current_actor( actor: tractor.Actor = current_actor(
err_on_no_runtime=False, err_on_no_runtime=False,
) )
if actor: if actor:
@ -971,9 +975,10 @@ async def acquire_debug_lock(
''' '''
Grab root's debug lock on entry, release on exit. Grab root's debug lock on entry, release on exit.
This helper is for actor's who don't actually need This helper is for actor's who don't actually need to acquired
to acquired the debugger but want to wait until the the debugger but want to wait until the lock is free in the
lock is free in the process-tree root. process-tree root such that they don't clobber an ongoing pdb
REPL session in some peer or child!
''' '''
if not debug_mode(): if not debug_mode():
@ -1013,43 +1018,71 @@ async def maybe_wait_for_debugger(
# tearing down. # tearing down.
sub_in_debug: tuple[str, str] | None = None sub_in_debug: tuple[str, str] | None = None
for _ in range(poll_steps): for istep in range(poll_steps):
if Lock.global_actor_in_debug: if sub_in_debug := Lock.global_actor_in_debug:
sub_in_debug = tuple(Lock.global_actor_in_debug) log.pdb(
f'Lock in use by {sub_in_debug}'
log.debug('Root polling for debug') )
# TODO: could this make things more deterministic?
with trio.CancelScope(shield=True): # wait to see if a sub-actor task will be
await trio.sleep(poll_delay) # scheduled and grab the tty lock on the next
# tick?
# TODO: could this make things more deterministic? wait # XXX => but it doesn't seem to work..
# to see if a sub-actor task will be scheduled and grab
# the tty lock on the next tick?
# XXX: doesn't seem to work
# await trio.testing.wait_all_tasks_blocked(cushion=0) # await trio.testing.wait_all_tasks_blocked(cushion=0)
debug_complete = Lock.no_remote_has_tty debug_complete: trio.Event|None = Lock.no_remote_has_tty
if ( if (
debug_complete debug_complete
and sub_in_debug is not None
and not debug_complete.is_set() and not debug_complete.is_set()
and sub_in_debug is not None
): ):
log.pdb( log.pdb(
'Root has errored but pdb is in use by ' 'Root has errored but pdb is in use by child\n'
f'child {sub_in_debug}\n' 'Waiting on tty lock to release..\n'
'Waiting on tty lock to release..' f'uid: {sub_in_debug}\n'
) )
await debug_complete.wait() await debug_complete.wait()
log.pdb(
f'Child subactor released debug lock!\n'
f'uid: {sub_in_debug}\n'
)
if debug_complete.is_set():
break
# is no subactor locking debugger currently?
elif (
debug_complete is None
or sub_in_debug is None
):
log.pdb(
'Root acquired debug TTY LOCK from child\n'
f'uid: {sub_in_debug}'
)
break
else:
# TODO: don't need this right?
# await trio.lowlevel.checkpoint()
log.debug(
'Root polling for debug:\n'
f'poll step: {istep}\n'
f'poll delya: {poll_delay}'
)
with trio.CancelScope(shield=True):
await trio.sleep(poll_delay) await trio.sleep(poll_delay)
continue continue
else: else:
log.debug( log.pdb('Root acquired debug TTY LOCK')
'Root acquired TTY LOCK'
)
# else:
# # TODO: non-root call for #320?
# this_uid: tuple[str, str] = current_actor().uid
# async with acquire_debug_lock(
# subactor_uid=this_uid,
# ):
# pass
# TODO: better naming and what additionals? # TODO: better naming and what additionals?
# - [ ] optional runtime plugging? # - [ ] optional runtime plugging?