From e38341826e70de1071beecb557484c4198c6de7d Mon Sep 17 00:00:00 2001 From: Hauke D Date: Sat, 4 Feb 2023 15:01:59 +0100 Subject: [PATCH 1/2] Add time.sleep_until() (GH #101558) Adds the `time.sleep_until` function, which allows sleeping until the specified absolute time. --- Doc/library/time.rst | 16 ++++ Lib/test/test_time.py | 12 +++ Misc/ACKS | 1 + ...-02-04-15-35-47.gh-issue-101558.ocOYkj.rst | 2 + Modules/timemodule.c | 85 +++++++++++++++---- 5 files changed, 100 insertions(+), 16 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-02-04-15-35-47.gh-issue-101558.ocOYkj.rst diff --git a/Doc/library/time.rst b/Doc/library/time.rst index 9f23a6fc7d5341..564679144990da 100644 --- a/Doc/library/time.rst +++ b/Doc/library/time.rst @@ -388,6 +388,22 @@ Functions by a signal, except if the signal handler raises an exception (see :pep:`475` for the rationale). +.. function:: sleep_until(secs) + + Like :func:`sleep`, but sleep until the specified time of the system clock (as + returned by :func:`time`). This can be used, for example, to schedule events + at a specific timestamp obtained from + :meth:`datetime.timestamp `. + + See the notes in :func:`sleep` on the behavior when interrupted and on accuracy. + Additional potential sources of inaccuracies include: + + * Because this function uses the system clock as a reference, this means the + reference clock is adjustable and may jump backwards. + * On Unix, if ``clock_nanosleep()`` is not available, the absolute timeout + is emulated using ``nanosleep()`` or ``select()``. + + .. versionadded:: 3.12 .. index:: single: % (percent); datetime format diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py index 02cc3f43a66a67..0675707bf6855c 100644 --- a/Lib/test/test_time.py +++ b/Lib/test/test_time.py @@ -159,6 +159,18 @@ def test_sleep(self): self.assertRaises(ValueError, time.sleep, -1) time.sleep(1.2) + def test_sleep_until(self): + start = time.time() + deadline = start + 2 + time.sleep_until(deadline) + stop = time.time() + delta = stop - deadline + # cargo-cult these 50ms from test_monotonic (bpo-20101) + self.assertGreater(delta, -0.050) + # allow sleep_until to take up to 1s longer than planned + # (e.g. in case the system is under heavy load during testing) + self.assertLess(delta, 1.000) + def test_epoch(self): # bpo-43869: Make sure that Python use the same Epoch on all platforms: # January 1, 1970, 00:00:00 (UTC). diff --git a/Misc/ACKS b/Misc/ACKS index 1e94d33a665e4c..39bd4c2232d0a6 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -403,6 +403,7 @@ Lisandro Dalcin Darren Dale Andrew Dalke Lars Damerow +Hauke Dämpfling Evan Dandrea Eric Daniel Scott David Daniels diff --git a/Misc/NEWS.d/next/Library/2023-02-04-15-35-47.gh-issue-101558.ocOYkj.rst b/Misc/NEWS.d/next/Library/2023-02-04-15-35-47.gh-issue-101558.ocOYkj.rst new file mode 100644 index 00000000000000..20ecbebc6a36d2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-02-04-15-35-47.gh-issue-101558.ocOYkj.rst @@ -0,0 +1,2 @@ +Added the :func:`time.sleep_until` function, which allows sleeping until the +specified absolute time. diff --git a/Modules/timemodule.c b/Modules/timemodule.c index c50e689bb6986c..e60d88adff7d5e 100644 --- a/Modules/timemodule.c +++ b/Modules/timemodule.c @@ -115,7 +115,7 @@ _PyTime_Init(void) /* Forward declarations */ -static int pysleep(_PyTime_t timeout); +static int pysleep(_PyTime_t timeout, int absolute); typedef struct { @@ -422,7 +422,7 @@ time_sleep(PyObject *self, PyObject *timeout_obj) "sleep length must be non-negative"); return NULL; } - if (pysleep(timeout) != 0) { + if (pysleep(timeout, 0) != 0) { return NULL; } Py_RETURN_NONE; @@ -434,6 +434,28 @@ PyDoc_STRVAR(sleep_doc, Delay execution for a given number of seconds. The argument may be\n\ a floating point number for subsecond precision."); +static PyObject * +time_sleep_until(PyObject *self, PyObject *deadline_obj) +{ + _PyTime_t deadline; + if (_PyTime_FromSecondsObject(&deadline, deadline_obj, _PyTime_ROUND_TIMEOUT)) + return NULL; + if (deadline < 0) { + PyErr_SetString(PyExc_ValueError, + "sleep_until deadline must be non-negative"); + return NULL; + } + if (pysleep(deadline, 1) != 0) { + return NULL; + } + Py_RETURN_NONE; +} + +PyDoc_STRVAR(sleep_until_doc, +"sleep_until(seconds)\n\ +\n\ +Delay execution until the specified system clock time."); + static PyStructSequence_Field struct_time_type_fields[] = { {"tm_year", "year, for example, 1993"}, {"tm_mon", "month of year, range [1, 12]"}, @@ -1868,6 +1890,7 @@ static PyMethodDef time_methods[] = { {"pthread_getcpuclockid", time_pthread_getcpuclockid, METH_VARARGS, pthread_getcpuclockid_doc}, #endif {"sleep", time_sleep, METH_O, sleep_doc}, + {"sleep_until", time_sleep_until, METH_O, sleep_until_doc}, {"gmtime", time_gmtime, METH_VARARGS, gmtime_doc}, {"localtime", time_localtime, METH_VARARGS, localtime_doc}, {"asctime", time_asctime, METH_VARARGS, asctime_doc}, @@ -2132,8 +2155,9 @@ PyInit_time(void) // time.sleep() implementation. // On error, raise an exception and return -1. // On success, return 0. +// If absolute==0, timeout is relative; otherwise timeout is absolute. static int -pysleep(_PyTime_t timeout) +pysleep(_PyTime_t timeout, int absolute) { assert(timeout >= 0); @@ -2145,13 +2169,27 @@ pysleep(_PyTime_t timeout) #else struct timeval timeout_tv; #endif - _PyTime_t deadline, monotonic; + _PyTime_t deadline, reference; int err = 0; - if (get_monotonic(&monotonic) < 0) { - return -1; + if (absolute) { + deadline = timeout; +#ifndef HAVE_CLOCK_NANOSLEEP + if (get_system_time(&reference) < 0) { + return -1; + } + timeout = deadline - reference; + if (timeout < 0) { + return 0; + } +#endif + } + else { + if (get_monotonic(&reference) < 0) { + return -1; + } + deadline = reference + timeout; } - deadline = monotonic + timeout; #ifdef HAVE_CLOCK_NANOSLEEP if (_PyTime_AsTimespec(deadline, &timeout_abs) < 0) { return -1; @@ -2174,7 +2212,8 @@ pysleep(_PyTime_t timeout) int ret; Py_BEGIN_ALLOW_THREADS #ifdef HAVE_CLOCK_NANOSLEEP - ret = clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &timeout_abs, NULL); + ret = clock_nanosleep(absolute ? CLOCK_REALTIME : CLOCK_MONOTONIC, + TIMER_ABSTIME, &timeout_abs, NULL); err = ret; #elif defined(HAVE_NANOSLEEP) ret = nanosleep(&timeout_ts, NULL); @@ -2201,10 +2240,17 @@ pysleep(_PyTime_t timeout) } #ifndef HAVE_CLOCK_NANOSLEEP - if (get_monotonic(&monotonic) < 0) { - return -1; + if (absolute) { + if (get_system_time(&reference) < 0) { + return -1; + } + } + else { + if (get_monotonic(&reference) < 0) { + return -1; + } } - timeout = deadline - monotonic; + timeout = deadline - reference; if (timeout < 0) { break; } @@ -2229,11 +2275,18 @@ pysleep(_PyTime_t timeout) return 0; } - LARGE_INTEGER relative_timeout; + LARGE_INTEGER due_time; // No need to check for integer overflow, both types are signed - assert(sizeof(relative_timeout) == sizeof(timeout_100ns)); - // SetWaitableTimer(): a negative due time indicates relative time - relative_timeout.QuadPart = -timeout_100ns; + assert(sizeof(due_time) == sizeof(timeout_100ns)); + if (absolute) { + // Adjust from Unix time (1970-01-01) to Windows time (1601-01-01) + // (the inverse of what is done in py_get_system_clock) + due_time.QuadPart = timeout_100ns + 116444736000000000; + } + else { + // SetWaitableTimer(): a negative due time indicates relative time + due_time.QuadPart = -timeout_100ns; + } HANDLE timer = CreateWaitableTimerExW(NULL, NULL, timer_flags, TIMER_ALL_ACCESS); @@ -2242,7 +2295,7 @@ pysleep(_PyTime_t timeout) return -1; } - if (!SetWaitableTimerEx(timer, &relative_timeout, + if (!SetWaitableTimerEx(timer, &due_time, 0, // no period; the timer is signaled once NULL, NULL, // no completion routine NULL, // no wake context; do not resume from suspend From ee5adf81aca3f92d631a6746ef88d178f9da3ca6 Mon Sep 17 00:00:00 2001 From: Hauke D Date: Mon, 10 Apr 2023 17:03:24 +0200 Subject: [PATCH 2/2] Apply PEP 7 coding style Co-authored-by: Victor Stinner --- Modules/timemodule.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/timemodule.c b/Modules/timemodule.c index e60d88adff7d5e..fce828d4272d58 100644 --- a/Modules/timemodule.c +++ b/Modules/timemodule.c @@ -438,8 +438,9 @@ static PyObject * time_sleep_until(PyObject *self, PyObject *deadline_obj) { _PyTime_t deadline; - if (_PyTime_FromSecondsObject(&deadline, deadline_obj, _PyTime_ROUND_TIMEOUT)) + if (_PyTime_FromSecondsObject(&deadline, deadline_obj, _PyTime_ROUND_TIMEOUT)) { return NULL; + } if (deadline < 0) { PyErr_SetString(PyExc_ValueError, "sleep_until deadline must be non-negative");