Диспетчер задач на и для Javascript +9


image

Идея несколько необычна, но вполне интересна — диспетчер задач для вашего кода в браузере. Даже с учётом современной ситуации js-пей полных аналогов не найдено. Отдалённо похожий диспетчер был обнаружен только в ExtJS.

Это статья о небольшой библиотеке, которая позволяет управлять отложенным выполнением кода несколько более удобно нежели setTimeout и setInterval. Задания могут останавливаться, запускаться, выполняться немедленно. Есть графический интерфейс и логи.

Однозначно не несёт никакой пользы опытному разработчику но будет интересна новичку, например для отладки или управления обновлением данных через http-запросы.

И да, это не про Node.js, библиотека написана для старого доброго браузерного Javascript.

Базовая функциональность


Библиотеку и пример использования можно скачать здесь. Для использования её необходимо подключить к проекту привычным вам способом, например через тэг

    <script type="text/javascript" src="chronoshift.js"></script>

Базовая сущность, с которой мы работаем — задача, task. В ней хранится исполняемый код, параметры выполнения и информация для чтения человеком. Подробный пример:

task:{
  // код, который будет выполнять эта задача
  handler: ()=>{conslole.log("Hello world!")}
  // задержка перед выполнением
  delay: 5000,
  // повторять или нет, по сути выбор между setTimeout и setInterval
  repeat: false,
  // имя задачи, должно соответствовать правилам именования переменных
  name: "task1",
  // описание задачи, используется только в логах и интерфейсе
  description: "This task created at the name of Invisible Pink Unicorn!"
  // время выполнения, для цикличных задач время первого запуска
  at: "2017-09-20",
  // идентификатор процесса
 pid: 42
}

Для хранения и управления ими создадим экземпляр библиотеки:

var verboseLogs = true,
  verboseTime = true,
  writeLogs = true;
var cs = new Chronoshift(verboseLogs, verboseTime, writeLogs) ;

Имена переменных достаточно понятны, однако разъясним аргументы конструктора подробнее:
verboseLogs — требуется ли выводить логи работы в консоль
verboseTime — требуется ли выводить время события, может использоваться в вызове без вывода лога
writeLogs — требуется ли хранить логи.

Хранение логов может стать достаточно дорогой операцией, примерно 0.1 КБ на событие. В современном мире эта цифра может показаться смешной, но… Тема хорошо раскрыта в «Ученике Чародея» Диснеем ещё в прошлом веке.

Для создания задач используется метод runTask. Очевидно, что вызывать его по полному описанию было бы довольно непродуктивно. Необходимый минимум — функция и задержка:

cs.runTask( () => {console.log("task 1 executed")}, 3000);

Этот код создал задачу, которая выведет в консоль сообщение с задержкой в три секунды. Однако в консоли мы увидим чуть больше ожидаемого:

cs.runTask( () => {console.log("task 1 executed")}, 3000)
[20.09.2017, 12:33:58.326]
Creating task: {"delay":3000}
[20.09.2017, 12:33:58.327]
Added a task task_7a0dh9bllhg with timeout 3000
2
task 1 executed
[20.09.2017, 12:34:01.327]
Task executed: task_7a0dh9bllhg

Как видно из логов, задача была создана, поскольку имя ей не было задано, имя было сгенерировано случайным образом.

В пояснении нуждается цифра 2 — это идентификатор процесса. Его выкидывает в консоль нативная функция setTimeout/setInterval. Подробнее можно почитать здесь.
Далее задача была выполнена и лог этого также был выведен.

Далеко не всегда нужны столь подробные логи. Способ логирования можно поменять в любой момент, не только в конструкторе, нужно вызвать метод setLogging, который принимает на вход те же аргументы:

cs.setLogging(false, false, true);

Теперь лишняя информация не будет выводиться в консоль, но доступ к ней останется через cs.logs. Если вам потребуется посмотреть логи в читаемом виде, вызовите метод showLogs:



Немного сахара: библиотека умеет создавать задачи по дате/времени. Стоит признать, что этот метод пока сильно не додуман, однако весьма интересен. В приведённых ниже примерах происходит автодополнение даты.

В первом вызове дата по умолчанию — сегодня, но если времени уже больше 23:55, то вызов будет назначен на завтра. Во втором примере по умолчанию задача назначится на этот год, но если седьмое июля уже прошло то вызов будет назначен на следующий год.

//выполним код за пять минут до полуночи
cs.runTaskAt(()=>{console.log("Уж полночь близится, а Германа всё нет!");}, "22:55", "German");
//или утром седьмого июля
cs.runTaskAt(()=>{console.log("Пора готовить топоры!");}, "07-07 07:07:07", "p777", "Время трёх топоров");

