GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — gh-101558: Add time.sleep_until() by haukex · Pull Request #101559 · python/cpython

gh-101558: Add time.sleep_until() #101559

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.

Sign up for GitHub

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

Open
haukex wants to merge 2 commits into python:main
base: main
Loading
Loading
from haukex:sleep_until

Uh oh!

There was an error while loading. Please reload this page.

Open

gh-101558: Add time.sleep_until() #101559

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
2 commits
Select commit Hold shift + click to select a range
e383418
Add time.sleep_until() (GH #101558)
haukex Feb 4, 2023
ee5adf8
Apply PEP 7 coding style
haukex Apr 10, 2023
File filter
Filter by extension

Clear filters
Conversations
Failed to load comments.
Loading

Uh oh!

There was an error while loading. Please reload this page.

Jump to
Jump to file
Failed to load files.
Loading

Uh oh!

There was an error while loading. Please reload this page.

Diff view
Diff view

There are no files selected for viewing

16 changes: 16 additions & 0 deletions 16 Doc/library/time.rst
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread

@vstinner vstinner May 23, 2023

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced from this documentation what is the benefit of using this function instead of existing code:

dt = deadline - time.time()
if dt > 0: time.sleep(dt)

How does it behave differently? If it does behave the same, the function has no benefit?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@haukex haukex May 26, 2023

Copy link
Copy Markdown
Contributor Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is your point about improving the documentation or are you questioning the usefulness of the function in general? I'd be happy to improve the documentation if that's all that's missing, but I'd prefer to finalize the implementation first (see above).

The code you showed does differ from sleep_until in at least one IMHO significant way: The fact that sleep_until (when backed by the SetWaitableTimerEx or clock_nanosleep implementations) is sensitive to changes to the system clock is actually desirable in some situations. Imagine for example a measurement system consisting of multiple processes on a single machine, or spread across several machines, where time is kept synchronized by NTP, running for several days with a measurement interval on the order of minutes. On this timescale, differences in the clocks of a few ms don't matter, but if one of the measurement processes were to drift off relative to the others, as it could with the code you showed, that wouldn't be too good.

Other than that, for short measurement intervals, in theory the code you showed introduces a small offset due to there being several instructions in between taking the current time and scheduling the sleep, which may make a difference on a heavily loaded machine.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
at a specific timestamp obtained from
:meth:`datetime.timestamp <datetime.datetime.timestamp>`.
Comment thread
haukex marked this conversation as resolved.
Resolved

Uh oh!

There was an error while loading. Please reload this page.


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.
Comment thread

@vstinner vstinner Apr 5, 2023

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unclear to me what is the expected behavior when the system clock jumps backward or forward. Does the function sleeps longer/shorter?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@skotchandsoda skotchandsoda Apr 19, 2024

Copy link
Copy Markdown

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a caller asks to sleep until an absolute time on a given clock and that clock then "jumps", the caller should still wake up when that absolute time has elapsed. For example, if the clock is set backwards after the caller blocks, then the caller should sleep for a longer duration because it will take a longer time for the clock to reach the chosen absolute value. This is not a violation of the caller's request: they asked to block until an absolute time, not to block for a given duration.

If sleeping a longer/shorter duration is unacceptable for a caller, then either (a) the caller should use a relative sleep, or (b) the caller should schedule the sleep against a clock that cannot "jump".

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
* On Unix, if ``clock_nanosleep()`` is not available, the absolute timeout
is emulated using ``nanosleep()`` or ``select()``.
Comment thread
Comment on lines +403 to +404

@vstinner vstinner Apr 5, 2023

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you consider that it's important to document the implementation, I suggest to do it in a separated section, since it's unclear to me what's the relationship between the chosen implementation and inaccuracies. If you care about the theorical clock resolution, you should document it, it's not obviously to users what is the resolution of these functions.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

.. versionadded:: 3.12

.. index::
single: % (percent); datetime format
Expand Down
12 changes: 12 additions & 0 deletions 12 Lib/test/test_time.py
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment thread

@vstinner vstinner Apr 5, 2023

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my experience, such test tends to fail on busy buildbots. I would prefer to not test the "performance" (accuracy) of the function in such unit test.

I'm not sure that it's useful to write an unit test for this function. It's very hard to write a reliable on the clock accuracy.

What if the system clock changes during the test? :-(

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@haukex haukex Apr 10, 2023

Copy link
Copy Markdown
Contributor Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree, and I actually already had to increase that final assertLess because the test failed on a busy runner with a smaller value. When writing the test I took my cue from tests like test_monotonic, but I'm also fine with simplifying the test case, let me know.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

def test_epoch(self):
# bpo-43869: Make sure that Python use the same Epoch on all platforms:
# January 1, 1970, 00:00:00 (UTC).
Expand Down
1 change: 1 addition & 0 deletions 1 Misc/ACKS
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ Lisandro Dalcin
Darren Dale
Andrew Dalke
Lars Damerow
Hauke Dämpfling
Evan Dandrea
Comment thread
haukex marked this conversation as resolved.
Resolved

Uh oh!

There was an error while loading. Please reload this page.

Eric Daniel
Scott David Daniels
Expand Down
2 changes: 2 additions & 0 deletions 2 Misc/NEWS.d/next/Library/2023-02-04-15-35-47.gh-issue-101558.ocOYkj.rst
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added the :func:`time.sleep_until` function, which allows sleeping until the
specified absolute time.
86 changes: 70 additions & 16 deletions 86 Modules/timemodule.c
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand All @@ -434,6 +434,29 @@ 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]"},
Expand Down Expand Up @@ -1868,6 +1891,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},
Expand Down Expand Up @@ -2132,8 +2156,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);

