Диагностика и решение проблем планировщика
Quartz
В некоторых случаях могут возникать проблемы с запуском бизнес-процессов: письма и рассылки не отправляются или отправляются с задержкой. В логах приложения могут появляться ошибки Quartz.
Если проблемы наблюдаются, необходимо выполнить диагностику планировщика. Диагностика включает в себя следующие этапы:
- Анализ логов приложения;
- Проверка конфигурационного файла и выполнение запросов по проблемам Quartz;
- Выполнение запроса по исправлению ошибок;
- Дополнительная диагностика нагрузки на серверы.
Анализ логов приложения
Необходимо проанализировать логи приложения на предмет ошибок, связанных с Quartz.
Ошибка генерации instance Id
Бизнес-процессы по таймерам не запускаются и не отрабатывает синхронизация. В логе Scheduler.log следующая ошибка:
Quartz.Impl.StdSchedulerFactory ExecutionContextCallback - Couldn't generate instance Id! Quartz.SchedulerException: Couldn't get host name! ---> System.Net.Internals.SocketExceptionFactory+ExtendedSocketException (00000005, 0xFFFDFFFF): Name or service not known
Ошибка возникла из-за того, что система не смогла создать уникальное имя ноды планировщика.
Проверьте настройку quartz.scheduler.instanceId. Если она установлена на AUTO, убедитесь, что указано имя хоста. Для этого откройте папку /etc/hosts или /etc/sysconfig/network в Linux и выполните команду hostname. Если имя хоста отсутствует, присвойте ему значение.
Другой вариант: вручную добавьте уникальные значения quartz.scheduler.instanceId в файл BPMSoft.WebHost.dll.config.
Зависание бизнес-процессов
Бизнес-процессы запускаются, но иногда зависают и не доходят до конца. В логе Application.log можно увидеть:
... 2025-02-06 13:38:45,152 [1] WARN IIS APPPOOL\ocrm BPMSoft.WebApp.Global Application_Start - Application start 2025-02-06 15:32:34,430 [1] WARN IIS APPPOOL\ocrm BPMSoft.WebApp.Global Application_Start - Application start 2025-02-06 17:26:54,758 [1] WARN IIS APPPOOL\ocrm BPMSoft.WebApp.Global Application_Start - Application start ...
Видно, что приложение часто перезапускается в течение дня. В журнале процессов можно найти зависший бизнес-процесс от 15:28, а в логе — перезапуск сайта в 15:32. Процессы, которые выполнялись на момент остановки или перезапуска приложения, не возобновляются. Это может указывать на проблему в коде бизнес-процесса.
Нужно проверить остальные логи на возможные ошибки, которые могли привести к перезагрузкам сайта. Если их не окажется (что вполне возможно), следует изучить Event Log в Windows или journalctl в Linux. Особое внимание нужно уделить критическим ошибкам. В большинстве случаев проблема связана с ошибкой в кастомизации, которая вызывает критические ошибки, такие как StackOverflow.
Quartz.SchedulerException: Unable to unschedule trigger [DEFAULT.ce86f88b-e2e0-489b-80e2-729fd3bcad31] while deleting job [ProcessMaintenanceGroup.BPMSoft.Configuration.ProcessMaintenanceJob] at Quartz.Core.QuartzScheduler.d__86.MoveNext() Quartz.JobPersistenceException: Couldn't remove trigger: Transaction (Process ID 78) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction. ---> System.Data.SqlClient.SqlException: Transaction (Process ID 78) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction. Error executing [BPMSoft.Core.Scheduler.RunAppJob] in context [JobExecutionContext: trigger: 'Mailing.BPMSoft.Configuration.TriggerEmailFailoverHandlerTrigger' job: 'Mailing.BPMSoft.Configuration.TriggerEmailFailoverHandler' fireTimeUtc: 'Wed, 31 Aug 2022 10:35:27 GMT' scheduledFireTimeUtc: 'Wed, 31 Aug 2022 10:35:26 GMT' previousFireTimeUtc: 'Wed, 31 Aug 2022 08:32:27 GMT' nextFireTimeUtc: 'Wed, 31 Aug 2022 10:40:26 GMT' recovering: False refireCount: 0]
Если ошибки не очевидны, то переходите к этапу Проверка конфигурационного файла и выполнение запросов по проблемам Quartz.
Проверка конфигурационного файла и выполнение запросов по проблемам Quartz
Для выявления проблем Quartz выполните в базе данных следующие действия:
- Запустите скрипты, которые представлены ниже. Если проблема проявляется явно, запустите их в момент ее возникновения (например, при запуске отложенной рассылки, если прошло 15 минут с запланированного времени, а рассылка не началась, или при запуске бизнес-процесса с промежуточным таймером, если время истекло, а процесс не выполнился). В противном случае запустите скрипты в рабочее время, когда нагрузка на стенд максимальна:
- Если известно, что Quartz не работает или задача не выполнена давно, а также для мониторинга производительности, раскомментируйте условие и отобразите только просроченные триггеры. Это сократит размер выгрузки;
- Если проблема связана с бизнес-процессами или их элементами (стартовый таймер, промежуточный таймер, фоновый режим), используйте скрипты для вывода подробной информации. Они выводят название схемы или элемента для каждого триггера (в названии таких триггеров указан только GUID без осмысленного имени).
- Если есть подозрения, что планировщик перегружен, например, из-за большого количества просроченных триггеров (overdue_min > 0) или постоянно работающих триггеров (особенно если их 5 или больше), проверьте параметры, установленные в конфигурационном файле.
<add key="quartz.scheduler.instanceName" value="BPMCRMQuartzScheduler" /> <add key="quartz.threadPool.threadCount" value="5" /> <add key="quartz.jobStore.misfireThreshold" value="300000" />
- Выполните анализ собранной информации:
- Если в результатах запроса на получение запланированных триггеров Quartz есть записи с TRIGGER_STATE=ERROR и значением overdue_min больше 60 минут, переходите к этапу Выполнение запроса по исправлению ошибок;
- Если в запросе на получение запущенных триггеров Quartz среднее количество записей превышает 80% от параметра threadCount в web.config, переходите к этапу Дополнительная диагностика нагрузки на серверы;
- Если в запросе на получение запланированных триггеров Quartz нет записей с TRIGGER_STATE=ERROR и overdue_min > 0, а в запросе на получение запущенных триггеров Quartz среднее количество записей не превышает 80% от параметра threadPool.threadCount в конфигурационном файле, то обратитесь за консультацией в Техническую поддержку .
Скрипты для получения триггеров
SELECT DATEDIFF_BIG(MINUTE, dbo.fn_QuartzTimeToDateTime(t.NEXT_FIRE_TIME), GETUTCDATE()) AS overdue_min, GETUTCDATE() AS [now], dbo.fn_QuartzTimeToDateTime(t.NEXT_FIRE_TIME) AS next_fire_time, dbo.fn_QuartzTimeToDateTime(t.PREV_FIRE_TIME) AS prev_fire_time, dbo.fn_QuartzTimeToDateTime(t.START_TIME) AS start_time, t.* FROM qrtz_triggers t WITH (NOLOCK) -- WHERE dbo.fn_QuartzTimeToDateTime(t.NEXT_FIRE_TIME) < GETUTCDATE() -- опционально, показать только просроченные ORDER BY 1 DESC;
SELECT DATEDIFF_BIG(MINUTE, dbo.fn_QuartzTimeToDateTime(qft.FIRED_TIME), GETUTCDATE()) AS duration_min, GETUTCDATE() AS [now], dbo.fn_QuartzTimeToDateTime(qft.FIRED_TIME) AS fired_time, dbo.fn_QuartzTimeToDateTime(qft.SCHED_TIME) AS sched_time, qft.* FROM qrtz_fired_triggers qft WITH (NOLOCK) ORDER BY 1 DESC;
select extract(epoch from (now() - to_timestamp(t.next_fire_time/10000000.0 - 62135596800.0))) / 60 as overdue_min, now() at time zone 'UTC' as "now", to_timestamp(t.next_fire_time/10000000.0 - 62135596800.0) at time zone 'UTC' as next_fire_time, to_timestamp(t.prev_fire_time/10000000.0 - 62135596800.0) at time zone 'UTC' as prev_fire_time, to_timestamp(t.start_time/10000000.0 - 62135596800.0) at time zone 'UTC' as start_time, t.* from qrtz_triggers t -- where to_timestamp(t.next_fire_time/10000000.0 - 62135596800.0) < now() -- опционально, показать только просроченные order by 1 desc;
select extract(epoch from (now() - to_timestamp(qft.fired_time/10000000.0 - 62135596800.0))) / 60 as duration_min, now() at time zone 'UTC' as "now", to_timestamp(qft.fired_time/10000000.0 - 62135596800.0) at time zone 'UTC' as fired_time, to_timestamp(qft.sched_time/10000000.0 - 62135596800.0) at time zone 'UTC' as sched_time, now() at time zone 'UTC' as "now", qft.* from qrtz_fired_triggers qft order by 1 desc;
Скрипты для получения подробной информации по триггерам
SELECT IIF(spel.Id IS NULL, IIF(ss2.Id IS NULL, 'Other', 'Process'), 'Element') AS trigger_source, spel.Caption AS element_name, spl.Id AS sysprocesslog_id, COALESCE(spl.Name, ss2.Caption) AS process_name, COALESCE(ss.Name, ss2.Name) AS schema_name, COALESCE(sp.Name, sp2.Name) AS package_name, GETUTCDATE() AS [now], DATEDIFF_BIG(MINUTE, dbo.fn_QuartzTimeToDateTime(t.PREV_FIRE_TIME), GETUTCDATE()) AS prev_fire_min, DATEDIFF_BIG(MINUTE, GETUTCDATE(), dbo.fn_QuartzTimeToDateTime(t.NEXT_FIRE_TIME)) AS next_fire_min, dbo.fn_QuartzTimeToDateTime(t.NEXT_FIRE_TIME) AS next_fire_time, dbo.fn_QuartzTimeToDateTime(t.PREV_FIRE_TIME) AS prev_fire_time, dbo.fn_QuartzTimeToDateTime(t.START_TIME) AS start_time, t.* FROM QRTZ_TRIGGERS t WITH(NOLOCK) LEFT JOIN SysProcessElementLog spel WITH(NOLOCK) ON t.JOB_GROUP = CAST(spel.Id as nvarchar(max)) LEFT JOIN SysProcessLog spl WITH(NOLOCK) ON spel.SysProcessId = spl.Id LEFT JOIN SysSchema ss WITH(NOLOCK) ON spl.SysSchemaId = ss.Id LEFT JOIN SysPackage sp WITH(NOLOCK) ON ss.SysPackageId = sp.Id LEFT JOIN SysSchema ss2 WITH(NOLOCK) ON t.JOB_GROUP = CAST(ss2.UId as nvarchar(max)) LEFT JOIN SysPackage sp2 WITH(NOLOCK) ON ss2.SysPackageId = sp2.Id -- WHERE dbo.fn_QuartzTimeToDateTime(t.NEXT_FIRE_TIME) < GETUTCDATE() -- вывести только просроченные ORDER BY 9 ASC;
SELECT IIF(spel.Id IS NULL, IIF(ss2.Id IS NULL, 'Other', 'Process'), 'Element') AS trigger_source, spel.Caption as element_name, spl.Id AS sysprocesslog_id, COALESCE(spl.Name, ss2.Caption) AS process_name, COALESCE(ss.Name, ss2.Name) AS schema_name, COALESCE(sp.Name, sp2.Name) AS package_name, GETUTCDATE() AS [now], DATEDIFF_BIG(MINUTE, dbo.fn_QuartzTimeToDateTime(qft.FIRED_TIME), GETUTCDATE()) AS duration_min, dbo.fn_QuartzTimeToDateTime(qft.FIRED_TIME) AS fired_time, dbo.fn_QuartzTimeToDateTime(qft.SCHED_TIME) AS sched_time, qft.* FROM QRTZ_FIRED_TRIGGERS qft WITH(NOLOCK) LEFT JOIN SysProcessElementLog spel WITH(NOLOCK) ON qft.TRIGGER_GROUP = CAST(spel.Id as nvarchar(max)) LEFT JOIN SysProcessLog spl WITH(NOLOCK) ON spel.SysProcessId = spl.Id LEFT JOIN SysSchema ss WITH(NOLOCK) ON spl.SysSchemaId = ss.Id LEFT JOIN SysPackage sp WITH(NOLOCK) ON ss.SysPackageId = sp.Id LEFT JOIN SysSchema ss2 WITH(NOLOCK) ON qft.TRIGGER_GROUP = CAST(ss2.UId as nvarchar(max)) LEFT JOIN SysPackage sp2 WITH(NOLOCK) ON ss2.SysPackageId = sp2.Id ORDER BY 8 DESC;
SELECT
CASE WHEN spel."Id" IS NULL
THEN CASE WHEN ss2."Id" IS NULL THEN 'Other' ELSE 'Process' END
ELSE 'Element' END AS trigger_source,
spel."Caption" AS element_name,
spl."Id" AS sysprocesslog_id,
COALESCE(spl."Name", ss2."Caption") AS process_name,
COALESCE(ss."Name", ss2."Name") AS schema_name,
COALESCE(sp."Name", sp2."Name") AS package_name,
NOW() AS "now",
EXTRACT(epoch from (now() - to_timestamp(t.prev_fire_time /10000000.0 - 62135596800.0))) / 60 AS prev_fire_min,
EXTRACT(epoch from (to_timestamp(t.next_fire_time/10000000.0 - 62135596800.0) - now())) / 60 AS next_fire_min,
to_timestamp(t.next_fire_time/10000000.0 - 62135596800.0) at time zone 'UTC' AS next_fire_time,
to_timestamp(t.prev_fire_time/10000000.0 - 62135596800.0) at time zone 'UTC' AS prev_fire_time,
to_timestamp(t.start_time/10000000.0 - 62135596800.0) at time zone 'UTC' AS start_time,
t.*
FROM qrtz_triggers t
LEFT JOIN "SysProcessElementLog" spel ON t.job_group = spel."Id"::text
LEFT JOIN "SysProcessLog" spl ON spel."SysProcessId" = spl."Id"
LEFT JOIN "SysSchema" ss ON spl."SysSchemaId" = ss."Id"
LEFT JOIN "SysPackage" sp ON ss."SysPackageId" = sp."Id"
LEFT JOIN "SysSchema" ss2 ON t.job_group = ss2."UId"::text
LEFT JOIN "SysPackage" sp2 ON ss2."SysPackageId" = sp2."Id"
--WHERE to_timestamp(t.next_fire_time/10000000.0 - 62135596800.0) < NOW() -- вывести только просроченные
ORDER BY 9 ASC;
SELECT
CASE WHEN spel."Id" IS NULL
THEN CASE WHEN ss2."Id" IS NULL THEN 'Other' ELSE 'Process' END
ELSE 'Element' END AS trigger_source,
spel."Caption" AS element_name,
spl."Id" AS sysprocesslog_id,
COALESCE(spl."Name", ss2."Caption") AS process_name,
COALESCE(ss."Name", ss2."Name") AS schema_name,
COALESCE(sp."Name", sp2."Name") AS package_name,
NOW() at time zone 'UTC' AS "now",
EXTRACT(epoch from (now() - to_timestamp(qft.fired_time/10000000.0 - 62135596800.0))) / 60 AS duration_min,
to_timestamp(qft.fired_time/10000000.0 - 62135596800.0) at time zone 'UTC' AS fired_time,
to_timestamp(qft.sched_time/10000000.0 - 62135596800.0) at time zone 'UTC' AS sched_time,
qft.*
FROM qrtz_fired_triggers qft
LEFT JOIN "SysProcessElementLog" spel ON qft.trigger_group = spel."Id"::text
LEFT JOIN "SysProcessLog" spl ON spel."SysProcessId" = spl."Id"
LEFT JOIN "SysSchema" ss ON spl."SysSchemaId" = ss."Id"
LEFT JOIN "SysPackage" sp ON ss."SysPackageId" = sp."Id"
LEFT JOIN "SysSchema" ss2 ON qft.trigger_group = ss2."UId"::text
LEFT JOIN "SysPackage" sp2 ON ss2."SysPackageId" = sp2."Id"
ORDER BY 8 DESC;
Выполнение запроса по исправлению ошибок
Проанализируйте количество и характер ошибочных триггеров. Если их немного, восстановите работу триггеров с помощью запроса:
UPDATE QRTZ_TRIGGERS SET TRIGGER_STATE = 'WAITING' WHERE TRIGGER_STATE = 'ERROR';
После этого повторно выполните скрипт на получение просроченные триггеров Quartz и убедитесь, что статус ERROR сменился на WAITING.
Если ошибочных триггеров много, дополнительно изучите возможные причины и примите решение на основе анализа. При необходимости обратитесь за консультацией в Техническую поддержку .
Дополнительная диагностика нагрузки на серверы
Соберите данные о нагрузке стенда за 3 и более дней и проверьте активные триггеры Quartz.
Если среднее количество активных триггеров (avg) превышает 50% от общего числа потоков (quartz.threadPool.threadCount), указанного в конфигурационном файле, или, если наблюдаются продолжительные периоды (регулярно по несколько часов) максимального использования потоков, как показано на рисунке ниже, увеличьте quartz.threadPool.threadCount.
Рисунок 1 — Лимит клиента в 20 потоков достигается при пиковых нагрузках и длится более 1 часа

Увеличьте quartz.threadPool.threadCount (примерно на 30-200% в зависимости от нагрузки на очередь и анализа запроса на получение просроченных триггеров Quartz) и повторите этот этап диагностики.
Если на графике нет явных завышений, а в запросе на получение просроченных триггеров Quartz отсутствуют зависшие триггеры, но ошибки продолжаются, то проблема не в Quartz.
Если стабильные показатели не достигаются, проконсультируйтесь с Технической поддержкой BPMSoft о горизонтальном масштабировании стенда.
При высокой нагрузке на базу данных в таблицах Quartz или для исключения влияния сопутствующей нагрузки (например, других таблиц системы на таблицы Quartz и наоборот) рассмотрите возможность переноса таблиц Quartz в отдельную базу данных. Подробнее: Перенос таблицы Quartz в отдельную базу данных.