Skip to content
This repository was archived by the owner on Mar 2, 2025. It is now read-only.

Таймеры

do- edited this page Nov 15, 2021 · 95 revisions

Таймер в Dia.js -- объект, используемый для запуска заданной функции

  • в отрыве от процесса-инициатора;
  • с защитой от параллельного исполнения;
  • не ранее заданного времени
    • хотя, возможно, существенно позже - с учётом однозадачности и прочих дополнительных требований.
При поступлении многих конкурирующих запросов на исполнение запоминается и исполняется только один из них. То есть в первом приближении таймер ведёт себя как FIFO-буфер объёмом 1. Но из множества заявок на разные моменты моменты в будущем истолняется не та, которая поступила первой (или последней), а та, у которой указан самый ранний требуемый момент. Другие дополнительные ограничения описаны ниже.

При помощи таймеров реализованы очереди и в норме в коде приложения непосредственно должны использоваться именно они.

Настоящая статья по большей части написана для разъяснения устройства этого внутреннего механизма.

Table of Contents

API

Конструктор

  new Timer ({...options})

Опции:

Имя Тип Обязательность Описание Примечание
conf объект конфигурации обязательно Параметр, устанавливающий связь с остальными подсистемами Dia.js (протоколирование и т. п.)
name string обязательно Обязательный параметр: уникальное имя таймера в рамках приложения. Для очереди совпадает с её именем.
todo async function или [Handler, params] обязательно Вызываемая функция или подзапрос
tolerance Number Максимальное число отказов до принудительного останова 0 преобразуется в 1, подробнее см. очереди
period Number или [Number] Минимальный период между последовательными вызовами, мс О значении типа Array см. очереди
is_paused Boolean Создать ли таймер изначально в режиме паузы
from string 'HH:MM:SS' Начало допустимого времения суток указывается одновременно с to
to string 'HH:MM:SS' Окончание допустимого времения суток указывается одновременно с from
ticker function () Если установлена, вызывается для вычисления следующего момента запуска Используется для эмуляции cron
on_change function (state) Функция, вызываемая при изменении состояния таймера Предусмотрена для публикации этих данных

Подробнее об опции todo

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

При создании таймера вручную в рамках Dia.js-приложения обычно такое действие оформляется в виде подзапроса и, соответственно, сводится к запуску обработчика с фиксированным набором параметров тип и действие. В такой ситуации можно указать эту пару прямо в виде значения todo. Например:

return new Timer ({
  conf,
  name: 'migration_' + type,
  label: 'migration step',
  todo: [Async, {rq: {type, action: 'update'}}],
  log_meta: {parent: this.log_event},
}).promise ()

Для очереди же todo-функция создаётся автоматически: помимо запуска подзапроса с заданными именами типа и действия, они включают обращения к БД.

Подробнее об опции ticker

Вообще таймер, описываемый в настоящей статье, предназначен только для упорядочения во времени асинхронно поступающих конкурирующих внешних запросов. Сам по себе он источником запросов не является.

Однако для реализации периодических процедур (по аналогии с cron) такой источник может быть задан. Это должна быть функция без параметров, возвращающая объекты Date или соответствующие числа. В норме это должны быть даты, бОльшие текущей физической.

Типичный (и, возможно, единственный) пример - функция, вычисляющая ближайший будущий момент согласно некоторому cron-подобному расписанию.

Если такая функция задана, то она вызывается и для её результата планируется запуск в двух ситуациях:

  • при создании таймера;
  • после завершения каждого вызова todo, если в этот момент вызов таймера не запланирован иным способом.

Методы

Здесь (пока) перечислены только методы, которые могут понадобиться при использовании объектов данного класса.

