Skip to content
 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

os-scheduler

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

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

Планировщик = Новый Планировщик;                                          // реализации по умолчанию
Планировщик.Зарегистрировать("gc-кэша", Расписания.ЕжедневноВ(3, 30), Джоб); // что и когда
Планировщик.Запустить();
Планировщик.Тик();                                                        // один шаг: цикл — за приложением

Библиотека самодостаточна: не зависит ни от ОСени, ни от какого-либо другого фреймворка и подходит любому OneScript-приложению (winow, CGI, свой демон).

Установка

opm install os-scheduler

Быстрый старт

#Использовать os-scheduler

// 1. Обработчик — любой объект с методом Исполнить().
//    (в реальном приложении это, например, желудь-сервис)
Обработчик = Новый СбросСтатистики; // Процедура Исполнить() Экспорт ... КонецПроцедуры

// 2. Планировщик с реализациями по умолчанию:
//    хранилище в памяти + синхронный исполнитель + системные часы.
Планировщик = Новый Планировщик;

// 3. Регистрируем задачи с расписаниями.
Планировщик.Зарегистрировать("сброс-статистики", Расписания.Интервал(60), Обработчик);
Планировщик.Зарегистрировать("gc-кэша", Расписания.ЕжедневноВ(3, 30), Новый ОчисткаКэша);

// 4. Запускаем и периодически вызываем Тик() из фонового цикла приложения.
Планировщик.Запустить();
Пока Истина Цикл
    Планировщик.Тик();
    Приостановить(1000); // 1 с; реальный сон организует приложение, не библиотека
КонецЦикла;

Планировщик намеренно не создаёт потоков и не спит внутри себя — он лишь предоставляет шаг Тик(). Реальный цикл (как часто вызывать Тик(), в каком потоке) организует приложение. В приложениях на ОСени это желудь-рогатка с фоновым заданием. Такая опросная модель делает поведение полностью детерминированным и тестируемым на инъектируемых часах.

Расписания

Расписания создаются фабрикой из модуля Расписания:

Вызов Смысл Пример задачи
Расписания.Интервал(Секунд) каждые N секунд; первый запуск — через N секунд после регистрации сброс статистики раз в минуту Интервал(60), ревалидация раз в час Интервал(3600)
Расписания.Однократно(Задержка) один раз через Задержка секунд после регистрации (по умолчанию 0 — немедленно) прогрев кэша при старте Однократно(), отложенная миграция Однократно(30)
Расписания.ЕжедневноВ(Час, Минута, Секунда) раз в сутки в указанное время суток (по часам планировщика) ночной GC ЕжедневноВ(3, 30), суточный отчёт ЕжедневноВ(0)

Интервал — это fixed-delay, а не fixed-rate. Следующий запуск отсчитывается от момента фактического старта задачи и от текущего тика, а не от идеальной «сетки» времени. Если тики приложения идут реже интервала или задача выполняется долго, пропущенные срабатывания не «накапливаются» и не выстреливают залпом — просто следующий запуск сдвигается вперёд. Для привязки к конкретному времени суток используйте ЕжедневноВ.

Поддержка cron

Полноценные cron-выражения намеренно не реализованы. Интервал покрывает «раз в N секунд/минут/часов/суток», а ЕжедневноВ — единственный практически важный случай, который интервал не выражает: запуск в конкретное время суток. Полный парсер crontab (* * * * *, списки, диапазоны, шаги) с корректной обработкой часовых поясов и переходов на летнее/зимнее время несоразмерен объёму библиотеки и редко нужен фоновым задачам сервера. Контракт расписания открыт: собственное расписание — это любой объект с методом СледующийМомент(Опора, БылЗапуск) → Дата | Неопределено; его можно передать в Зарегистрировать вместо штатного, не меняя планировщик.

