Skip to content

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

Open
Rich Chiodo (rchiodo) wants to merge 2 commits 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 2 commits 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>
@StellaHuang95

Copy link
Copy Markdown
Contributor

🔒 Automated review in progress — Stella Huang (@StellaHuang95) is auto-reviewing this PR.

Comment thread src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_vars.py Outdated
@StellaHuang95 Stella Huang (StellaHuang95) added the review-auto:changes-requested Automated review: posted blocking findings to address. label Aug 6, 2026
…inline issue URLs

Co-authored-by: Copilot <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

review-auto:changes-requested Automated review: posted blocking findings to address.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Debugger hangs when inspecting LanceDBConnection object on Python 3.13

2 participants