Introduce TimeoutSeconds utility type for timeout parameters (#7271)

* Introduce TimeoutSeconds utility type for timeout parameters

Follow-up refactoring from #7237.

Python timeout parameters typically accept both float and int. Several
places in the codebase used Either<f64, i64> for this, each repeating
the same match-and-convert boilerplate. This extracts that into a
TimeoutSeconds type in vm::function::number.

Refactored sites:
- _sqlite3::ConnectArgs.timeout
- _thread::AcquireArgs.timeout
- _thread::ThreadHandle::join timeout

Either<f64, i64> in time.rs (6 sites) left unchanged: those are
timestamp values with per-branch logic (floor, range checks, etc).
Either<f64, isize> in select.rs also left unchanged (different type).

* Validate timeout in ThreadHandle::join to prevent panic

* refactor: move TimeoutSeconds from number to time module
This commit is contained in:
Jiseok CHOI
2026-03-09 11:36:03 +09:00
committed by GitHub
parent ead2d98557
commit fa4f84ce3f
4 changed files with 49 additions and 23 deletions

View File

@@ -61,8 +61,8 @@ mod _sqlite3 {
},
convert::IntoObject,
function::{
ArgCallable, ArgIterable, Either, FsPath, FuncArgs, OptionalArg, PyComparisonValue,
PySetterValue,
ArgCallable, ArgIterable, FsPath, FuncArgs, OptionalArg, PyComparisonValue,
PySetterValue, TimeoutSeconds,
},
object::{Traverse, TraverseFn},
protocol::{
@@ -333,8 +333,8 @@ mod _sqlite3 {
struct ConnectArgs {
#[pyarg(any)]
database: FsPath,
#[pyarg(any, default = Either::A(5.0))]
timeout: Either<f64, i64>,
#[pyarg(any, default = TimeoutSeconds::new(5.0))]
timeout: TimeoutSeconds,
#[pyarg(any, default = 0)]
detect_types: c_int,
#[pyarg(any, default = Some(vm.ctx.empty_str.to_owned()))]
@@ -991,10 +991,7 @@ mod _sqlite3 {
fn initialize_db(args: &ConnectArgs, vm: &VirtualMachine) -> PyResult<Sqlite> {
let path = args.database.to_cstring(vm)?;
let db = Sqlite::from(SqliteRaw::open(path.as_ptr(), args.uri, vm)?);
let timeout = (match args.timeout {
Either::A(float) => float,
Either::B(int) => int as f64,
} * 1000.0) as c_int;
let timeout = (args.timeout.to_secs_f64() * 1000.0) as c_int;
db.busy_timeout(timeout);
if let Some(isolation_level) = &args.isolation_level {
begin_statement_ptr_from_isolation_level(isolation_level, vm)?;

View File

@@ -8,6 +8,7 @@ mod getset;
mod method;
mod number;
mod protocol;
mod time;
pub use argument::{
ArgumentError, FromArgOptional, FromArgs, FuncArgs, IntoFuncArgs, KwArgs, OptionalArg,
@@ -23,6 +24,7 @@ pub(super) use getset::{IntoPyGetterFunc, IntoPySetterFunc, PyGetterFunc, PySett
pub use method::{HeapMethodDef, PyMethodDef, PyMethodFlags};
pub use number::{ArgIndex, ArgIntoBool, ArgIntoComplex, ArgIntoFloat, ArgPrimitiveIndex, ArgSize};
pub use protocol::{ArgCallable, ArgIterable, ArgMapping, ArgSequence};
pub use time::TimeoutSeconds;
use crate::{PyObject, PyResult, VirtualMachine, builtins::PyStr, convert::TryFromBorrowedObject};
use builtin::{BorrowedParam, OwnedParam, RefParam};

View File

@@ -0,0 +1,34 @@
use crate::{PyObjectRef, PyResult, TryFromObject, VirtualMachine};
/// A Python timeout value that accepts both `float` and `int`.
///
/// `TimeoutSeconds` implements `FromArgs` so that a built-in function can accept
/// timeout parameters given as either `float` or `int`, normalizing them to `f64`.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TimeoutSeconds {
value: f64,
}
impl TimeoutSeconds {
pub const fn new(secs: f64) -> Self {
Self { value: secs }
}
#[inline]
pub fn to_secs_f64(self) -> f64 {
self.value
}
}
impl TryFromObject for TimeoutSeconds {
fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
let value = match super::Either::<f64, i64>::try_from_object(vm, obj)? {
super::Either::A(f) => f,
super::Either::B(i) => i as f64,
};
if value.is_nan() {
return Err(vm.new_value_error("Invalid value NaN (not a number)".to_owned()));
}
Ok(Self { value })
}
}

View File

@@ -15,7 +15,7 @@ pub(crate) mod _thread {
builtins::{PyDictRef, PyStr, PyTupleRef, PyType, PyTypeRef, PyUtf8StrRef},
common::wtf8::Wtf8Buf,
frame::FrameRef,
function::{ArgCallable, Either, FuncArgs, KwArgs, OptionalArg, PySetterValue},
function::{ArgCallable, FuncArgs, KwArgs, OptionalArg, PySetterValue, TimeoutSeconds},
types::{Constructor, GetAttr, Representable, SetAttr},
};
use alloc::{
@@ -65,33 +65,26 @@ pub(crate) mod _thread {
struct AcquireArgs {
#[pyarg(any, default = true)]
blocking: bool,
#[pyarg(any, default = Either::A(-1.0))]
timeout: Either<f64, i64>,
#[pyarg(any, default = TimeoutSeconds::new(-1.0))]
timeout: TimeoutSeconds,
}
macro_rules! acquire_lock_impl {
($mu:expr, $args:expr, $vm:expr) => {{
let (mu, args, vm) = ($mu, $args, $vm);
let timeout = match args.timeout {
Either::A(f) => f,
Either::B(i) => i as f64,
};
let timeout = args.timeout.to_secs_f64();
match args.blocking {
true if timeout == -1.0 => {
vm.allow_threads(|| mu.lock());
Ok(true)
}
true if timeout < 0.0 => {
Err(vm.new_value_error("timeout value must be positive".to_owned()))
Err(vm
.new_value_error("timeout value must be a non-negative number".to_owned()))
}
true => {
// modified from std::time::Duration::from_secs_f64 to avoid a panic.
// TODO: put this in the Duration::try_from_object impl, maybe?
let nanos = timeout * 1_000_000_000.0;
if timeout > TIMEOUT_MAX as f64 || nanos < 0.0 || !nanos.is_finite() {
return Err(vm.new_overflow_error(
"timestamp too large to convert to Rust Duration".to_owned(),
));
if timeout > TIMEOUT_MAX {
return Err(vm.new_overflow_error("timeout value is too large".to_owned()));
}
Ok(vm.allow_threads(|| mu.try_lock_for(Duration::from_secs_f64(timeout))))