Skip to content

Hang when reattaching after detach during shutdown#6085

Open
anuraaga wants to merge 16 commits into
PyO3:mainfrom
anuraaga:detach-hang-finalizagion
Open

Hang when reattaching after detach during shutdown#6085
anuraaga wants to merge 16 commits into
PyO3:mainfrom
anuraaga:detach-hang-finalizagion

Conversation

@anuraaga
Copy link
Copy Markdown

@anuraaga anuraaga commented Jun 1, 2026

Fixes #6084

By submitting these contributions you agree for them to be dual-licensed under PyO3's MIT OR Apache-2.0 license.

@anuraaga anuraaga changed the title Detach hang finalizagion Hang when reattaching after detach during shutdown Jun 1, 2026
Copy link
Copy Markdown
Member

@davidhewitt davidhewitt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, in principle this seems ok.

General nit: a lot of the comments are very verbose, read like LLM-generated. If you did indeed use AI to help you produce this, please try to refrain from allowing the output to be too bloated.

Comment thread pyo3-ffi/src/ceval.rs Outdated
Comment on lines +161 to +171
#[cfg(not(any(Py_3_14, target_arch = "wasm32")))]
struct HangThread;

#[cfg(not(any(Py_3_14, target_arch = "wasm32")))]
impl Drop for HangThread {
fn drop(&mut self) {
loop {
std::thread::park(); // Block forever.
}
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this to pyo3-ffi/src/impl_/mod.rs and deduplicate with the same logic in pystate.rs.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I wasn't sure what a good place for the shared logic would be will do that

Comment on lines +9 to +20
@pytest.mark.skipif(
sysconfig.get_config_var("Py_DEBUG"),
reason="causes a crash on debug builds",
)
def test_detach_then_reattach_in_thread():
# A Python daemon thread calls block_in_detach_until_finalizing(), which releases
# the GIL via py.detach() and polls Py_IsFinalizing() (safe without GIL — reads
# an atomic). When the interpreter begins finalizing, the thread exits the closure
# and SuspendAttach::drop() calls PyEval_RestoreThread() on a finalizing interpreter.
t = threading.Thread(target=misc.block_in_detach_until_finalizing, daemon=True)
t.start()
# No join — thread polls until finalization
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this please have a similar mechanism to test_hammer_attaching_in_thread.py, perhaps both files can be merged together as test_finalization.py or similar?

Comment thread pytests/src/misc.rs Outdated
{
extern "C" {
fn _Py_IsFinalizing() -> std::ffi::c_int;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will probably not work on PyPy or older x86 windows builds due to wrong symbol name. As per other comment I think this should be unified with test_hammer_attaching_in_thread.py

Copy link
Copy Markdown
Author

@anuraaga anuraaga Jun 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried pretty hard to do that and it's in the first version of my issue comment. I couldn't avoid this different error

Fatal Python error: PyGILState_Release: thread state 0xefd390000910 must be current when releasing
Python runtime state: finalizing (tstate=0x000000000097a5d0)

I guess it could be about timing of being during GC or not. So I had to come up with a test case closer to the repro I got which was also polling until finalization. If you have any advice would be great

@anuraaga
Copy link
Copy Markdown
Author

anuraaga commented Jun 2, 2026

If you did indeed use AI to help you produce this, please try to refrain from allowing the output to be too bloated.

Sorry about that - the logic change was quite easy and I could do that manually but did many iterations with AI to get a test case that flipped with the change. While I tried to clean them up probably some things are more obvious than I thought, will trim more.

Comment thread pytests/src/misc.rs
}

/// Wrapper to mark Receiver as Sync.
struct SyncReceiver<T>(std::sync::mpsc::Receiver<T>);
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incidentally I've always wondered if PyO3 could provide an easier way for this :-)

@anuraaga
Copy link
Copy Markdown
Author

anuraaga commented Jun 3, 2026

@davidhewitt I have gone ahead and merged the test with the existing pattern. Just as a reminder, the error message from this test is

Fatal Python error: PyGILState_Release: thread state 0xefd390000910 must be current when releasing
Python runtime state: finalizing (tstate=0x000000000097a5d0)

while the one I was debugging is "FATAL: exception not rethrown".

But since the same fix applied to both, maybe this new test is close enough in behavior that it is effectively a regression test for both, though I'm not sure. Since I've confirmed it fixes my issue I am comfortable with it

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.

Fatal python error when detach in a rust thread during finalization

2 participants