Надёжность

  • Изоляция ошибок. Обработчик, бросивший исключение, не роняет планировщик: исключение перехватывает исполнитель, ошибка пишется в состояние задачи (статус = "ошибка", ошибка), остальные задачи и сам планировщик продолжают работу. Сбой самого исполнителя тоже изолирован.
  • Перекрытие (overlap). Если очередной запуск наступил, а предыдущий ещё идёт (актуально для асинхронного исполнителя):
    • "пропустить" (по умолчанию) — срабатывание пропускается, момент переносится на следующий слот расписания;
    • "очередь" — ставится один отложенный запуск, он стартует сразу по завершении текущего выполнения (очередь глубины 1, без неограниченного роста).
  • Повтор при ошибке. МаксПовторов и ЗадержкаПовтораСекунд в настройках задачи: после ошибки задача повторяется до МаксПовторов раз с указанной паузой; при исчерпании — возврат к обычному расписанию. Бюджет повторов восстанавливается в начале каждого планового цикла, то есть каждый плановый запуск получает свои МаксПовторов попыток (важно для регулярно падающих джобов), но «шторм» повторов внутри одного цикла невозможен. Сбой самого исполнителя изолируется и идёт через ту же логику повторов (без учёта в запусковВсего — обработчик не стартовал). Исполнитель, нарушивший контракт (вернувший не дескриптор), не подвешивает задачу — фиксируется ошибка контракта.
  • Грациозная остановка. Остановить() (без отмены) прекращает запуск новых задач, но Тик() продолжает финализировать уже идущие — приложение вызывает Тик(), пока Активных() не станет 0 (дождаться текущих). Библиотека не может прервать чужой поток исполнителя, поэтому «жёсткая» Остановить(Истина) лишь помечает идущие задачи как "остановлена" и отпускает их дескрипторы; фактическое прерывание работы — на стороне исполнителя.

API

Планировщик

Новый Планировщик(Настройки = Неопределено)

НастройкиСтруктура или Соответствие с необязательными полями Хранилище, Исполнитель, Часы. Незаданное берётся по умолчанию: хранилище в памяти, синхронный исполнитель, системные часы. Все три — утиные контракты (см. ниже), никакого класса библиотеки реализация наследовать не обязана.

  • Зарегистрировать(Имя, Расписание, Обработчик, Настройки = Неопределено) — регистрирует задачу; восстанавливает её состояние из хранилища (переживает рестарт) и вычисляет момент первого запуска. Дубликат имени — исключение.
    • Имя — уникальное имя (ключ в планировщике и хранилище);
    • Расписание — результат Расписания.Интервал/Однократно/ЕжедневноВ либо свой объект с методом СледующийМомент;
    • Обработчик — любой объект с методом Исполнить();
    • НастройкиСтруктура/Соответствие: ПолитикаПерекрытия ("пропустить" по умолчанию | "очередь"), МаксПовторов (0), ЗадержкаПовтораСекунд (0).
  • Состояние(Имя)Структура | Неопределено — снимок состояния задачи;
  • Состояния()Соответствие — имя → снимок, по всем задачам;
  • Запустить() — разрешает диспетчеризацию;
  • Тик()Число — один шаг: финализирует завершившиеся выполнения и запускает задачи, у которых наступил момент; возвращает число запущенных в этом тике;
  • Остановить(Отменять = Ложь)Число — грациозная (по умолчанию) либо отменяющая остановка; возвращает число ещё активных выполнений;
  • Запущен()Булево, Активных()Число.

Снимок состояния задачи

Наружу выдаётся не объект задачи, а плоская Структура-копия — её можно логировать, класть в ответ health-check и менять, не задев планировщик:

Поле Тип Смысл
имя Строка имя задачи
статус Строка "ожидает", "выполняется", "успех", "ошибка", "остановлена"
последнийЗапуск Дата, Неопределено момент последнего старта
следующийЗапуск Дата, Неопределено запланированный момент (Неопределено — запусков больше не будет)
ошибка Строка, Неопределено текст ошибки последнего запуска
выполняется Булево идёт ли выполнение сейчас
оставшихсяПовторов Число остаток бюджета повторов текущего цикла
запусковВсего, успехов, ошибок Число счётчики
Для Каждого Запись Из Планировщик.Состояния() Цикл
    Снимок = Запись.Значение;
    Сообщить(СтрШаблон("%1: %2, запусков %3, ошибок %4",
        Снимок.имя, Снимок.статус, Снимок.запусковВсего, Снимок.ошибок));
