Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 0 additions & 1 deletion Lib/test/test_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,6 @@ def test_sys_getframemodulename(self):
self.assertEqual(actual, expected)


@unittest.expectedFailure # TODO: RUSTPYTHON
def test_threading(self):
returncode, events, stderr = self.run_python("test_threading")
if returncode:
Expand Down
90 changes: 90 additions & 0 deletions crates/host_env/src/select.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,19 @@ pub fn duration_to_timeval(d: Duration) -> timeval {
tv
}

pub fn duration_to_timeval_ceiling(duration: Duration) -> Option<timeval> {
let micros = i64::try_from(duration.as_nanos().div_ceil(1000)).ok()?;
timeval_from_microseconds(micros)
}

#[allow(clippy::useless_conversion, reason = "timeval fields can be 32-bit")]
pub fn timeval_from_microseconds(micros: i64) -> Option<timeval> {
Some(timeval {
tv_sec: micros.div_euclid(1_000_000).try_into().ok()?,
tv_usec: micros.rem_euclid(1_000_000).try_into().ok()?,
})
}

fn saturate_secs<T>(secs: u64, _sample: T) -> T
where
T: TryFrom<u64> + TryFrom<i32>,
Expand Down Expand Up @@ -482,6 +495,83 @@ pub fn poll_fds(fds: &mut [PollFd], timeout: i32) -> std::io::Result<i32> {
}
}

// ppoll preserves Python's nanosecond timeout on platforms with a supported
// binding. Other poll backends continue to use rounded signed milliseconds.
#[cfg(unix)]
pub struct PollTimeout {
#[cfg(any(target_os = "linux", target_os = "android"))]
timespec: Option<libc::timespec>,
#[cfg(not(any(target_os = "linux", target_os = "android")))]
milliseconds: i32,
}

