Stemy.Persistence.Scheduler.Valkey 1.0.0

Stemy.Persistence.Scheduler.Valkey

Хранилище заданий Quartz 4 в Valkey (Redis) для Stemy.Persistence.Scheduler. Задания, триггеры, календари, паузы, выполняющиеся срабатывания и список узлов кластера лежат в Valkey, PostgreSQL планировщику не нужен. Несколько реплик сервиса делят одно хранилище: каждое срабатывание выполняет ровно одна из них.

Установка

<PackageReference Include="Stemy.Persistence.Scheduler.Valkey" Version="1.0.0" />

Подключение

IConnectionMultiplexer берётся из контейнера — собственного подключения пакет не создаёт.

builder.Services.AddValkey(builder.Configuration);   // Stemy.Valkey

builder.Services.AddQuartzScheduler(quartz => quartz.UseValkey(
    builder.Configuration,
    options => options.KeyPrefix = "my-service",
    scheduler => scheduler.RetryCounts = 3));

Перегрузка без IConfiguration — quartz.UseValkey(options => ...). Использование планировщика не отличается от хранилища PostgreSQL (IPersistenceScheduler: Enqueue, Schedule, SchedulePeriodic, ScheduleCron).

Конфигурация

Секция ValkeyClient:Scheduler:

Параметр По умолчанию Описание
KeyPrefix quartz Префикс всех ключей. К нему добавляется имя планировщика, поэтому сервисы с разными префиксами не пересекаются
LockTimeout 30 с Срок жизни кластерной блокировки. Запись устаревшего владельца отклоняется
LockWaitTimeout 60 с Сколько операция ждёт блокировку до ошибки
MisfireThreshold 5 с Опоздание, после которого срабатывание считается пропущенным
MaxConsecutiveFireFailures 5 Сколько срабатываний подряд могут упасть до перевода триггера в Error. 0 — без ограничения
NodeCheckInInterval 7,5 с Как часто узел отмечается в кластере
NodeFailureThreshold 30 с Через сколько без отметки узел считается упавшим. Больше NodeCheckInInterval

Реплики одного сервиса должны использовать один и тот же KeyPrefix и имя планировщика, разные сервисы — разные.

Как это работает

  • Блокировка. Составные операции выполняются под блокировкой в Valkey (SET NX PX, токен-владелец, освобождение Lua-скриптом). Изменения копятся в памяти и уходят одной транзакцией MULTI с условием «блокировка всё ещё моя»: если блокировка истекла и её забрал другой узел, запись отклоняется, а не портит состояние.
  • Расписание. Ожидающие триггеры лежат в ZSET с оценкой «время срабатывания, затем приоритет». Захват пачки читает индекс страницами, а не целиком; удаление триггера не загружает всю его группу.
  • Кластер. Каждый узел отмечается каждые NodeCheckInInterval. Узел, не отмечавшийся дольше NodeFailureThreshold, считается упавшим: его захваченные триггеры возвращаются в ожидание, а выполнявшиеся задания с RequestRecovery получают триггер восстановления.
  • Закрепление. Триггер с PreferredNode достаётся только своему узлу, пока тот жив.
  • Ключи. Все ключи планировщика содержат hash-tag {префикс:имя планировщика}, поэтому Lua и транзакции работают и в Redis Cluster.

Проверка

Реализация проверена набором контрактных тестов самого Quartz (JobStoreContractTest, 113 сценариев), который запускается и против RAMJobStore (эталон), и против этого хранилища, плюс сценарии кластера: упавший узел, закрепление, ограждение блокировки, три узла под нагрузкой. Тесты — Stemy.Persistence.Scheduler.Valkey.Tests (нужен Docker).

Пропускная способность на одном узле (Valkey в Docker, локально, 2026-10-01): 2000 триггеров планируются за 0,2 с, захват + срабатывание + завершение — около 350 триггеров/с (PostgreSQL ADO-хранилище в том же сценарии — около 94/с); время не растёт с числом ожидающих триггеров. Скорость даёт опережающее чтение: операция запрашивает все нужные ключи одним конвейером (Internal/StoreSession.Prefetch.cs), а не десятками обращений подряд. Сравнение с Stemy.Queue (постановка и выполнение) — проект Stemy.BackgroundJobs.Benchmarks.

Ограничения

  • Состояние планировщика — только в Valkey: потеря данных Valkey (без персистентности) означает потерю расписаний. Для периодических задач, регистрируемых при старте сервиса, это самовосстанавливается; для разовых — нет. Включайте AOF/RDB.
  • Хранилище опирается на внутренние члены Quartz 4.3 (TriggerDetailsUpdate, JobDataMap.Clone, MisfireInstruction), доступные через рефлексию; при обновлении Quartz на minor/major нужно прогнать контрактные тесты.
  • Все операции идут через одну блокировку на планировщик: масштабирование — числом реплик планировщика (разные KeyPrefix), а не ростом нагрузки на одно хранилище.

Лицензии

Логика хранилища — порт RAMJobStore из Quartz.NET (© Marko Lahma, Apache License 2.0), контрактные тесты взяты оттуда же. См. NOTICE.md.

No packages depend on Stemy.Persistence.Scheduler.Valkey.

Version Downloads Last updated
1.0.0 0 10/11/2026