Quartz.NET 4.4: задания сообщают о своём результате, а проверка работоспособности замечает, когда они перестают выполняться
Quartz.NET 4.4.0 позволяет заданию записать в context.Result объект JobRunReport (Succeeded, Failed, Cancelled или Skipped, с кратким описанием и метриками), хранит результат по каждому заданию в истории выполнения и добавляет проверки работоспособности RequireSuccessWithin, которые переходят в состояние Degraded, когда ночное задание перестаёт завершаться успешно.
Quartz.NET v4.4.0 вышел 2026-10-07. Главные цифры относятся к кластеризации (триггеры, срок которых уже наступил, срабатывают в той же транзакции, которая их захватывает, а на PostgreSQL при 20 триггерах в секунду от 95 до 99 % срабатываний укладываются в 50 мс, тогда как в 4.3.0 было 54 %), но изменение, которое чаще всего пригодится в прикладном коде, скромнее: задание наконец может сказать, чего достиг его запуск, а ваша конечная точка проверки работоспособности может на это реагировать.
”Не выбросило исключение” не значит “сработало”
До версии 4.3 Quartz знал два исхода: задание выбросило исключение или не выбросило. Задание очистки, которому нечего было очищать, синхронизация, которая завершилась досрочно из-за пустой страницы от внешнего API, и запуск, выполнивший реальную работу, выглядели в истории одинаково. А ночное задание, у которого триггер три дня стоял на паузе или пропускал срабатывания (misfire), не было заметно вовсе, потому что ничего не падало.
Передача результата через JobRunReport
В 4.4 задание записывает в context.Result объект JobRunReport. Доступны четыре результата (Succeeded, Failed, Cancelled, Skipped), необязательное краткое описание до 1000 символов и метрики, которые хранятся как AOT-совместимый JSON размером не более 4000 символов:
public sealed class ReleaseStaleReservationsJob : IJob
{
public async ValueTask Execute(IJobExecutionContext context, CancellationToken cancellationToken = default)
{
int scanned = await CountReservations(cancellationToken);
int released = await ReleaseStale(cancellationToken);
context.Result = released == 0
? JobRunReport.Skipped("no stale reservations").With("scanned", scanned)
: JobRunReport.Succeeded($"released {released}")
.With("scanned", scanned)
.With("released", released);
}
}
Записываемый результат определяется в фиксированном порядке: отменённое срабатывание получает Cancelled, задание, выбросившее исключение, получает Failed, иначе берётся то, что вы положили в context.Result, а если там пусто, то Succeeded. Вернуть JobRunReport.Failed(...) не то же самое, что выбросить исключение. Только выброшенное исключение запускает политику повторных попыток и продолжения OnFailure, поэтому сообщайте о сбое через отчёт тогда, когда повтор не поможет.
История хранит JobRunStatus для каждого задания (LastSucceededAtUtc, ConsecutiveFailures, LastFailureMessage, счётчики запусков и сбоев), срок хранения можно задавать отдельно для каждого результата, а у метрики quartz.job.execution.duration появился тег quartz.job.result, так что на панелях OpenTelemetry можно отделять пропущенные запуски от настоящих.
Проверка работоспособности для каждого задания
Именно эту часть я бы включил уже сегодня. RequireSuccessWithin помечает задание как нездоровое, если оно не завершалось успешно в течение заданного окна. Это ловит тихие случаи: триггер на паузе, пропущенное срабатывание, задание, которое раз за разом пропускается из-за вето.
builder.Services.AddQuartzExecutionHistory();
builder.Services.AddHealthChecks().AddQuartz(options =>
{
// Degraded once the nightly report has not succeeded for 26 hours.
options.RequireSuccessWithin(new JobKey("nightly-report", "reports"), TimeSpan.FromHours(26));
// Unhealthy, so the node leaves rotation, after 90 minutes without a ledger close.
options.RequireSuccessWithin(
new JobKey("ledger-close", "billing"),
TimeSpan.FromMinutes(90),
HealthStatus.Unhealthy);
});
Статус по умолчанию равен Degraded, а запуск со статусом Skipped считается успешным, поэтому ветка “делать нечего” из задания выше никого не разбудит ночью. Список также можно привязать к конфигурации через RequiredJobs. Если история выполнения выключена, хост завершится с ошибкой при запуске, а не будет сообщать о проверке, которая никогда не сможет пройти.
Обновление с версии 4.3
Выпуск аддитивный: код для 4.3 компилируется без изменений, а у каждого нового члена интерфейса есть реализация по умолчанию. Если вы используете постоянное хранилище заданий с включённой историей выполнения, запустите скрипт 4.4/add_execution_outcome_<dialect>.sql до старта первого узла с версией 4.4. Кроме того, в выпуск вошли атрибут [RetryPolicy] для классов заданий, метод q.RunAtStartup(jobKey) и управление схемой через Weasel для MySQL, Oracle и Firebird; всё это описано в руководстве по миграции и в практическом руководстве по результатам заданий.
Если вы всё ещё на 4.2, то анализатор cron и генератор исходного кода [QuartzJob] из версии 4.2 достанутся вам в придачу.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.