Background Jobs: Refine RecurringBackgroundJobBase API#22966
Conversation
|
Claude finished @ronaldbarendse's task in 6m 48s —— View job PR ReviewTarget: This PR refines
Important
Suggestions
Approved with Suggestions for improvementGood to go, but please carefully consider the importance of the suggestions. The race condition in |
There was a problem hiding this comment.
Pull request overview
Refines the new recurring background jobs API to make runtime schedule/back-off changes easier and safer by enhancing RecurringBackgroundJobBase, wiring up IgnoredDelayChanged handling in the hosted service, and migrating existing concrete jobs and tests to the new constructor-based period initialization.
Changes:
- Update
RecurringBackgroundJobBaseto require an initialPeriodvia constructor, add protected setters forPeriod/IgnoredDelaythat auto-raise change events, and implementIDisposable. - Enhance
RecurringBackgroundJobHostedServiceto react toIgnoredDelayChangedby interrupting in-progress ignored back-off waits and recomputing remaining time. - Migrate built-in jobs and unit tests to the updated base-class API and add new base-class unit tests.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobBase.cs |
Makes period constructor-required, adds change-signalling setters + events, and implements IDisposable. |
src/Umbraco.Infrastructure/BackgroundJobs/IRecurringBackgroundJob.cs |
Adds IgnoredDelayChanged (default implementation) and updates event commentary for future nullable-event change. |
src/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobHostedService.cs |
Subscribes to IgnoredDelayChanged and adds logic to interrupt/recompute ignored back-off waits. |
src/Umbraco.Infrastructure/BackgroundJobs/Jobs/TempFileCleanupJob.cs |
Migrates to : base(period) instead of overriding Period. |
src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ReportSiteJob.cs |
Migrates to : base(period) instead of overriding Period. |
src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/InstructionProcessJob.cs |
Migrates to : base(period) instead of storing a _period field + override. |
src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/TouchServerJob.cs |
Migrates to constructor period + uses protected Period setter on options changes; disposes options registration via Dispose(bool). |
tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobHostedServiceTests.cs |
Adds tests verifying ignored back-off can be interrupted/recomputed when IgnoredDelay changes. |
tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobHostedServiceRunnerTests.cs |
Updates internal test jobs to call : base(period) per new base-class contract. |
tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobBaseTests.cs |
Adds unit tests for constructor validation, event raising/no-op behavior, and subscriber clearing on dispose. |
…rd, clarify setter remarks - Swallow ObjectDisposedException in OnIgnoredDelayChanged for the shutdown race where an in-flight handler reads the to-be-disposed CTS via Interlocked.Exchange before Dispose disposes it. - Restore "skip back-off when IgnoredDelay <= TimeSpan.Zero (and not Timeout.InfiniteTimeSpan)" guard in IgnoreAndWaitAsync to defend against direct IRecurringBackgroundJob implementations / property overrides returning a negative value that would otherwise tight-loop via ComputeNextDelay clamping to zero. - Add regression test for the negative-IgnoredDelay skip path. - Mirror the constructor "stored without raising" remark on the Period and IgnoredDelay setter doc comments.
|
Thanks for the reviews. Pushed
On the Happy to revisit if you would prefer a different trade-off (e.g. dropping |
0c7c838 to
fa7d928
Compare
AndyButland
left a comment
There was a problem hiding this comment.
Looks good @ronaldbarendse. I pushed a small update to XML docs and introduced a helper for the "rotate and cancel" pattern you had used in a couple of places for the cancellation token.
I've verified with the various test jobs I had available from the last PR and all looks to work as expected. Will merge this in, cherry-pick up for 18 and update the docs.
* Add IgnoredDelayChanged event to allow updates during back-off * Make Period and IgnoredDelay settable on RecurringBackgroundJobBase with auto-raising events * Address PR review: handle CTS race, restore negative-IgnoredDelay guard, clarify setter remarks - Swallow ObjectDisposedException in OnIgnoredDelayChanged for the shutdown race where an in-flight handler reads the to-be-disposed CTS via Interlocked.Exchange before Dispose disposes it. - Restore "skip back-off when IgnoredDelay <= TimeSpan.Zero (and not Timeout.InfiniteTimeSpan)" guard in IgnoreAndWaitAsync to defend against direct IRecurringBackgroundJob implementations / property overrides returning a negative value that would otherwise tight-loop via ComputeNextDelay clamping to zero. - Add regression test for the negative-IgnoredDelay skip path. - Mirror the constructor "stored without raising" remark on the Period and IgnoredDelay setter doc comments. * Dispose newly-installed CTS when shutdown race wins the rotate-and-cancel * Clarify XML docs. * Introduce helper for cancellation source rotate and cancel. --------- Co-authored-by: Andy Butland <abutland73@gmail.com>
Summary
Refinements to
RecurringBackgroundJobBase, which was newly introduced in #22331 and is currently inrelease/17.5.0/release/18.0.0but not yet shipped to consumers (no RC tagged). The refinements significantly improve the ergonomics of writing recurring jobs, especially jobs whose schedule changes at runtime, and they add anIgnoredDelaysignalling capability that mirrors what already exists forPeriod.What changes
Protected setters on
PeriodandIgnoredDelay, with auto-raising events. The setters validate the value, no-op on same value, and raisePeriodChanged/IgnoredDelayChangedautomatically. Jobs no longer need to maintain their own_periodfield plus a custom event override just to react to config changes —Period = newValueis now enough.Periodis required via the base constructor.RecurringBackgroundJobBase(TimeSpan period)stores the initial value directly without raisingPeriodChanged. This replaces the previous "abstractPeriod { get; }" contract, eliminates the implicit-default question, and enforces initialisation at compile time.IDisposableon the base. StandardDispose()/protected virtual Dispose(bool disposing)pattern. Disposal clears_periodChangedand_ignoredDelayChangedso the disposed job does not retain subscriber references and cannot invoke listeners post-teardown.Concrete jobs migrated.
TempFileCleanupJob,ReportSiteJob,InstructionProcessJob, andTouchServerJobupdated to passPeriodviabase(period).TouchServerJobin particular shrinks substantially — no more custom event override, no custom_periodfield, and it now also disposes itsIOptionsMonitor.OnChange(...)registration via the newDispose(bool)hook.The previously-shipped
TouchServerJobpattern (manual_periodfield + customPeriodChangedevent override + manual_periodChanged?.Invoke(...)) is exactly the pattern the docs PR Complex example currently shows. After this PR, the canonical pattern is roughly half the code (see "Required docs updates" below).Breaking changes
RecurringBackgroundJobBaseconstructor now requiresTimeSpan period: base(period). Source-breaking but compile-time-detected.RecurringBackgroundJobBase.Periodchanged fromabstracttovirtualwithprotected setpublic override TimeSpan Period => ...still compiles.RecurringBackgroundJobBasenow implementsIDisposableDisposeneed no changes.Why these belong in 17.5 (and 18.0) rather than 17.6/18.1
RecurringBackgroundJobBaseis brand-new in Background Jobs: RewriteRecurringHostedServiceBasewithSemaphoreSlimand add signalling support #22331.release/17.5.0andrelease/18.0.0branches exist but no*-rchas been tagged. No consumer can currently depend on the old shape.RecurringBackgroundJobBase. TheIRecurringBackgroundJobinterface contract is unchanged (the V19 TODO comments on the events are purely informational — no runtime change).Changes required to docs PR umbraco/UmbracoDocs#8082
1. Minimal example: use
: base(period), drop thePeriodoverridepublic class CleanUpYourRoom : RecurringBackgroundJobBase { - public override TimeSpan Period => TimeSpan.FromMinutes(60); + public CleanUpYourRoom() + : base(TimeSpan.FromMinutes(60)) + { + } public override Task RunJobAsync(CancellationToken cancellationToken) { // your job code goes here return Task.CompletedTask; } }2. DI example: same pattern, just chain
: base(...)public CleanUpYourRoom( IContentService contentService, ICoreScopeProvider scopeProvider) + : base(TimeSpan.FromMinutes(60)) { _contentService = contentService; _scopeProvider = scopeProvider; } - -public override TimeSpan Period => TimeSpan.FromMinutes(60);3. Complex example: dramatically simplified
The current Complex example has ~15 lines of
_period+_periodChanged+ custom event override + manual invoke. The new equivalent is:Worth calling out in the prose around this example:
_periodChangedevent override is no longer required for the common "external config drives the period" case.Periodsetter is the new mechanism — assignment raises the event automatically and no-ops if the value is unchanged.IOptionsMonitor.OnChangereturns anIDisposableand should be disposed via the base'sDispose(bool)hook.4. New
IgnoredDelayChangedeventThe current docs PR documents the
IgnoredDelayproperty but does not mentionIgnoredDelayChanged. Add a short subsection (e.g. underIgnoredDelayor nearPeriodChanged):5. Manual-only jobs snippet
6. (Optional) "Base Classes" section
Brief note that
RecurringBackgroundJobBaseimplementsIDisposablewith aprotected virtual Dispose(bool)hook, and that subclasses with disposable resources (e.g. anIOptionsMonitor.OnChangeregistration) should override that hook and callbase.Dispose(disposing).Test plan
RecurringBackgroundJobBaseTests(12 tests) covers: constructor sets initialPeriod, infinite period allowed, negative throws,IgnoredDelaydefault, both setters raise the change event on different value, no-op on same value, infinite allowed, negative throws,DisposeclearsPeriodChangedandIgnoredDelayChangedsubscribers.RecurringBackgroundJobHostedServiceTestsunchanged and passing.RecurringBackgroundJobHostedServiceRunnerTestsupdated: internalTestJobA/TestJobBcall: base(TimeSpan.FromSeconds(30)).Fixes the
TouchServerJobcleanup gap as a side effect — the previous code held aglobalSettings.OnChange(...)registration with no disposal pathway.Once this is approved, it should also be cherry-picked to
release/18.0.0.