Имя, параметры Описание Примечание
at (ts, comment)
Установка на момент не ранее ts, с комментарием comment ts может быть объектом Date или числом, соответствующим Date.getTime ()
in (ms, comment)
Установка не менее, чем через mc мс, с комментарием comment
on (comment)
Установка на ближайший возможный момент, с комментарием comment синоним in (0, comment)
promise (comment)
Запуск в виде объекта типа Promise до первой ошибки или полной остановки
clear (comment)
Отмена текущей установки, с комментарием comment Если таймер был установлен, то соответствующий момент в виде Date, иначе null
pause (error)
Установка на паузу по поводу ошибки error (если задана) Если таймер уже был на паузе, его состояние не меняется
resume (comment)
Отмена режима паузы, с комментарием comment Если таймер не был на паузе, его состояние не меняется
from_to (from, to)
Установка времени суток от from до to или его отмена для (null, null)

События

Класс Timer наследует EventEmitter. В силу исторических причин, метод on задействован для установки таймера. Однако назначение обработчиков событий возможно посредством addListener. Кроме того, обработчик change можно назначить при создании объекта, указав опцию on_change.

Имя Параметры Описание
stop Вызов todo завершён, на будущее таймер не установлен
change state, name Состояние таймера name приняло значение state

Структура state события change

Значения state имеют тип string, не Object. Это строки в формате JSON. Сериализация используется, в частности, для обеспечения уникальности значений: если при двух подряд событиях отображаемые поля не меняются, второй раз change не вызывается.

После применения JSON.parse получается структура со следующими полями (любое может отсутствовать):

Имя Тип Описание
ts_scheduled Строка Date.toJSON () Момент запланированного запуска
is_busy true или отсутствует Исполняется ли todo в настоящее время
is_paused true или отсутствует стоит ли очередь на паузе
ts_paused Строка Date.toJSON () если очередь на паузе, то с какого момента
error Строка если очередь на паузе из-за ошибки, то текст ошибки

Описание реализации

Объект класса Timer в основном занимается созданием вложенных объектов, описанных в подразделах ниже. Каждый из таких объектов имеет поле timer: ссылку на родительский объект, через которую он получает доступ ко всем остальным деталям данного механизма.

Timer/Executor: исполнитель функции

При создании таймера параметр

Имя Тип Обязательность Описание Примечание
todo function обязательно Вызываемая функция асинхронная

передаётся в конструктор объекта Timer/Executor, который запоминается в поле executor родительского объекта и используется в качестве обёртки для этой функции, предотвращающей её параллельное исполнение.

Вызов todo производится методом executor.run () (асинхронным, как сама todo).

В поле executor.is_busy хранится статус: во время исполнения todo оно имеет значение true, до начала и по окончании -- false.

Если запуск executor.run () вызывается в то время, когда is_busy === true, то todo не вызывается, однако устанавливается дополнительный флаг executor.is_to_reset = true. Конкурирующих вызовов executor.run () может быть сколько угодно, результат будет один: моментальный выход с установкой is_to_reset.

По окончании (успешном или аварийном) исполнения todo, executor проверяет значение is_to_reset и

  • если оно истинно
    • то есть было запрошено исполнение todo в тот момент, когда оно оказалось запрещено), то
  • родительский timer вновь устанавливается .on ()
    • то есть планируется исполнение той же todo в ближайший допустимый момент.
Timer/Executor тоже наследует EventEmitter и публикует следующие события
Имя Параметры Описание
start Перед запуском todo (который не будет отменён)
data data - результат todo После успешного вызова
error error После получения ошибки
finish После data или error

На событие finish подписан родительский объект-таймер. Если при finish у него не запланирован запуск и задан ticker, то планируется запуск в соответствии с результатом ticker ().

Timer/PlannedEvent: отдельный факт запуска

Жизненный цикл запуска функции todo от его инициирования методами at / in / on до окончания -- воплощаяется в виде объекта класса Timer/PlannedEvent.

Поле status такого объекта может принимать одно из 5 значений:

Имя Описание
ST_NEW От вызова конструктора до успешной установки или досрочной отмены
ST_SCHEDULED Вызов запланирован: setTimeout вызван и его результат записан в поле timeout
ST_CANCELLING Запрошена отмена пока timeout ещё не успел сработать
ST_RUNNING timeout сработал и todo в процессе исполнения
ST_FINISHED Жизненный цикл окончен

Объект типа Timer имеет 2 поля, ссылающихся на Timer/PlannedEvent:

Имя Статус Описание
scheduled_event ST_SCHEDULED Событие, для которого в данный момент существует неотменённый результат setTimeout
running_event ST_RUNNING Событие, соответствующее текущему процессу в executor

Переходя в один из двух указанных статусов, события сами устанавливают ссылки на себя из родительского объекта, покидая его -- меняют их на null.

Процесс создания

Объект Timer/PlannedEvent создаётся в рамках вызова timer.at () (в том числе через посредство timer.in () и timer.on ()).

Изначально для него устанавливается status = ST_NEW, однако увидеть объект в этотм статусе никакой другой процесс не может, поскольку прямо внутри конструктора вызывается синхронный метод schedule, по ходу которого:

  • запрошенный момент уточняется (переносится в будущее) с учётом ограничений (об этом ниже);
  • если выявляется конфликтующее событие с более ранним или таким же моментом (см. следующий раздел), статус переводится ST_FINISHED и процесс завершается;
  • если же проблем нет, сразу после установки таймера статус меняется на ST_SCHEDULED.

Системный таймер

В рамках этой статьи несколько раз упоминается стандартная функция setTimeout. Строго говоря, используется не всегда она. Возможны 2 ситуации:

  • требуемый момент всё ещё находится в будущем - тогда действительно вызывается setTimeout;
  • иначе (если ушло на расчёты самого таймера или изначально был запрошен момент в прошлом) применяется setimmediate.
Функциональной разницы здесь нет, так что в остальных местах документации говорится лишь о setTimeout.

Разрешение конфликтов

Если перед попыткой перехода в ST_SCHEDULED событие обнаруживает, что родительское поле scheduled_event не пусто, оно извлекает оттуда плановую дату и:

  • если имеющееся событие запланировано на болеее ранний или тот же момент, что требуется сейчас -- сразу переходит в ST_FINISHED (без попытки запуска todo)
  • иначе оно инициирует отмену имеющегося события (по ходму чего вызывается clearTimeout) и, вызвав свой setTimeout, ставит себя на его место.
Таким образом, в каждый момент времени на один таймер может быть не более одного активного setTimeout, причём из всех конкурирующих вызовов будет установлен наиболее ранний момент.

Timer/Pause: информация о временном останове

Пара методов

Имя, параметры Описание Примечание
pause (error)
Установка на паузу по поводу ошибки error (если задана) Если таймер уже был на паузе, его состояние не меняется
resume (comment)
Отмена режима паузы, с комментарием comment Если таймер не был на паузе, его состояние не меняется

позволяют приостановить работу таймера и возобновить её. Если при создании объекта указывается опция is_paused: true, то метод pause вызывается внутри контруктора.

Информация о состоянии таймера на момент вызова метода pause сохраняется в поле таймера current_pause в виде объекта класса Timer/Pause. После вызова resume это поле устанавливается в null.

Правила выхода из паузы

Если при вызове pause executor исполнял todo, причём у него был установлен флаг is_to_reset, то в случае исполнения resume таймер устанавливается на ближайший момент.

Иначе, если при вызове pause значение поля scheduled_event было не пусто, соответствующий момент времени запоминается и в случае исполнения resume таймер устанавливается на ближайший допустимый к нему момент.

Если ни то, ни другое, но установлен ticker -- то при выходе из паузы вызов планируется в соответствии с ним.

Работа таймера в режиме паузы

Если executor.run () вызывается в то время, как установлена пауза, он сразу завершается с установкой timer.current_pause.is_to_reset. (Хотя на текущий процесс исполнения executor.run () вызов pause никак не влияет: запущенный процесс не прерывается).

