Fix debugger hang when expanding objects with blocking property getters (#2053) - #2055
Open
rchiodo wants to merge 1 commit into
Open
Fix debugger hang when expanding objects with blocking property getters (#2053)#2055rchiodo wants to merge 1 commit into
rchiodo wants to merge 1 commit into
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #2053.
Inspecting a
LanceDBConnectionin the debugger hung on Python 3.13. The root cause is not a slowrepr— it's a thread deadlock during variable expansion.At a breakpoint, debugpy suspends all threads (in
multi_threads_single_notificationmode), including background daemon threads.LanceDBConnectionexposes@propertygetters such asread_consistency_intervalthat 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.db = Nonebefore the breakpoint avoids the hang.Changes
1. Recover instead of hang (the real cure)
PYDEVD_UNBLOCK_THREADS_ON_VARIABLES_TIMEOUT(default3.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.unblock_threads_on_timeoutcontext manager inpydevd_vars.py._run_with_unblock_threadsnow uses it (evaluate-path behavior unchanged).internal_get_variable_json(the DAP variables request).2. Dedupe double
getattrDefaultResolver._get_py_dictionarypreviously didhasattr()+getattr(), evaluating every property/descriptor twice. It now callsgetattr()a single time insidetry/except AttributeError. This halves the cost (and side effects) of expanding objects with expensive properties.Tests
_debugger_case_deadlock_thread_variables.pyandtest_debugger_case_deadlock_thread_variablesreproduce 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.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 thegetattrdedupe is behavior-preserving.All changes are in pure-Python pydevd modules (no Cython regeneration needed).