КонецЦикла;

Модуль Расписания (фабрика расписаний)

  • Расписания.Интервал(ИнтервалСекунд) → расписание
  • Расписания.Однократно(ЗадержкаСекунд = 0) → расписание
  • Расписания.ЕжедневноВ(Час, Минута = 0, Секунда = 0) → расписание

Контракт исполнителя

Исполнитель развязывает библиотеку с конкретным способом выполнения (синхронно, через фоновые задания, через autumn-async и т.п.):

  • Запустить(Обработчик)дескриптор выполнения — запускает обработчик и возвращает дескриптор. Исполнитель обязан изолировать исключение обработчика: падение обработчика фиксируется в дескрипторе, а не летит в планировщик.

Дескриптор — любой объект с методами Завершено()Булево, ЗавершеноУспешно()Булево, ТекстОшибки()Строка | Неопределено. Тип не проверяется: планировщик опрашивает дескриптор в каждом тике по этим методам. Исполнитель, вернувший не дескриптор, задачу не подвешивает — задача получает ошибку контракта и обычный слот расписания.

По умолчанию используется синхронный исполнитель: выполняет обработчик немедленно в тике и возвращает уже завершённый дескриптор (без зависимости от ОСени). Асинхронный исполнитель приложения возвращает незавершённый дескриптор и завершает его из фонового потока — планировщик подхватит это в одном из следующих тиков (опросная модель).

Контракт хранилища состояния

Хранилище персистит только состояние задач (последний/следующий запуск, статус, ошибка, счётчики) — обработчик и расписание задаются в коде при регистрации:

  • СохранитьСостояние(Имя, Состояние) — сохранить снимок (Структура примитивов);
  • ПрочитатьСостояние(Имя)Структура | Неопределено (независимая копия);
  • Удалить(Имя);
  • ВсеСостояния()Соответствие (имя → копия снимка).

По умолчанию — хранилище в памяти (теряется при завершении процесса). Реализация поверх БД (состояние переживает рестарт) пишется приложением по этому же контракту; рекомендуется индекс по «следующийЗапуск». Повреждённый или неполный снимок регистрацию не роняет: задача стартует с чистого состояния, факт логируется.

Контракт часов

Планировщик берёт время только из объекта-часов с единственным методом Сейчас()Дата. По умолчанию — системные часы (ТекущаяУниверсальнаяДата()).

Тестируемость

Управляемые часы — это шесть строк в тестах приложения; никакого Sleep и никакой зависимости от системного времени, поэтому проверки расписания, перекрытия и повторов детерминированы и не флейкают:

// ЧасыРучные.os — тестовый двойник в вашем проекте
Перем Момент_;
Процедура ПриСозданииОбъекта()
    Момент_ = Дата(2026, 1, 1, 12, 0, 0);
КонецПроцедуры
Функция Сейчас() Экспорт
    Возврат Момент_;
КонецФункции
Процедура Прибавить(Знач Секунд) Экспорт
    Момент_ = Момент_ + Секунд;
КонецПроцедуры
Часы = ЗагрузитьСценарий("ЧасыРучные.os");
Планировщик = Новый Планировщик(Новый Структура("Часы", Часы));
Планировщик.Зарегистрировать("job", Расписания.Интервал(60), Обработчик);
Планировщик.Запустить();

Часы.Прибавить(60);      // «прошла минута» — мгновенно
Планировщик.Тик();       // задача запущена ровно один раз
Ожидаем.Что(Планировщик.Состояние("job").успехов).Равно(1);

Готовые двойники (управляемые часы, ручной и падающий исполнители, обработчики) лежат в tests/fixtures/ — их можно взять за образец.

Разработка и тесты

Тесты — OneUnit + asserts:

oneunit execute

Лицензия

MIT

About

Планировщик фоновых/периодических задач для OneScript: расписания (интервал, однократно, ежедневно), инъектируемые часы, абстрактные исполнитель и хранилище состояния, политики перекрытия, повтор при ошибке, изоляция сбоев, грациозная остановка

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages