-
Notifications
You must be signed in to change notification settings - Fork 17
Таймеры
Таймер в Dia.js -- объект, используемый для запуска заданной функции
- в отрыве от процесса-инициатора;
- с защитой от параллельного исполнения;
- не ранее заданного времени
- хотя, возможно, существенно позже - с учётом однозадачности и прочих дополнительных требований.
При помощи таймеров реализованы очереди и в норме в коде приложения непосредственно должны использоваться именно они.
Настоящая статья по большей части написана для разъяснения устройства этого внутреннего механизма.
|
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 должна быть ссылкой на асинхронную функцию, не ожидающую параметров -- то, что таймер должен запускать.
При создании таймера вручную в рамках 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-функция создаётся автоматически: помимо запуска подзапроса с заданными именами типа и действия, они включают обращения к БД.
Вообще таймер, описываемый в настоящей статье, предназначен только для упорядочения во времени асинхронно поступающих конкурирующих внешних запросов. Сам по себе он источником запросов не является.
Однако для реализации периодических процедур (по аналогии с 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 имеют тип string, не Object. Это строки в формате JSON. Сериализация используется, в частности, для обеспечения уникальности значений: если при двух подряд событиях отображаемые поля не меняются, второй раз change не вызывается.
После применения JSON.parse получается структура со следующими полями (любое может отсутствовать):
| Имя | Тип | Описание |
|---|---|---|
ts_scheduled
|
Строка Date.toJSON () | Момент запланированного запуска |
is_busy
|
true или отсутствует
|
Исполняется ли todo в настоящее время |
is_paused
|
true или отсутствует
|
стоит ли очередь на паузе |
ts_paused
|
Строка Date.toJSON () | если очередь на паузе, то с какого момента |
error
|
Строка | если очередь на паузе из-за ошибки, то текст ошибки |
Объект класса Timer в основном занимается созданием вложенных объектов, описанных в подразделах ниже. Каждый из таких объектов имеет поле timer: ссылку на родительский объект, через которую он получает доступ ко всем остальным деталям данного механизма.
При создании таймера параметр
| Имя | Тип | Обязательность | Описание | Примечание |
|---|---|---|---|---|
| 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 ().
Жизненный цикл запуска функции 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.
Если перед попыткой перехода в ST_SCHEDULED событие обнаруживает, что родительское поле scheduled_event не пусто, оно извлекает оттуда плановую дату и:
- если имеющееся событие запланировано на болеее ранний или тот же момент, что требуется сейчас -- сразу переходит в
ST_FINISHED(без попытки запускаtodo) - иначе оно инициирует отмену имеющегося события (по ходму чего вызывается clearTimeout) и, вызвав свой setTimeout, ставит себя на его место.
Пара методов
| Имя, параметры | Описание | Примечание |
|---|---|---|
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 в режиме паузы не вызывается.
Для реализации ограничений на частоту последовательных запусков предусмотрен сохраняемый в поле 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 (). Вторым таким объектом-уточнителем в этой последовательности может быть описанный в следующем разделе.
Если при создании таймера указаны параметры
| Имя | Тип | Обязательность | Описание | Примечание |
|---|---|---|---|---|
| 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) добавляются ещё одни сутки.
Иногда таймер создаётся для протяжённой во времени, но гарантированно конечной последовательности шагов. Например, это может быть сложная миграция данных на старте системы. В таких случаях удобно иметь готовый объект класса Promise, чтобы дождаться окончания процесса и после него запустить что-то другое.
Timer/Promise, выдаваемый методом .promise () представляет собой именно такую обёртку над таймером. Ожидание оканчивается:
- либо при первом сбое
todo(Timer/Promiseустанавливаетtimer.throttle.tolerance = 1) - либо при событии
finish, после которого дальнейших запусков не запланировано.