Ближе к концу статьи приведено описание подводных камней этого метода.

Управление задачами


Несмотря на то, что для создания задачи обязательны только два параметра работать с ней намного удобнее при использовании несколько более полного описания при создании. Давайте попробуем сделать так:

cs.runTask( () => {console.log("task 1 executed")}, 3000, false, "task1");

Пока что, помимо логирования, отличий от обычного исполнения с задержкой не было. Однако у библиотеки есть пара фокусов. Во первых, мы можем выполнить код нашей задачи в любой момент, не меняя при этом расписания. Для этого надо вызвать метод executeTask:

cs.executeTask("task1");

Сработает этот метод вне зависимости от того была ли наша задача выполнена или ещё только ожидает выполнения. Собственно, и в момент выполнения, если вы сумеете удачно синхронизировать вызовы, она также будет выполнена.

Ещё два метода сильно облегчают отладку, это stopTask и restartTask. Названия более чем говорящие, приведём код их вызова для лучшего запоминания:

cs.stopTask("task1");
cs.restartTask("task1");

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

Заметьте, что перезапуск выполняется именно как перезапуск, не как продолжение прерванного интервала. Например:

// создадим задачу, которая выполнится через три секунды
cs.runTask( () => {console.log("task 1 executed")}, 3000, false, "task1");
...
//какой-то код выполнялся две секунды, задача должна выполниться через секунду но мы её остановим
cs.stopTask("task1");
...
//какой-то код выполнялся ещё две секунды, то есть всего прошло 4 секунды и мы перезапускаем задачу
cs.restartTask("task1");
//задача выполнится ещё через три секунды, итого семь секунд спустя

И последний метод, который хотелось бы упомянуть это removeTask, который, как видно из названия, полностью удаляет задачу. От остановки он отличается именно удалением из памяти, не только из списка. Удалённый метод будет полностью недоступен, однако также будет освобождена вся память, которая могла на нём замусориться через замыкания. Выглядит вызов так:

//в качестве аргумента для любого метода изменяющего задачи могут использоваться как имя так и  id процесса
 cs.removeTask(2);

Графический интерфейс


Говорить тут особо не о чем. Вот скриншот из примера, который лежит в репозитории:



Для запуска необходимо трижды нажать клавишу Ctrl, закрывается интерфейс так же либо нажатием Esc.

Расти, прямо скажем, есть куда. Выполняются все перечисленные функции, в планах добавить управление режимом логирования и вызов showLogs и showTasks, просто для завершённости.

Подводные камни


Для проекта в 500 строк кода их ожидаемо немного, но достойны упоминания.

Камень первый, вызов setTimeout в методе runTaskAt. Казалось бы, какой может быть подвох? Собственно тут и подвоха-то особого нет, просто сложно предположить, что такая задержка может кому-то потребоваться. Итак, по порядку.

Метод runTaskAt получает на вход дату в текстовом формате и, при необходимости, дополняет её нужными значениями. Например, в приведённом выше коде такое значение «07-07 07:07:07». Поскольку статья написана в сентябре, дата дополнилась до седьмого июля следующего года. Затем дата была преобразована в метку времени, была получена разница в милисекундах между введённой датой и текущим моментом и вызвана функция setTimeout с этой задержкой.

Собственно здесь и начинается что-то странное: дата задания устанавливается верно, однако выполняется оно немедленно. Притом, переменная где хранится это значение показывает правильное число миллисекунд. Очень обескураживающее поведение.

Происходит это из за того, что максимально допустимая задержка для setTimeout составляет 2147483647, поскольку хранится в 32-битной целочисленной переменной, до которой и обрезается любое переданное значение. Если значение больше этого максимума то функция выполнится немедленно.

Если перевести в человекочитаемые единицы, то максимальная задержка составит чуть меньше чем 24 дня 20 часов 31 минуту и 23,65 секунды. В библиотеке поставлена проверка на переполнение.

Второй подводный камень — замыкания. Оставим его для самых пытливых читателей, поскольку это довольно распространённая задача. Борьба с замыканиями двумя разными, но схожими способами происходит в коде самой библиотеки и в коде примера.

Небольшое послесловие


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

Каких-то особых планов по развитию проекта нет. Делался он в основном для собственного удовольствия и вряд ли тянет на жизненно необходимый инструмент. Тем не менее, область применения ему явно найдётся и некоторые улучшения обязательно будут добавлены в ближайшее время. Замечания и пожелания, если таковые появятся, будут реализованы максимально быстро, по крайней мере насколько это позволит наличие свободного времени.




К сожалению, не доступен сервер mySQL