Вызовы at () / in () / on () в режиме паузы обрабатываются, как обычно: они приводят к созданию Timer/PlannedEvent. Но если в запланированный момент пауза не снята, вместо исполнения todo происходит процесс, описанный в предыдыщем абзаце.

Функция ticker в режиме паузы не вызывается.

Timer/Throttle: регулятор пропускной способности

Для реализации ограничений на частоту последовательных запусков предусмотрен сохраняемый в поле throttle объект класса Timer/Throttle, которому при создании передаются исходные опции таймера:

Имя Тип Обязательность Описание Примечание
tolerance Number Максимальное число отказов до принудительного останова 0 преобразуется в 1, подробнее см. очереди
period Number или [Number] Минимальный период между последовательными вызовами, мс О значении типа Array см. очереди

throttle подписывается на события executor'а и

  • запоминает время последнего запуска (событие start) и
  • ведёт счётчик последовательных сбоев:
    • изначально 0;
    • после каждого аварийного завершения todo (событие error) он увеличивается на единицу;
    • а после успешного (событие data) -- сбрасывается в 0.
Используется этот счётчик двояко:
  • если установлена tolerance, то при превышении этого значения throttle автоматически ставит таймер на паузу (см. выше)
  • если в качестве period установлен массив, то для расчёта задержки используется тот его элемент, который соответствует числу сбоев (при превышении -- последний).

Подробнее о вычислении задержки

Если у таймера установлен параметр period, он не имеет права запускать todo чаще, чем 1 раз в period мс. (В векторном случае period зависит от числа последовательных сбоев -- об этом сказано выше, далее будем упоминать просто значение period).

Итак, после события start должно пройти не менее period мс, чтобы таймер имел право вызвать ближайший executor.run ().

Чтобы обеспечить это условие, каждый Timer/PlannedEvent до вызова setTimeout передаёт свою плановую дату методу throttle.adjust (). Тот, если обнаруживает, что запрошенное время слишком близко, сдвигает дату до ближайшего известного ему значения (момент последнего start плюс period мс).

Точнее, Timer/PlannedEvent вызывает не прямо throttle.adjust (), а последовательно .adjust () каждого объекта, выдаваемого генератором timer.adjusters (). Вторым таким объектом-уточнителем в этой последовательности может быть описанный в следующем разделе.

Timer/TimeSlot: Ограничитель времени суток

Если при создании таймера указаны параметры

Имя Тип Обязательность Описание Примечание
from string 'HH:MM:SS' Начало допустимого времения суток указывается одновременно с to
to string 'HH:MM:SS' Окончание допустимого времения суток указывается одновременно с from

или они же установлены методом from_to, то для их хранения и применения создаётся объект Timer/TimeSlot, сохраняемый в поле time_slot.

Вся функциональность этого класса сводится к методу .adjust (), который, так же, как и у Timer/Throttle, сдвигает переданное значение типа Date до ближайшего разрешённого.

Возможны 2 варианта задания from и to:

  • from < to (например: from='03:00:00', to='04:00:00') -- тогда считается, что это время в рамках одних суток и допустимы значения между from и to;
  • from > to (например: from='23:00:00', to='02:00:00') -- это, соответственно, переход через границу суток и внутренность отрезка from..to, наоборот, недоступна.
В любом случае для недопустимого времени производится установка времени на from. Только в первом варианте (from < to) добавляются ещё одни сутки.

Timer/Promise: обёртка для запуска в виде асинхронной функции

Иногда таймер создаётся для протяжённой во времени, но гарантированно конечной последовательности шагов. Например, это может быть сложная миграция данных на старте системы. В таких случаях удобно иметь готовый объект класса Promise, чтобы дождаться окончания процесса и после него запустить что-то другое.

Timer/Promise, выдаваемый методом .promise () представляет собой именно такую обёртку над таймером. Ожидание оканчивается:

  • либо при первом сбое todo (Timer/Promise устанавливает timer.throttle.tolerance = 1)
  • либо при событии finish, после которого дальнейших запусков не запланировано.

Clone this wiki locally