From a04e20c407a8c56247ae064372457a399916387a Mon Sep 17 00:00:00 2001 From: maliming Date: Sat, 21 Mar 2026 16:10:41 +0800 Subject: [PATCH] Fix UpdateScheduleAsync restart behavior and revert TickerQ IsRegistered - Fix Hangfire/Quartz UpdateScheduleAsync to operate directly on persistent scheduler state without checking the in-memory registry, consistent with the RemoveAsync fix; this allows updating schedules after restart - Revert TickerQ IsRegistered to return false instead of throwing; a boolean query method should not throw exceptions - Update docs: note that UpdateScheduleAsync works after restart for persistent providers --- .../infrastructure/background-workers/index.md | 2 +- .../Hangfire/HangfireDynamicBackgroundWorkerManager.cs | 8 +++----- .../Quartz/QuartzDynamicBackgroundWorkerManager.cs | 10 +++++----- .../TickerQ/TickerQDynamicBackgroundWorkerManager.cs | 5 ++--- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/docs/en/framework/infrastructure/background-workers/index.md b/docs/en/framework/infrastructure/background-workers/index.md index 8355739e95..5252d92ac5 100644 --- a/docs/en/framework/infrastructure/background-workers/index.md +++ b/docs/en/framework/infrastructure/background-workers/index.md @@ -176,7 +176,7 @@ var updated = await dynamicWorkerManager.UpdateScheduleAsync( * **`CronExpression` is only supported by scheduler-backed providers ([Hangfire](./hangfire.md), [Quartz](./quartz.md)).** The default in-memory provider requires `Period` and does not support `CronExpression` alone. * **[TickerQ](./tickerq.md) does not support dynamic background workers** because it uses `FrozenDictionary` for function registration, which requires all functions to be registered before the application starts. * `RemoveAsync` stops and removes a dynamic worker. Returns `true` if the worker was found and removed. -* `UpdateScheduleAsync` changes the schedule of an existing dynamic worker. Returns `true` if the worker was found and updated. The handler itself is not changed. +* `UpdateScheduleAsync` changes the schedule of an existing dynamic worker. Returns `true` if the worker was found and updated. The handler itself is not changed. For persistent providers (Hangfire, Quartz), this also works correctly after an application restart — the persistent scheduling record is updated even if the handler is no longer registered in memory. > **Important:** Dynamic worker handlers are stored **in memory only** and are not persisted across application restarts. When using a persistent scheduler provider (Hangfire or Quartz), the recurring job entries remain in the database after a restart, but the handlers will no longer be registered. Until the handler is re-registered, each scheduled execution will be **skipped with a warning log**. To ensure handlers are always available, register them in `OnApplicationInitializationAsync` so they are re-registered on every startup. diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerManager.cs index 6f7e7b325e..a9b9026606 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerManager.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireDynamicBackgroundWorkerManager.cs @@ -86,11 +86,6 @@ public class HangfireDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMa schedule.Validate(); - if (!HandlerRegistry.IsRegistered(workerName)) - { - return Task.FromResult(false); - } - var cronExpression = schedule.CronExpression; if (cronExpression.IsNullOrWhiteSpace()) { @@ -98,6 +93,9 @@ public class HangfireDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMa cronExpression = GetCron(period); } + // Always update the persistent recurring job regardless of in-memory registry state. + // This ensures UpdateScheduleAsync works correctly after an application restart, + // when the registry is empty but the Hangfire recurring job may still exist in the database. ScheduleRecurringJob(workerName, cronExpression, cancellationToken); return Task.FromResult(true); diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerManager.cs index f5145de8b6..5a729ad974 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerManager.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzDynamicBackgroundWorkerManager.cs @@ -92,11 +92,6 @@ public class QuartzDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMana schedule.Validate(); - if (!HandlerRegistry.IsRegistered(workerName)) - { - return false; - } - var triggerKey = new TriggerKey($"DynamicWorker:{workerName}"); var jobKey = new JobKey($"DynamicWorker:{workerName}"); var jobDetail = JobBuilder.Create() @@ -105,6 +100,11 @@ public class QuartzDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMana .Build(); var trigger = BuildTrigger(schedule, jobDetail, triggerKey); + + // Always attempt to reschedule the persistent job regardless of in-memory registry state. + // This ensures UpdateScheduleAsync works correctly after an application restart, + // when the registry is empty but the Quartz job may still exist in the scheduler store. + // RescheduleJob returns null if the trigger was not found, indicating the job did not exist. var result = await Scheduler.RescheduleJob(triggerKey, trigger, cancellationToken); return result != null; } diff --git a/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/TickerQDynamicBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/TickerQDynamicBackgroundWorkerManager.cs index 45e53fc53d..6976f49cb5 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/TickerQDynamicBackgroundWorkerManager.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/TickerQDynamicBackgroundWorkerManager.cs @@ -38,9 +38,8 @@ public class TickerQDynamicBackgroundWorkerManager : IDynamicBackgroundWorkerMan public virtual bool IsRegistered(string workerName) { - throw new AbpException( - "TickerQ does not support dynamic background worker registration at runtime. " + - "Please use Hangfire or Quartz provider for dynamic background workers."); + // TickerQ does not support runtime registration, so there are never any registered workers. + return false; } public virtual Task StopAllAsync(CancellationToken cancellationToken = default)