Expand All @@ -2145,13 +2170,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;
Expand All @@ -2174,7 +2213,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);
Comment thread

@Livius90 Livius90 Feb 9, 2023

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why need to use CLOCK_REALTIME? @vstinner mentioned in a mailing thread that CLOCK_REALTIME can jump forwards and backwards as the system time-of-day clock is changed.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@haukex haukex Feb 12, 2023 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quoting my reply from the email:

Yes, using the system clock has its caveats, which is why I mentioned it in the documentation, and I wouldn't mind highlighting these even more.

The use case in my specific case is long-running recurring measurements, for example a measurement every minute or every ten minutes over a period of many hours or often days. In these cases it makes much more sense to tie the measurements to wall-clock time, because often there are other measurement systems also using wall-clock time.

Although of course not everyone uses it, an NTP daemon like Chrony is able to adjust the system clock by changing the system clock frequency and thereby adjusting the system clock gradually, which keeps the system clock monotonic. Having multiple measurement computers stay synchronized via NTP is then much more reasonable. In the past I've built a system consisting of multiple machines, one of them using a GPS clock and acting as an NTP server, that does exactly this.

Since both clock_nanosleep and SetWaitableTimerEx allow sleeping until a specific time, my reasoning is simply that this functionality available in the lower-level API could be made available to the user so they can write simple loops - aside from the precision of clock_nanosleep appealing to my perfectionist side 😉

While there are schedulers like cron or similar, sometimes, a measurement may need to happen e.g. every second, where then the (admittedly small) inaccuracy introduced by calculating the sleep() timeout based on the current time can start to make a difference.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
err = ret;
#elif defined(HAVE_NANOSLEEP)
ret = nanosleep(&timeout_ts, NULL);
Expand All @@ -2201,10 +2241,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;
}
}
timeout = deadline - monotonic;
else {
if (get_monotonic(&reference) < 0) {
return -1;
}
}
timeout = deadline - reference;
if (timeout < 0) {
break;
}
Expand All @@ -2229,11 +2276,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);
Expand All @@ -2242,7 +2296,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
Expand Down
Add this suggestion to a batch that can be applied as a single commit. This suggestion is invalid because no changes were made to the code. Suggestions cannot be applied while the pull request is closed. Suggestions cannot be applied while viewing a subset of changes. Only one suggestion per line can be applied in a batch. Add this suggestion to a batch that can be applied as a single commit. Applying suggestions on deleted lines is not supported. You must change the existing code in this line in order to create a valid suggestion. Outdated suggestions cannot be applied. This suggestion has been applied or marked resolved. Suggestions cannot be applied from pending reviews. Suggestions cannot be applied on multi-line comments. Suggestions cannot be applied while the pull request is queued to merge. Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.