Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using asyncio to run a function at the start (00 seconds) of every minute

I'm trying to run several functions at the same time (approximately or course) with different parameters and repeat that at the start of every minute.

I managed to get an asyncio example to run where I get a function callback to run at specific times with a different parameter, but what I can't figure out is how to run it (and keep running it forever) at very specific times (i.e. I want to run it at the start of every minute, so at 19:00:00, 19:01:00, etc..).

Asyncio call_at should be able to do that, but it uses a time format that is not the standard python time format and I can't figure out to specify that time format as the 00 seconds of the next minute.

import asyncio
import time


def callback(n, loop, msg):
    print(msg)
    print('callback {} invoked at {}'.format(n, loop.time()))


async def main(loop):
    now = loop.time()
    print('clock time: {}'.format(time.time()))
    print('loop  time: {}'.format(now))

    print('registering callbacks')
    loop.call_at(now + 0.2, callback, 1, loop, 'a')
    loop.call_at(now + 0.1, callback, 2, loop, 'b')
    loop.call_soon(callback, 3, loop, 'c')

    await asyncio.sleep(1)


event_loop = asyncio.get_event_loop()
try:
    print('entering event loop')
    event_loop.run_until_complete(main(event_loop))
finally:
    print('closing event loop')
    event_loop.close()
like image 242
jbssm Avatar asked Feb 28 '18 17:02

jbssm


1 Answers

As others have noted, there is no built-in functionality for that kind of thing, you will need to write it yourself. It is straightforward to implement, though - a simple version could look like this:

import asyncio, datetime

async def at_minute_start(cb):
    while True:
        now = datetime.datetime.now()
        after_minute = now.second + now.microsecond / 1_000_000
        if after_minute:
            await asyncio.sleep(60 - after_minute)
        cb()

This doesn't use call_later, it is a coroutine which can be canceled when no longer necessary. It simply takes the seconds value of the current wallclock time x, and sleeps (60 - x) to reach the next minute. Here is a test:

import time
loop = asyncio.get_event_loop()
loop.create_task(at_minute_start(lambda: print(time.asctime())))
loop.run_forever()

# output:
Wed Feb 28 21:36:00 2018
Wed Feb 28 21:37:00 2018
Wed Feb 28 21:38:00 2018
...

Unfortunately, the simple implementation can misbehave if asyncio.sleep ever happens to sleep a tiny bit shorter than the requested period, e.g. due to clock skew. In that case the subsequent asyncio.sleep will try to again reach the start of the same minute and sleep for only a fraction of a second, resulting in the callback effectively firing twice in quick succession. To prevent that, additional code is needed to compensate for the short sleeps:

async def at_minute_start(cb):
    now = datetime.datetime.now()
    wait_for_next_minute = False
    while True:
        after_minute = now.second + now.microsecond / 1_000_000
        if after_minute != 0:
            to_next_minute = 60 - after_minute
        else:
            to_next_minute = 0  # already at the minute start
        if wait_for_next_minute:
            to_next_minute += 60
        await asyncio.sleep(to_next_minute)
        cb()
        prev = now
        now = datetime.datetime.now()
        # if we're still at the same minute, our sleep was slightly
        # too short, so we'll need to wait an additional minute
        wait_for_next_minute = now.minute == prev.minute
like image 166
user4815162342 Avatar answered Sep 21 '22 10:09

user4815162342