tractor/tractor
Tyler Goodlet 2f1a97e73e Catch `.pause_from_sync()` in root bg thread bugs!
Originally discovered as while using `tractor.pause_from_sync()`
from the `i3ipc` client running in a bg-thread that uses `asyncio`
inside `modden`.

Turns out we definitely aren't correctly handling `.pause_from_sync()`
from the root actor when called from a `trio.to_thread.run_sync()`
bg thread:
- root-actor bg threads which can't `Lock._debug_lock.acquire()` since
  they aren't in `trio.Task`s.
- even if scheduled via `.to_thread.run_sync(_debug._pause)` the
  acquirer won't be the task/thread which calls `Lock.release()` from
  `PdbREPL` hooks; this results in a RTE raised by `trio`..
- multiple threads will step on each other's stdio since cpython's GIL
  seems to ctx switch threads on every input from the user to the REPL
  loop..

Reproduce via reworking our example and test so that they catch and fail
for all edge cases:
- rework the `/examples/debugging/sync_bp.py` example to demonstrate the
  above issues, namely the stdio clobbering in the REPL when multiple
  threads and/or a subactor try to debug simultaneously.
  |_ run one thread using a task nursery to ensure it runs conc with the
     nursery's parent task.
  |_ ensure the bg threads run conc a subactor usage of
     `.pause_from_sync()`.
  |_ gravely detail all the special cases inside a TODO comment.
  |_ add some control flags to `sync_pause()` helper and don't use
     `breakpoint()` by default.
- extend and adjust `test_debugger.test_pause_from_sync` to match (and
  thus currently fail) by ensuring exclusive `PdbREPL` attachment when
  the 2 bg root-actor threads are concurrently interacting alongside the
  subactor:
  |_ should only see one of the `_pause_msg` logs at a time for either
     one of the threads or the subactor.
  |_ ensure each attaches (in no particular order) before expecting the
     script to exit.

Impl adjustments to `.devx._debug`:
- drop `Lock.repl`, no longer used.
- add `Lock._owned_by_root: bool` for the `.ctx_in_debug == None`
  root-actor-task active case.
- always `log.exception()` for any `._debug_lock.release()` ownership
  RTE emitted by `trio`, like we used to..
- add special `Lock.release()` log message for the stale lock but
  `._owned_by_root == True` case; oh yeah and actually
  `log.devx(message)`..
- rename `Lock.acquire()` -> `.acquire_for_ctx()` since it's only ever
  used from subactor IPC usage; well that and for local root-task
  usage we should prolly add a `.acquire_from_root_task()`?
- buncha `._pause()` impl improvements:
 |_ type `._pause()`'s `debug_func` as a `partial` as well.
 |_ offer `called_from_sync: bool` and `called_from_bg_thread: bool`
    for the special case handling when called from `.pause_from_sync()`
 |_ only set `DebugStatus.repl/repl_task` when `debug_func != None`
   (OW ensure the `.repl_task` is not the current one).
 |_ handle error logging even when `debug_func is None`..
 |_ lotsa detailed commentary around root-actor-bg-thread special cases.
- when `._set_trace(hide_tb=False)` do `pdbp.set_trace(frame=currentframe())`
  so the `._debug` internal frames are always included.
- by default always hide tracebacks for `.pause[_from_sync]()` internals.
- improve `.pause_from_sync()` to avoid root-bg-thread crashes:
 |_ pass new `called_from_xxx_` flags and ensure `DebugStatus.repl_task`
    is actually set to the `threading.current_thread()` when needed.
 |_ manually call `Lock._debug_lock.acquire_nowait()` for the non-bg
    thread case.
 |_ TODO: still need to implement the bg-thread case using a bg
    `trio.Task`-in-thread with an `trio.Event` set by thread REPL exit.
2025-03-24 14:04:52 -04:00
..
_testing Start a new `._testing.fault_simulation` 2025-03-24 14:04:51 -04:00
devx Catch `.pause_from_sync()` in root bg thread bugs! 2025-03-24 14:04:52 -04:00
experimental Drop now-deprecated deps on modern `trio`/Python 2025-03-16 16:06:24 -04:00
msg Drop sub-decoder proto-cruft from `.msg._codec` 2025-03-24 14:04:52 -04:00
trionics Drop now-deprecated deps on modern `trio`/Python 2025-03-16 16:06:24 -04:00
__init__.py Expose `tractor.current_ipc_ctx()` at pkg level 2025-03-24 14:04:51 -04:00
_child.py Hide `._entry`/`._child` frames, tweak some more type annots 2025-03-20 23:22:45 -04:00
_clustering.py Passthrough runtime kwargs from `open_actor_cluster()` 2022-12-11 19:56:08 -05:00
_context.py Move `Context.open_stream()` impl to `._streaming` 2025-03-24 14:04:52 -04:00
_discovery.py More spaceless union type annots 2025-03-20 19:50:31 -04:00
_entry.py Call `.devx._debug.hide_runtime_frames()` by default 2025-03-24 14:04:51 -04:00
_exceptions.py Pass `boxed_type` from `_mk_msg_type_err()` 2025-03-24 14:04:52 -04:00
_forkserver_override.py Re-license code base for distribution under AGPL 2021-12-14 23:33:27 -05:00
_ipc.py Drop `msg.types.Msg` for new replacement types 2025-03-24 14:04:51 -04:00
_mp_fixup_main.py Avoid importing mp for as long as possible 2022-02-17 11:55:26 -05:00
_multiaddr.py Fix doc string "its" typo.. 2025-03-20 19:50:31 -04:00
_portal.py Adjust `Portal` usage of `Context.pld_rx` 2025-03-24 14:04:51 -04:00
_root.py Call `.devx._debug.hide_runtime_frames()` by default 2025-03-24 14:04:51 -04:00
_rpc.py Use `Context` repr APIs for RPC outcome logs 2025-03-24 14:04:52 -04:00
_runtime.py Don't (noisly) log about runtime cancel RPC tasks 2025-03-24 14:04:51 -04:00
_spawn.py Add debug check-n-wait inside `._spawn.soft_kill()` 2025-03-24 14:04:51 -04:00
_state.py Add error suppress flag to `current_ipc_ctx()` 2025-03-24 14:04:51 -04:00
_streaming.py Move `Context.open_stream()` impl to `._streaming` 2025-03-24 14:04:52 -04:00
_supervise.py Show runtime nursery frames on internal errors 2025-03-24 14:04:51 -04:00
log.py Bleh, make `log.devx()` level less then cancel but > `.runtime()` 2025-03-20 23:22:45 -04:00
to_asyncio.py Provision for infected-`asyncio` debug mode support 2025-03-20 22:37:51 -04:00