0

TL;DR threading.Timer uses system time but the time changes while I'm using it, how can I get it to use system uptime?

I have a Python script that does a bunch of stuff one of which is set the system time. When this script starts up the time is wrong. This script also needs to have a global timeout of 30 seconds.

I have been using the following timeout class:

class Timeout(object):
    def __init__(self, seconds=1, signum=signal.SIGUSR1, exception=TimeoutException):
        self.exception = exception
        self.pid = os.getpid()
        self.signum = signum
        self.timer = threading.Timer(seconds, self.exit_function)

    def exit_function(self):
        os.kill(self.pid, self.signum)

    def handle_timeout(self, signum, frame):
        raise self.exception()

    def __enter__(self):
        signal.signal(self.signum, self.handle_timeout)
        self.timer.start()

    def __exit__(self, type, value, traceback):
        self.timer.cancel()

Which wraps my entire script:

with Timeout(seconds=30):
    main()

occasionally the script fails really quickly or never gets killed after the 30 seconds. I believe this is because threading.Timer uses the system time which gets changed while the script is running. Is there anyway I can get it to use system uptime?

Ross MacArthur
  • 4,735
  • 1
  • 24
  • 36
  • 1
    Possible duplicate of [How to implement a watchdog timer in Python?](https://stackoverflow.com/questions/16148735/how-to-implement-a-watchdog-timer-in-python) – Skandix Feb 14 '18 at 08:05
  • 1
    Not a duplicate, all those examples also use system time, I want to use system uptime like that is is `/proc/uptime` – Ross MacArthur Feb 14 '18 at 12:07
  • 2
    `threading.Timer` are based on condition locks, based on a monotonic timer which saids: *The clock is not affected by system clock updates.* So this is not your issue. You don't need system-uptime - you allready have an independent time – Skandix Feb 14 '18 at 13:17
  • 1
    Hmmm I am running `Debian Linux 3.10.103-marvell armv7l GNU/Linux` and Python 2.7.9. If I change the time it affects the `Event.interval` call inside `threading.Timer`. – Ross MacArthur Feb 14 '18 at 13:32
  • yeah, just found that you probably have python < 3.3. Pretty intresting topic. I may have a approach for you in the given answer. EDIT: oh crap - you are using linux – Skandix Feb 14 '18 at 13:50

2 Answers2

3

Update

What I am doing now is to monkey patch threading._time with a monotonic function from the monotonic package on PyPI.

import threading
import monotonic

threading._time = monotonic.monotonic

Original answer

I ended up extending threading.Timer to use system uptime.

class Timer(threading._Timer):

    def __init__(self, *args, **kwargs):
        super(Timer, self).__init__(*args, **kwargs)

        # only works on Linux
        self._libc = ctypes.CDLL('libc.so.6')
        self._buf = ctypes.create_string_buffer(128)

    def uptime(self):
        self._libc.sysinfo(self._buf)
        return struct.unpack_from('@l', self._buf.raw)[0]

    def run(self):
        start_time = self.uptime()
        while not self.finished.is_set():
            time.sleep(0.1)
            if self.uptime() - start_time > self.interval:
                self.function(*self.args, **self.kwargs)
                break
        self.finished.set()
Ross MacArthur
  • 4,735
  • 1
  • 24
  • 36
0

It seem like your are using Python < 3.3. On Python 3.3 or newer, monotonic will be an alias of time.monotonic from the standard library. time.monotonic was then also used for the threading-libary. As said in the docs:

Return the value (in fractional seconds) of a monotonic clock, i.e. a clock that cannot go backwards. The clock is not affected by system clock updates.

So, with Python >=3.3 threading.Timer will be independent.


trying to solve your problem: Windows

I see an option to get your desired system-uptime using gettickcount from the kernel32.dll using ctypes:

import ctypes
kernel_32 = ctypes.cdll.LoadLibrary("Kernel32.dll")
kernel_32.GetTickCount()  # overflow after 49.7 days
kernel_32.GetTickCount64()

with this you could create your own timer with something unperformant as this:

def sleep(time):
    start = kernel_32.GetTickCount64()
    end = start + time
    while kernel_32.GetTickCount64() < end:
        pass
    print('done')

I realy hope this approach helps with your problem - good luck


EDIT: Linux

based on monotonic: you can try this as alternative to getTickCount1:

try:
    clock_gettime = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True).clock_gettime
except Exception:
    clock_gettime = ctypes.CDLL(ctypes.util.find_library('rt'), use_errno=True).clock_gettime

class timespec(ctypes.Structure):
    """Time specification, as described in clock_gettime(3)."""
    _fields_ = (('tv_sec', ctypes.c_long), ('tv_nsec', ctypes.c_long))

if sys.platform.startswith('linux'):
    CLOCK_MONOTONIC = 1
elif sys.platform.startswith('freebsd'):
    CLOCK_MONOTONIC = 4
elif sys.platform.startswith('sunos5'):
    CLOCK_MONOTONIC = 4
elif 'bsd' in sys.platform:
    CLOCK_MONOTONIC = 3
elif sys.platform.startswith('aix'):
    CLOCK_MONOTONIC = ctypes.c_longlong(10)

def monotonic():
    """Monotonic clock, cannot go backward."""
    ts = timespec()
    if clock_gettime(CLOCK_MONOTONIC, ctypes.pointer(ts)):
        errno = ctypes.get_errno()
        raise OSError(errno, os.strerror(errno))
    return ts.tv_sec + ts.tv_nsec / 1.0e9

1: Copyright 2014, 2015, 2016 Ori Livneh Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

Skandix
  • 1,916
  • 6
  • 27
  • 36