Сопоставимо ли asyncio.loop.time () с datetime.datetime.now () и как? - PullRequest
2 голосов
/ 09 апреля 2019

Я надеюсь использовать asyncio.loop для установки обратных вызовов в определенное время.Моя проблема в том, что мне нужно планировать их на основе datetime.datetime объектов (UTC), но asyncio.loop.call_at() использует внутреннее эталонное время.

Быстрый тест на python 3.7.3, работающем в Ubuntu, показывает, что asyncio.loop.time()сообщает о работоспособности системы.Для конвертации моя первая мысль - наивно хранить эталонное время и использовать его позже:

from asyncio import new_event_loop
from datetime import datetime, timedelta

_loop = new_event_loop()
_loop_base_time = datetime.utcnow() - timedelta(seconds=_loop.time())

def schedule_at(when, callback, *args):
    _loop.call_at((when - _loop_base_time).total_seconds(), callback, *args)

Однако не ясно, стабильно ли это смещение (datetime.utcnow() - timedelta(seconds=loop.time())).Я понятия не имею, дрейфует ли время работы системы по сравнению с UTC, даже когда системные часы модифицированы (например, через обновления NTP).

Принимая это во внимание, это предназначено для мониторинга программного обеспечения, которое потенциально может работать месяцами.в то же время небольшие смещения могут быть очень значительными.Я должен отметить, что я видел системы, теряющие минуты в день без NTP-демона, и одноразовые обновления NTP могут смещать время на много минут за короткий промежуток времени.Так как я не знаю, синхронизируются ли эти два, неясно, насколько я должен быть обеспокоен.


Примечание: я знаю о проблеме Python с планированием событий более 24 часов вбудущее.Я смогу обойти это, сохраняя отдаленные будущие события в списке и опрашивая предстоящие события каждые 12 часов, планируя их только тогда, когда они будут <24 часа в будущем. </p>


Возможно линадежно конвертировать из datetime.datetime в asyncio.loop раз?или две системы времени несопоставимы?Если они сопоставимы, нужно ли что-то особенное делать, чтобы мои расчеты были правильными.

Ответы [ 2 ]

1 голос
/ 09 апреля 2019

Вы можете вычислить разницу в секундах, используя ту же временную структуру, которая используется для планирования, а затем использовать asyncio.call_later с вычисленной задержкой:

def schedule_at(when, callback, *args):
  delay = (when - datetime.utcnow()).total_seconds()
  _loop.call_later(delay, callback, *args)

Это обойдется вокруг вопросастабильна ли разница между временем цикла и utcnow;оно должно быть стабильным только между временем планирования задачи и временем ее выполнения (которое, согласно вашим заметкам, должно быть менее 12 часов).

Например: если внутренние часы цикла событий смещаются на 1 секунду с интервалом utcnow каждый час (преднамеренно экстремальный пример), вы будете дрейфовать максимум 12 секунд на задачу, но вы не накапливаете эту ошибку замесяцы выполнения.По сравнению с подходом использования фиксированной ссылки этот подход дает лучшую гарантию.

0 голосов
/ 09 апреля 2019

Альтернативным подходом было бы вообще не полагаться на внутренние часы цикла.Вы можете запустить задачу в фоновом режиме и периодически проверять, должен ли выполняться обратный вызов.

Неточность этого метода соответствует времени, которое вы ожидаете перед следующей проверкой, но я не думаю, что это критично, учитывая любые другие возможные неточности (например,Например, Python GC «останови мир».

Хорошая сторона в том, что вы не ограничены 24 часами.

Этот код показывает основную идею:

import asyncio
import datetime



class Timer:
    def __init__(self):
        self._callbacks = set()
        self._task = None

    def schedule_at(self, when, callback):        
        self._callbacks.add((when, callback,))
        if self._task is None:
            self._task = asyncio.create_task(self._checker())

    async def _checker(self):
        while True:
            await asyncio.sleep(0.01)
            self._exec_callbacks()

    def _exec_callbacks(self):
        ready_to_exec = self._get_ready_to_exec()
        self._callbacks -= ready_to_exec

        for _, callback in ready_to_exec:
            callback()

    def _get_ready_to_exec(self):
        now = datetime.datetime.utcnow()
        return {
            (when, callback,)
            for (when, callback,)
            in self._callbacks
            if when <= now
        }


timer = Timer()


async def main():
    now = datetime.datetime.utcnow()
    s1_after = now + datetime.timedelta(seconds=1)
    s3_after = now + datetime.timedelta(seconds=3)
    s5_after = now + datetime.timedelta(seconds=5)

    timer = Timer()
    timer.schedule_at(s1_after, lambda: print('Hey!'))
    timer.schedule_at(s3_after, lambda: print('Hey!'))
    timer.schedule_at(s5_after, lambda: print('Hey!'))

    await asyncio.sleep(6)


if __name__ ==  '__main__':
    asyncio.run(main())
...