Skip to content

Fix debugger hang when expanding objects with blocking property getters (#2053) - #2055

Open
Rich Chiodo (rchiodo) wants to merge 1 commit into
mainfrom
rchiodo-investigate-repr-hang-2053
Open

Fix debugger hang when expanding objects with blocking property getters (#2053)#2055
Rich Chiodo (rchiodo) wants to merge 1 commit into
mainfrom
rchiodo-investigate-repr-hang-2053

Conversation

@rchiodo

Copy link
Copy Markdown
Contributor

Summary

Fixes #2053.

Inspecting a LanceDBConnection in the debugger hung on Python 3.13. The root cause is not a slow repr — it's a thread deadlock during variable expansion.

At a breakpoint, debugpy suspends all threads (in multi_threads_single_notification mode), including background daemon threads. LanceDBConnection exposes @property getters such as read_consistency_interval that dispatch work to a background asyncio event loop running in a daemon thread and block on the result (asyncio.run_coroutine_threadsafe(...).result()). When the variables view expands the object, debugpy evaluates those properties on the suspended thread, which waits forever on the also-suspended background loop thread → deadlock.

This is why:

  • repr(db) is fast — it never touches the properties.
  • vars(db) works — it only reads the instance __dict__, no property evaluation.
  • Setting db = None before the breakpoint avoids the hang.

Changes

1. Recover instead of hang (the real cure)

  • New constant PYDEVD_UNBLOCK_THREADS_ON_VARIABLES_TIMEOUT (default 3.0s). When resolving a variable's children takes longer than the timeout, the other threads are resumed until the resolution finishes, then re-suspended — so a property that depends on another thread can complete instead of deadlocking.
  • Reuses the existing evaluate-path anti-deadlock machinery, extracted into a shared unblock_threads_on_timeout context manager in pydevd_vars.py. _run_with_unblock_threads now uses it (evaluate-path behavior unchanged).
  • Wired into internal_get_variable_json (the DAP variables request).

2. Dedupe double getattr

  • DefaultResolver._get_py_dictionary previously did hasattr() + getattr(), evaluating every property/descriptor twice. It now calls getattr() a single time inside try/except AttributeError. This halves the cost (and side effects) of expanding objects with expensive properties.

Tests

  • New resource _debugger_case_deadlock_thread_variables.py and test_debugger_case_deadlock_thread_variables reproduce the exact scenario (a property getter blocking on a suspended background thread). The test passes with the fix and deadlocks/fails when the timeout is disabled, proving it catches the regression.
  • All existing evaluate-path deadlock/unblock tests pass (test_debugger_case_deadlock_thread_eval, test_debugger_case_breakpoint_on_unblock_thread_eval, test_debugger_case_unblock_manually, test_debugger_case_deadlock_notify_evaluate_timeout, test_debugger_case_deadlock_interrupt_thread).
  • test_hasattr_failure, test_getattr_warning, and the broader variable/resolver test set pass, confirming the getattr dedupe is behavior-preserving.

All changes are in pure-Python pydevd modules (no Cython regeneration needed).

…rs (#2053)

Inspecting an object whose property getters block on a background thread
(e.g. lancedb's LanceDBConnection, which dispatches to a daemon event-loop
thread) hung the debugger. At a breakpoint all threads are suspended, so
expanding the variable evaluated a property that waited forever on the
suspended background thread -> deadlock. repr() was unaffected because it
never touches the properties.

Fixes:
- Add PYDEVD_UNBLOCK_THREADS_ON_VARIABLES_TIMEOUT (default 3.0s). When
  resolving a variable's children takes too long, resume the other threads
  until it finishes, then re-suspend -- so the debugger recovers instead of
  hanging. Reuses the existing evaluate-path unblock machinery, extracted
  into a shared unblock_threads_on_timeout context manager.
- DefaultResolver._get_py_dictionary now calls getattr a single time
  (try/except AttributeError) instead of hasattr + getattr, which evaluated
  every property/descriptor twice.

Adds a regression test reproducing the deadlock via the variables request.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Debugger hangs when inspecting LanceDBConnection object on Python 3.13

1 participant