-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Background Jobs: Rewrite RecurringHostedServiceBase with SemaphoreSlim and add signalling support
#22331
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
AndyButland
merged 53 commits into
v17/dev
from
v17/feature/recurringbackgroundjob-signalling
May 21, 2026
Merged
Background Jobs: Rewrite RecurringHostedServiceBase with SemaphoreSlim and add signalling support
#22331
Changes from all commits
Commits
Show all changes
53 commits
Select commit
Hold shift + click to select a range
3c2f198
Compute next delay to compensate for time drift
ronaldbarendse a2d97f8
Use SemaphoreSlim to properly handle exceptions, cancellation tokens …
ronaldbarendse 8fe60d6
Add RecurringBackgroundJobBase to contain default values and hide obs…
ronaldbarendse d1ec78d
Add NextExecutionStrategy parameter to adjust the schedule after trig…
ronaldbarendse 9f8b533
Add TriggerExecution methods to RecurringBackgroundJobHostedServiceRu…
ronaldbarendse 4f50356
Merge branch 'main' into v17/feature/recurringbackgroundjob-signalling
ronaldbarendse de7e72e
Handle cancellation (application shutdown) and publish RecurringBackg…
ronaldbarendse 4eca474
Match hosted services by Type instead of type name string
ronaldbarendse d226d8c
Extract shared helper for TriggerExecution tests
ronaldbarendse 504a56c
Clear trigger state when initial delay is interrupted
ronaldbarendse 7aaa786
Clear _nextExecutionSkipOnOvershoot unconditionally
ronaldbarendse 42f15d8
Combine ComputeNextDelay tests
ronaldbarendse 7f29069
Consolidate trigger state into an immutable record for thread safety
ronaldbarendse 3607092
Use ConcurrentDictionary for thread-safe hosted service lookup
ronaldbarendse 3012138
Remove hosted services from dictionary on stop
ronaldbarendse 7d82afa
Fix API compatibility errors
ronaldbarendse 9e510e0
Removed unneeded using.
AndyButland 417c7ae
Register RecurringBackgroundJobHostedServiceRunner as resolvable sing…
ronaldbarendse 34e51e4
Remove failed hosted service from dictionary when StartAsync throws
ronaldbarendse 524a56d
Use semaphore signaling instead of Task.Delay in trigger tests
ronaldbarendse e984e0e
Inject TimeProvider into RecurringHostedServiceBase for deterministic…
ronaldbarendse 2a3df70
Use DelayCalculator.GetDelay instead of RecurringHostedServiceBase.Ge…
ronaldbarendse d245c02
Merge branch 'main' into v17/feature/recurringbackgroundjob-signalling
ronaldbarendse 5d1b7da
Fix Exception_In_PerformExecuteAsync_Does_Not_Kill_Loop test
ronaldbarendse 3f1325e
Avoid disposing period-change CTS while wait loop may still reference it
ronaldbarendse a2e11c7
Configure IEventMessagesFactory mock to return real EventMessages
ronaldbarendse 9c9518d
Clarify TriggerExecution(TimeSpan) docs and add ChangePeriod test
ronaldbarendse 312c20f
Validate period is positive and use GetOrAdd to avoid creating unused…
ronaldbarendse ed41368
Set up Period and Delay on mock job to satisfy constructor validation
ronaldbarendse 48f407b
Ensure PeriodChanged event is unsubscribed again
ronaldbarendse 7795e97
Fix trigger state race, simplify ReleaseSignal, and add canceled noti…
ronaldbarendse 7e97d46
Use Interlocked for _period reads/writes and implement thread-safe di…
ronaldbarendse 9c3ac1f
Remove hosted service from dictionary before stopping to prevent trig…
ronaldbarendse 0631f0a
Replace Task.Yield with semaphore timeouts in negative assertions
ronaldbarendse e078072
Tidy RecurringBackgroundJobBase docs and runner error handling
ronaldbarendse 49a1f44
Wait IgnoredDelay after ignored execution to prevent tight looping wh…
ronaldbarendse 4465ed5
Add IRecurringBackgroundJobTrigger<TJob> for opt-in job triggering
ronaldbarendse e19877a
Merge branch 'v17/dev' into v17/feature/recurringbackgroundjob-signal…
ronaldbarendse ca057fe
Register IRecurringBackgroundJobTrigger as open generic and drop AddT…
ronaldbarendse 87059b7
Fix and add parameter validation
ronaldbarendse 702a544
Allow Timeout.InfiniteTimeSpan as Period for manual-trigger-only recu…
ronaldbarendse c5b14cc
Migrate built-in jobs to RecurringBackgroundJobBase and require ITrig…
ronaldbarendse 7e511ec
Support infinite Delay and honor TriggerExecution(TimeSpan) issued du…
ronaldbarendse 4578aeb
Merge branch 'v17/dev' into v17/feature/recurringbackgroundjob-signal…
AndyButland d8938a8
Handle edge case of backoff via InfiniteTimeSpan.
AndyButland 68cd735
Refactored large method.
AndyButland 7f07265
Added clarifying documentation.
AndyButland 1aa631c
Suppress ExecutionContext flow when starting the recurring background…
AndyButland ca75501
Relocate Suppress ExecutionContext flow to avoid package validation e…
AndyButland cee97a8
Align IRecurringBackgroundJobTrigger generic type constraint with Add…
ronaldbarendse 4c3f7a7
Rename ApplyTriggerState to ComputeNextDelayFromTriggerState
ronaldbarendse 1e2f285
Allow Timeout.InfiniteTimeSpan as IgnoredDelay to fully disable a job…
ronaldbarendse edaf8fa
Fix generic type constraint
ronaldbarendse File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
75 changes: 63 additions & 12 deletions
75
src/Umbraco.Infrastructure/BackgroundJobs/IRecurringBackgroundJob.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,38 +1,89 @@ | ||
| using Umbraco.Cms.Core; | ||
| using Umbraco.Cms.Core.Sync; | ||
|
|
||
| namespace Umbraco.Cms.Infrastructure.BackgroundJobs; | ||
|
|
||
| /// <summary> | ||
| /// A recurring background job | ||
| /// A recurring background job. | ||
| /// </summary> | ||
| public interface IRecurringBackgroundJob | ||
| { | ||
| static readonly TimeSpan DefaultDelay = System.TimeSpan.FromMinutes(3); | ||
| static readonly ServerRole[] DefaultServerRoles = new[] { ServerRole.Single, ServerRole.SchedulingPublisher }; | ||
| /// <summary> | ||
| /// The default delay to use for recurring tasks for the first run after application start-up if no alternative is configured. | ||
| /// </summary> | ||
| [Obsolete("Use RecurringBackgroundJobBase.DefaultDelay instead. Scheduled for removal in Umbraco 19.")] | ||
| static readonly TimeSpan DefaultDelay = RecurringBackgroundJobBase.DefaultDelay; | ||
|
|
||
| /// <summary> | ||
| /// The default server roles that recurring background jobs run on. | ||
| /// </summary> | ||
| [Obsolete("Use RecurringBackgroundJobBase.DefaultServerRoles instead. Scheduled for removal in Umbraco 19.")] | ||
| static readonly ServerRole[] DefaultServerRoles = RecurringBackgroundJobBase.DefaultServerRoles; | ||
|
|
||
| /// <summary> | ||
| /// Timespan representing how often the task should recur. | ||
| /// </summary> | ||
| /// <value> | ||
| /// The period. | ||
| /// </value> | ||
| /// <remarks> | ||
| /// Set to <see cref="Timeout.InfiniteTimeSpan" /> to (temporarily) disable automatic scheduling and turn the job into a manually triggered one (via <see cref="IRecurringBackgroundJobTrigger{TJob}" />). Raise <see cref="PeriodChanged" /> to switch back to a finite period at runtime. | ||
| /// </remarks> | ||
| TimeSpan Period { get; } | ||
|
|
||
| /// <summary> | ||
| /// Timespan representing the initial delay after application start-up before the first run of the task | ||
| /// occurs. | ||
| /// Timespan representing the initial delay after application start-up before the first run of the task occurs. | ||
| /// </summary> | ||
| /// <value> | ||
| /// The delay. | ||
| /// </value> | ||
| /// <remarks> | ||
| /// Set to <see cref="Timeout.InfiniteTimeSpan" /> to skip the automatic first run entirely; the first execution then only occurs when manually triggered via <see cref="IRecurringBackgroundJobTrigger{TJob}" />. | ||
| /// </remarks> | ||
| TimeSpan Delay => RecurringBackgroundJobBase.DefaultDelay; // TODO (V19): Remove the default implementation | ||
|
|
||
| /// <summary> | ||
| /// Timespan to wait before re-evaluating execution conditions when an execution is ignored (e.g. runtime not ready, wrong server role or not main domain). | ||
| /// </summary> | ||
| TimeSpan Delay { get => DefaultDelay; } | ||
| /// <value> | ||
| /// The ignored delay. | ||
| /// </value> | ||
| /// <remarks> | ||
| /// This back-off prevents tight looping when <see cref="Period" /> is short (or <see cref="TimeSpan.Zero" />) and an execution is skipped without invoking <see cref="RunJobAsync(CancellationToken)" />. | ||
| /// Set to <see cref="Timeout.InfiniteTimeSpan" /> to disable the job for the remaining application lifecycle once an ignored condition is encountered — useful when the condition is known not to change (e.g. a server role that will not be promoted on this instance). | ||
| /// </remarks> | ||
| TimeSpan IgnoredDelay => RecurringBackgroundJobBase.DefaultIgnoredDelay; // TODO (V19): Remove the default implementation | ||
|
|
||
| /// <summary> | ||
| /// Gets the server roles for which this recurring background job is intended. | ||
| /// Gets the server roles the task executes on. | ||
| /// </summary> | ||
| ServerRole[] ServerRoles { get => DefaultServerRoles; } | ||
| /// <value> | ||
| /// The server roles. | ||
| /// </value> | ||
| ServerRole[] ServerRoles => RecurringBackgroundJobBase.DefaultServerRoles; // TODO (V19): Remove the default implementation | ||
|
|
||
| /// <summary> | ||
| /// This event should be raised when the <see cref="Period" /> property changes to notify the background job manager to update the schedule for this job. | ||
| /// </summary> | ||
| event EventHandler PeriodChanged; | ||
|
|
||
| /// <summary> | ||
| /// Executes the logic associated with the recurring background job asynchronously. | ||
| /// Runs the background job. | ||
| /// </summary> | ||
| /// <returns>A <see cref="System.Threading.Tasks.Task"/> that represents the asynchronous execution of the background job.</returns> | ||
| /// <returns> | ||
| /// A task representing the asynchronous operation. | ||
| /// </returns> | ||
| [Obsolete("Use RunJobAsync(CancellationToken) instead. Scheduled for removal in Umbraco 19.")] | ||
| Task RunJobAsync(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Runs the background job with cancellation support. | ||
| /// </summary> | ||
| /// <param name="cancellationToken">A cancellation token that is signaled when the host is shutting down.</param> | ||
| /// <returns> | ||
| /// A task representing the asynchronous operation. | ||
| /// </returns> | ||
| Task RunJobAsync(CancellationToken cancellationToken) | ||
| #pragma warning disable CS0618 // Type or member is obsolete | ||
| => RunJobAsync(); // TODO (V19): Remove the default implementation when RunJobAsync() is removed | ||
| #pragma warning restore CS0618 // Type or member is obsolete | ||
| } |
45 changes: 45 additions & 0 deletions
45
src/Umbraco.Infrastructure/BackgroundJobs/IRecurringBackgroundJobTrigger.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| // Copyright (c) Umbraco. | ||
| // See LICENSE for more details. | ||
|
|
||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Umbraco.Cms.Infrastructure.HostedServices; | ||
| using Umbraco.Extensions; | ||
|
|
||
| namespace Umbraco.Cms.Infrastructure.BackgroundJobs; | ||
|
|
||
| /// <summary> | ||
| /// Provides methods to signal a specific recurring background job to execute immediately. | ||
| /// </summary> | ||
| /// <typeparam name="TJob">The type of the recurring background job to trigger, as registered via <see cref="ServiceCollectionExtensions.AddRecurringBackgroundJob{TJob}(IServiceCollection)" />.</typeparam> | ||
| public interface IRecurringBackgroundJobTrigger<TJob> | ||
| where TJob : class, ITriggerableRecurringBackgroundJob | ||
| { | ||
| /// <summary> | ||
| /// Signals the background loop to execute immediately. | ||
| /// After the triggered execution, the original schedule is kept. | ||
| /// </summary> | ||
| /// <returns> | ||
| /// <c>true</c> if the job was found and triggered; <c>false</c> if no hosted service is running for this job type. | ||
| /// </returns> | ||
| /// <seealso cref="NextExecutionStrategy.None" /> | ||
| bool TriggerExecution(); | ||
|
|
||
| /// <summary> | ||
| /// Signals the background loop to execute immediately, with the specified strategy for determining the next execution after the triggered one completes. | ||
| /// </summary> | ||
| /// <param name="strategy">Controls the delay after the triggered execution.</param> | ||
| /// <returns> | ||
| /// <c>true</c> if the job was found and triggered; <c>false</c> if no hosted service is running for this job type. | ||
| /// </returns> | ||
| bool TriggerExecution(NextExecutionStrategy strategy); | ||
|
|
||
| /// <summary> | ||
| /// Signals the background loop to execute immediately. | ||
| /// After the triggered execution, the next execution is scheduled after the specified delay (measured from execution start; execution time is subtracted to prevent drift). | ||
| /// </summary> | ||
| /// <param name="nextDelay">The target interval from execution start to the next execution. Execution time is subtracted to prevent drift.</param> | ||
| /// <returns> | ||
| /// <c>true</c> if the job was found and triggered; <c>false</c> if no hosted service is running for this job type. | ||
| /// </returns> | ||
| bool TriggerExecution(TimeSpan nextDelay); | ||
| } | ||
11 changes: 11 additions & 0 deletions
11
src/Umbraco.Infrastructure/BackgroundJobs/ITriggerableRecurringBackgroundJob.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| // Copyright (c) Umbraco. | ||
| // See LICENSE for more details. | ||
|
|
||
| namespace Umbraco.Cms.Infrastructure.BackgroundJobs; | ||
|
|
||
| /// <summary> | ||
| /// Marker interface for recurring background jobs that support being triggered manually. | ||
| /// Only jobs implementing this interface can be triggered via <see cref="IRecurringBackgroundJobTrigger{TJob}" />. | ||
| /// </summary> | ||
| public interface ITriggerableRecurringBackgroundJob : IRecurringBackgroundJob | ||
| { } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.