Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix calling POOL.update_counts() when no gil-refs feature #4200

Merged
merged 8 commits into from
Jun 2, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 69 additions & 45 deletions src/gil.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,8 @@ impl GILGuard {
/// `GILGuard::Ensured` will be returned.
pub(crate) fn acquire() -> Self {
if gil_is_acquired() {
increment_gil_count();
return GILGuard::Assumed;
// SAFETY: We just checked that the GIL is already acquired.
return unsafe { Self::assume() };
}

// Maybe auto-initialize the GIL:
Expand Down Expand Up @@ -205,43 +205,43 @@ impl GILGuard {
}
}

Self::acquire_unchecked()
// SAFETY: We have ensured the Python interpreter is initialized.
unsafe { Self::acquire_unchecked() }
}

/// Acquires the `GILGuard` without performing any state checking.
///
/// This can be called in "unsafe" contexts where the normal interpreter state
/// checking performed by `GILGuard::acquire` may fail. This includes calling
/// as part of multi-phase interpreter initialization.
pub(crate) fn acquire_unchecked() -> Self {
pub(crate) unsafe fn acquire_unchecked() -> Self {
if gil_is_acquired() {
increment_gil_count();
return GILGuard::Assumed;
return Self::assume();
}

let gstate = unsafe { ffi::PyGILState_Ensure() }; // acquire GIL
increment_gil_count();

#[cfg(feature = "gil-refs")]
#[allow(deprecated)]
{
let pool = unsafe { mem::ManuallyDrop::new(GILPool::new()) };
increment_gil_count();
GILGuard::Ensured { gstate, pool }
}
let pool = unsafe { mem::ManuallyDrop::new(GILPool::new()) };

#[cfg(not(feature = "gil-refs"))]
{
increment_gil_count();
// Update counts of PyObjects / Py that have been cloned or dropped since last acquisition
#[cfg(not(pyo3_disable_reference_pool))]
POOL.update_counts(unsafe { Python::assume_gil_acquired() });

GILGuard::Ensured { gstate }
#[cfg(not(pyo3_disable_reference_pool))]
POOL.update_counts(unsafe { Python::assume_gil_acquired() });
GILGuard::Ensured {
gstate,
#[cfg(feature = "gil-refs")]
pool,
}
}

/// Acquires the `GILGuard` while assuming that the GIL is already held.
pub(crate) unsafe fn assume() -> Self {
increment_gil_count();
GILGuard::Assumed
let guard = GILGuard::Assumed;
#[cfg(not(pyo3_disable_reference_pool))]
POOL.update_counts(guard.python());
guard
}

/// Gets the Python token associated with this [`GILGuard`].
Expand All @@ -256,15 +256,14 @@ impl Drop for GILGuard {
fn drop(&mut self) {
match self {
GILGuard::Assumed => {}
#[cfg(feature = "gil-refs")]
GILGuard::Ensured { gstate, pool } => unsafe {
GILGuard::Ensured {
gstate,
#[cfg(feature = "gil-refs")]
pool,
} => unsafe {
// Drop the objects in the pool before attempting to release the thread state
#[cfg(feature = "gil-refs")]
mem::ManuallyDrop::drop(pool);

ffi::PyGILState_Release(*gstate);
},
#[cfg(not(feature = "gil-refs"))]
GILGuard::Ensured { gstate } => unsafe {
ffi::PyGILState_Release(*gstate);
},
}
Expand Down Expand Up @@ -549,14 +548,15 @@ fn decrement_gil_count() {

#[cfg(test)]
mod tests {
#[cfg(not(pyo3_disable_reference_pool))]
use super::{gil_is_acquired, POOL};
use super::GIL_COUNT;
#[cfg(feature = "gil-refs")]
#[allow(deprecated)]
use super::{GILPool, GIL_COUNT, OWNED_OBJECTS};
use crate::types::any::PyAnyMethods;
use super::OWNED_OBJECTS;
#[cfg(not(pyo3_disable_reference_pool))]
use super::{gil_is_acquired, POOL};
#[cfg(feature = "gil-refs")]
use crate::{ffi, gil};
use crate::{gil::GILGuard, types::any::PyAnyMethods};
use crate::{PyObject, Python};
use std::ptr::NonNull;

Expand All @@ -582,7 +582,7 @@ mod tests {
.contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) })
}

#[cfg(all(not(pyo3_disable_reference_pool), not(target_arch = "wasm32")))]
#[cfg(not(pyo3_disable_reference_pool))]
fn pool_dec_refs_contains(obj: &PyObject) -> bool {
POOL.pending_decrefs
.lock()
Expand Down Expand Up @@ -702,30 +702,29 @@ mod tests {
}

#[test]
#[cfg(feature = "gil-refs")]
#[allow(deprecated)]
fn test_gil_counts() {
// Check with_gil and GILPool both increase counts correctly
// Check with_gil and GILGuard both increase counts correctly
let get_gil_count = || GIL_COUNT.with(|c| c.get());

assert_eq!(get_gil_count(), 0);
Python::with_gil(|_| {
assert_eq!(get_gil_count(), 1);

let pool = unsafe { GILPool::new() };
assert_eq!(get_gil_count(), 1);
let pool = unsafe { GILGuard::assume() };
assert_eq!(get_gil_count(), 2);

let pool2 = unsafe { GILPool::new() };
assert_eq!(get_gil_count(), 1);
let pool2 = unsafe { GILGuard::assume() };
assert_eq!(get_gil_count(), 3);

drop(pool);
assert_eq!(get_gil_count(), 1);
assert_eq!(get_gil_count(), 2);

Python::with_gil(|_| {
// nested with_gil doesn't update gil count
assert_eq!(get_gil_count(), 2);
// nested with_gil updates gil count
assert_eq!(get_gil_count(), 3);
});
assert_eq!(get_gil_count(), 1);
assert_eq!(get_gil_count(), 2);

drop(pool2);
assert_eq!(get_gil_count(), 1);
Expand Down Expand Up @@ -794,19 +793,20 @@ mod tests {

#[test]
#[cfg(not(pyo3_disable_reference_pool))]
#[cfg(feature = "gil-refs")]
fn test_update_counts_does_not_deadlock() {
// update_counts can run arbitrary Python code during Py_DECREF.
// if the locking is implemented incorrectly, it will deadlock.

use crate::ffi;
use crate::gil::GILGuard;

Python::with_gil(|py| {
let obj = get_object(py);

unsafe extern "C" fn capsule_drop(capsule: *mut ffi::PyObject) {
// This line will implicitly call update_counts
// -> and so cause deadlock if update_counts is not handling recursion correctly.
#[allow(deprecated)]
let pool = GILPool::new();
let pool = GILGuard::assume();

// Rebuild obj so that it can be dropped
PyObject::from_owned_ptr(
Expand All @@ -826,4 +826,28 @@ mod tests {
POOL.update_counts(py);
})
}

#[test]
#[cfg(not(pyo3_disable_reference_pool))]
fn test_gil_guard_update_counts() {
use crate::gil::GILGuard;

Python::with_gil(|py| {
let obj = get_object(py);

// For GILGuard::acquire

POOL.register_decref(NonNull::new(obj.clone_ref(py).into_ptr()).unwrap());
assert!(pool_dec_refs_contains(&obj));
let _guard = GILGuard::acquire();
assert!(pool_dec_refs_does_not_contain(&obj));

// For GILGuard::assume

POOL.register_decref(NonNull::new(obj.clone_ref(py).into_ptr()).unwrap());
assert!(pool_dec_refs_contains(&obj));
let _guard2 = unsafe { GILGuard::assume() };
assert!(pool_dec_refs_does_not_contain(&obj));
})
}
}
5 changes: 5 additions & 0 deletions src/marker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1280,6 +1280,11 @@ mod tests {
const GIL_NOT_HELD: c_int = 0;
const GIL_HELD: c_int = 1;

// Before starting the interpreter the state of calling `PyGILState_Check`
// seems to be undefined, so let's ensure that Python is up.
#[cfg(not(any(PyPy, GraalPy)))]
crate::prepare_freethreaded_python();

let state = unsafe { crate::ffi::PyGILState_Check() };
assert_eq!(state, GIL_NOT_HELD);

Expand Down
Loading