#[cfg(unix)]
impl PollTimeout {
#[allow(clippy::useless_conversion, reason = "time_t can be 32-bit")]
pub fn from_nanoseconds(timeout: Option<i64>) -> Option<Self> {
#[cfg(any(target_os = "linux", target_os = "android"))]
if let Some(nanoseconds) = timeout {
// CPython validates timespec even for negative (infinite) waits.
// Floor division matches the normalized signed timespec seconds.
libc::time_t::try_from(nanoseconds.div_euclid(1_000_000_000)).ok()?;
}
Self::from_duration(
timeout
.filter(|&ns| ns >= 0)
.map(|ns| Duration::from_nanos(ns as u64)),
)
}

pub fn from_duration(timeout: Option<Duration>) -> Option<Self> {
#[cfg(any(target_os = "linux", target_os = "android"))]
{
let timespec = match timeout {
Some(duration) => Some(libc::timespec {
tv_sec: duration.as_secs().try_into().ok()?,
tv_nsec: duration.subsec_nanos() as _,
}),
None => None,
};
Some(Self { timespec })
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
{
let milliseconds = match timeout {
Some(duration) => duration_as_millis_ceiling(duration)?,
None => -1,
};
Some(Self { milliseconds })
}
}
}

#[cfg(unix)]
pub fn poll_fds_with_timeout(fds: &mut [PollFd], timeout: &PollTimeout) -> io::Result<i32> {
#[cfg(any(target_os = "linux", target_os = "android"))]
{
let timespec = timeout.timespec.as_ref().map_or(core::ptr::null(), |ts| ts);
// A null mask preserves the calling thread's signal mask. libc owns
// any platform-specific copying needed by the ppoll syscall.
let result = unsafe {
libc::ppoll(
fds.as_mut_ptr(),
fds.len() as _,
timespec,
core::ptr::null(),
)
};
if result < 0 {
Err(io::Error::last_os_error())
} else {
Ok(result)
}
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
{
poll_fds(fds, timeout.milliseconds)
}
}

#[cfg(any(target_os = "linux", target_os = "android", target_os = "redox"))]
pub mod epoll {
use std::os::fd::{IntoRawFd, RawFd};
Expand Down
102 changes: 65 additions & 37 deletions crates/stdlib/src/_queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ mod _queue {
use crate::vm::{
AsObject, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
builtins::{PyBaseExceptionRef, PyException, PyGenericAlias, PyStr, PyType, PyTypeRef},
function::TimeoutSeconds,
function::OptionalArg,
protocol::PyNumberMethods,
stdlib::time::pytime,
types::{AsNumber, Constructor, Representable},
};

Expand Down Expand Up @@ -85,15 +86,6 @@ mod _queue {
vm.allow_threads(|| self.mutex.lock())
}

fn release(&self, vm: &VirtualMachine) {
{
let mut count = self.lock_count(vm);
*count += 1;
} // lock dropped. now we can notify a waiting thread

self.cond.notify_one();
}

/// `Ok(true)` if acquired, `Ok(false)` on timeout, `Err` if a signal
/// handler raised (e.g. `KeyboardInterrupt`) while we were waiting.
fn acquire(
Expand Down Expand Up @@ -167,10 +159,21 @@ mod _queue {
expect(unused_variables, reason = "only the semaphore needs the vm")
)]
fn push(&self, item: PyObjectRef, vm: &VirtualMachine) {
self.buf.lock().push_back(item);

#[cfg(feature = "threading")]
self.sem.release(vm);
{
// Publish the permit before unlocking the buffer, so another
// thread cannot observe an item that no getter can claim.
let mut count = self.sem.lock_count(vm);
{
let mut buf = self.buf.lock();
buf.push_back(item);
*count += 1;
}
drop(count);
self.sem.cond.notify_one();
}
#[cfg(not(feature = "threading"))]
self.buf.lock().push_back(item);
}

/// Returns a strong reference from the head of the buffer.
Expand Down Expand Up @@ -204,26 +207,25 @@ mod _queue {
struct PutArgs {
#[pyarg(any)]
item: PyObjectRef,
#[expect(
dead_code,
reason = "Intentional. Provide compatibility with the Queue class"
)]
#[pyarg(any, default = true)]
block: bool,
#[expect(
dead_code,
reason = "Intentional. Provide compatibility with the Queue class"
)]
#[pyarg(any, optional, py_default = "True")]
block: OptionalArg<PyObjectRef>,
#[pyarg(any, optional)]
timeout: Option<PyObjectRef>,
}

#[derive(FromArgs)]
struct GetArgs {
#[pyarg(any, default = true)]
block: bool,
#[pyarg(any, optional, py_default = "True")]
block: OptionalArg<PyObjectRef>,

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.

isn't ArgToBool possible?

#[pyarg(any, optional)]
timeout: Option<TimeoutSeconds>,
timeout: Option<PyObjectRef>,
}

fn is_blocking(block: &OptionalArg<PyObjectRef>, vm: &VirtualMachine) -> PyResult<bool> {
match block {
OptionalArg::Present(value) => value.try_to_bool(vm),
OptionalArg::Missing => Ok(true),
}
}

#[pyclass(
Expand All @@ -248,9 +250,15 @@ mod _queue {
}

#[pymethod]
fn put(zelf: &Py<Self>, args: PutArgs, vm: &VirtualMachine) {
let PutArgs { item, .. } = args;
fn put(zelf: &Py<Self>, args: PutArgs, vm: &VirtualMachine) -> PyResult<()> {
let PutArgs {
item,
block,
timeout: _timeout,
} = args;
is_blocking(&block, vm)?;
zelf.push(item, vm);
Ok(())
}

#[pymethod]
Expand All @@ -261,10 +269,11 @@ mod _queue {
#[pymethod]
fn get(zelf: &Py<Self>, args: GetArgs, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
let GetArgs { block, timeout } = args;
let blocking = is_blocking(&block, vm)?;

// Non-blocking: just try once
if !block {
return Self::get_inner(&mut zelf.buf.lock()).ok_or_else(|| empty_error(vm));
if !blocking {
return Self::get_nowait(zelf, vm);
}

#[cfg_attr(
Expand All @@ -274,22 +283,37 @@ mod _queue {
reason = "We are still validating the 'timeout' arg even if we don't have threading"
)
)]
let deadline = match timeout.map(|v| v.to_secs_f64()) {
Some(v) if v < 0.0 => {
return Err(vm.new_value_error("'timeout' must be a non-negative number"));
let deadline = match timeout.as_ref() {
Some(value) => {
let overflow =
|| vm.new_overflow_error("timestamp out of range for C PyTime_t");
// SimpleQueue uses ceiling, unlike the away-from-zero
// rounding used by thread locks and select timeouts.
let nanos = pytime::from_seconds_object(value, pytime::Round::Ceiling, vm)?;
if nanos < 0 {
return Err(vm.new_value_error("'timeout' must be a non-negative number"));
}
Some(
Instant::now()
.checked_add(Duration::from_nanos(nanos as u64))
.ok_or_else(overflow)?,
)
}
Some(v) => Some(Instant::now() + Duration::from_secs_f64(v)),
None => None,
};

#[cfg(feature = "threading")]
{
if !zelf.sem.acquire(block, deadline, vm)? {
if !zelf.sem.acquire(blocking, deadline, vm)? {
return Err(empty_error(vm));
}
}

Self::get_inner(&mut zelf.buf.lock()).ok_or_else(|| empty_error(vm))
let item = {
let mut buf = zelf.buf.lock();
Self::get_inner(&mut buf)
};
item.ok_or_else(|| empty_error(vm))
}

#[pymethod]
Expand All @@ -301,7 +325,11 @@ mod _queue {
}
}

Self::get_inner(&mut zelf.buf.lock()).ok_or_else(|| empty_error(vm))
let item = {
let mut buf = zelf.buf.lock();
Self::get_inner(&mut buf)
};
item.ok_or_else(|| empty_error(vm))
}

#[pyclassmethod]
Expand Down
Loading
Loading