Skip to content

Commit 96110f8

Browse files
committed
Add time.sleep_until() (GH #101558)
Adds the `time.sleep_until` function, which allows sleeping until the specified absolute time.
1 parent 6ef6915 commit 96110f8

File tree

5 files changed

+100
-16
lines changed

5 files changed

+100
-16
lines changed

Doc/library/time.rst

+16
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,22 @@ Functions
388388
by a signal, except if the signal handler raises an exception (see
389389
:pep:`475` for the rationale).
390390

391+
.. function:: sleep_until(secs)
392+
393+
Like :func:`sleep`, but sleep until the specified time of the system clock (as
394+
returned by :func:`time`). This can be used, for example, to schedule events
395+
at a specific timestamp obtained from
396+
:meth:`datetime.timestamp <datetime.datetime.timestamp>`.
397+
398+
See the notes in :func:`sleep` on the behavior when interrupted and on accuracy.
399+
Additional potential sources of inaccuracies include:
400+
401+
* Because this function uses the system clock as a reference, this means the
402+
reference clock is adjustable and may jump backwards.
403+
* On Unix, if ``clock_nanosleep()`` is not available, the absolute timeout
404+
is emulated using ``nanosleep()`` or ``select()``.
405+
406+
.. versionadded:: 3.12
391407

392408
.. index::
393409
single: % (percent); datetime format

Lib/test/test_time.py

+12
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,18 @@ def test_sleep(self):
159159
self.assertRaises(ValueError, time.sleep, -1)
160160
time.sleep(1.2)
161161

162+
def test_sleep_until(self):
163+
start = time.time()
164+
deadline = start + 2
165+
time.sleep_until(deadline)
166+
stop = time.time()
167+
delta = stop - deadline
168+
# cargo-cult these 50ms from test_monotonic (bpo-20101)
169+
self.assertGreater(delta, -0.050)
170+
# allow sleep_until to take up to 1s longer than planned
171+
# (e.g. in case the system is under heavy load during testing)
172+
self.assertLess(delta, 1.000)
173+
162174
def test_epoch(self):
163175
# bpo-43869: Make sure that Python use the same Epoch on all platforms:
164176
# January 1, 1970, 00:00:00 (UTC).

Misc/ACKS

+1
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,7 @@ Lisandro Dalcin
402402
Darren Dale
403403
Andrew Dalke
404404
Lars Damerow
405+
Hauke Dämpfling
405406
Evan Dandrea
406407
Eric Daniel
407408
Scott David Daniels
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Added the :func:`time.sleep_until` function, which allows sleeping until the
2+
specified absolute time.

Modules/timemodule.c

+69-16
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ _PyTime_Init(void)
113113

114114

115115
/* Forward declarations */
116-
static int pysleep(_PyTime_t timeout);
116+
static int pysleep(_PyTime_t timeout, int absolute);
117117

118118

119119
typedef struct {
@@ -420,7 +420,7 @@ time_sleep(PyObject *self, PyObject *timeout_obj)
420420
"sleep length must be non-negative");
421421
return NULL;
422422
}
423-
if (pysleep(timeout) != 0) {
423+
if (pysleep(timeout, 0) != 0) {
424424
return NULL;
425425
}
426426
Py_RETURN_NONE;
@@ -432,6 +432,28 @@ PyDoc_STRVAR(sleep_doc,
432432
Delay execution for a given number of seconds. The argument may be\n\
433433
a floating point number for subsecond precision.");
434434

435+
static PyObject *
436+
time_sleep_until(PyObject *self, PyObject *deadline_obj)
437+
{
438+
_PyTime_t deadline;
439+
if (_PyTime_FromSecondsObject(&deadline, deadline_obj, _PyTime_ROUND_TIMEOUT))
440+
return NULL;
441+
if (deadline < 0) {
442+
PyErr_SetString(PyExc_ValueError,
443+
"sleep_until deadline must be non-negative");
444+
return NULL;
445+
}
446+
if (pysleep(deadline, 1) != 0) {
447+
return NULL;
448+
}
449+
Py_RETURN_NONE;
450+
}
451+
452+
PyDoc_STRVAR(sleep_until_doc,
453+
"sleep_until(seconds)\n\
454+
\n\
455+
Delay execution until the specified system clock time.");
456+
435457
static PyStructSequence_Field struct_time_type_fields[] = {
436458
{"tm_year", "year, for example, 1993"},
437459
{"tm_mon", "month of year, range [1, 12]"},
@@ -1862,6 +1884,7 @@ static PyMethodDef time_methods[] = {
18621884
{"pthread_getcpuclockid", time_pthread_getcpuclockid, METH_VARARGS, pthread_getcpuclockid_doc},
18631885
#endif
18641886
{"sleep", time_sleep, METH_O, sleep_doc},
1887+
{"sleep_until", time_sleep_until, METH_O, sleep_until_doc},
18651888
{"gmtime", time_gmtime, METH_VARARGS, gmtime_doc},
18661889
{"localtime", time_localtime, METH_VARARGS, localtime_doc},
18671890
{"asctime", time_asctime, METH_VARARGS, asctime_doc},
@@ -2126,8 +2149,9 @@ PyInit_time(void)
21262149
// time.sleep() implementation.
21272150
// On error, raise an exception and return -1.
21282151
// On success, return 0.
2152+
// If absolute==0, timeout is relative; otherwise timeout is absolute.
21292153
static int
2130-
pysleep(_PyTime_t timeout)
2154+
pysleep(_PyTime_t timeout, int absolute)
21312155
{
21322156
assert(timeout >= 0);
21332157

@@ -2139,13 +2163,27 @@ pysleep(_PyTime_t timeout)
21392163
#else
21402164
struct timeval timeout_tv;
21412165
#endif
2142-
_PyTime_t deadline, monotonic;
2166+
_PyTime_t deadline, reference;
21432167
int err = 0;
21442168

2145-
if (get_monotonic(&monotonic) < 0) {
2146-
return -1;
2169+
if (absolute) {
2170+
deadline = timeout;
2171+
#ifndef HAVE_CLOCK_NANOSLEEP
2172+
if (get_system_time(&reference) < 0) {
2173+
return -1;
2174+
}
2175+
timeout = deadline - reference;
2176+
if (timeout < 0) {
2177+
return 0;
2178+
}
2179+
#endif
2180+
}
2181+
else {
2182+
if (get_monotonic(&reference) < 0) {
2183+
return -1;
2184+
}
2185+
deadline = reference + timeout;
21472186
}
2148-
deadline = monotonic + timeout;
21492187
#ifdef HAVE_CLOCK_NANOSLEEP
21502188
if (_PyTime_AsTimespec(deadline, &timeout_abs) < 0) {
21512189
return -1;
@@ -2168,7 +2206,8 @@ pysleep(_PyTime_t timeout)
21682206
int ret;
21692207
Py_BEGIN_ALLOW_THREADS
21702208
#ifdef HAVE_CLOCK_NANOSLEEP
2171-
ret = clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &timeout_abs, NULL);
2209+
ret = clock_nanosleep(absolute ? CLOCK_REALTIME : CLOCK_MONOTONIC,
2210+
TIMER_ABSTIME, &timeout_abs, NULL);
21722211
err = ret;
21732212
#elif defined(HAVE_NANOSLEEP)
21742213
ret = nanosleep(&timeout_ts, NULL);
@@ -2195,10 +2234,17 @@ pysleep(_PyTime_t timeout)
21952234
}
21962235

21972236
#ifndef HAVE_CLOCK_NANOSLEEP
2198-
if (get_monotonic(&monotonic) < 0) {
2199-
return -1;
2237+
if (absolute) {
2238+
if (get_system_time(&reference) < 0) {
2239+
return -1;
2240+
}
2241+
}
2242+
else {
2243+
if (get_monotonic(&reference) < 0) {
2244+
return -1;
2245+
}
22002246
}
2201-
timeout = deadline - monotonic;
2247+
timeout = deadline - reference;
22022248
if (timeout < 0) {
22032249
break;
22042250
}
@@ -2223,11 +2269,18 @@ pysleep(_PyTime_t timeout)
22232269
return 0;
22242270
}
22252271

2226-
LARGE_INTEGER relative_timeout;
2272+
LARGE_INTEGER due_time;
22272273
// No need to check for integer overflow, both types are signed
2228-
assert(sizeof(relative_timeout) == sizeof(timeout_100ns));
2229-
// SetWaitableTimer(): a negative due time indicates relative time
2230-
relative_timeout.QuadPart = -timeout_100ns;
2274+
assert(sizeof(due_time) == sizeof(timeout_100ns));
2275+
if (absolute) {
2276+
// Adjust from Unix time (1970-01-01) to Windows time (1601-01-01)
2277+
// (the inverse of what is done in py_get_system_clock)
2278+
due_time.QuadPart = timeout_100ns + 116444736000000000;
2279+
}
2280+
else {
2281+
// SetWaitableTimer(): a negative due time indicates relative time
2282+
due_time.QuadPart = -timeout_100ns;
2283+
}
22312284

22322285
HANDLE timer = CreateWaitableTimerExW(NULL, NULL, timer_flags,
22332286
TIMER_ALL_ACCESS);
@@ -2236,7 +2289,7 @@ pysleep(_PyTime_t timeout)
22362289
return -1;
22372290
}
22382291

2239-
if (!SetWaitableTimerEx(timer, &relative_timeout,
2292+
if (!SetWaitableTimerEx(timer, &due_time,
22402293
0, // no period; the timer is signaled once
22412294
NULL, NULL, // no completion routine
22422295
NULL, // no wake context; do not resume from suspend

0 commit comments

Comments
 (0)