Skip to content

Commit b42155d

Browse files
NielsPilgaardNiels Pilgaard Grøndahl
andauthored
Fix RecurringJob schedule inconsistencies (#91)
* added more jobs to the workerService example * formatting * BackgroundJobRegistrations now state whether they're RecurringJobs or not * added method for retrieving recurring jobs from the scheduler * added logic for scheduling and running recurring jobs by using a timer * removed unnecessary namespace * added tests to cover new functionality * Added two new recurring job types: IRecurringJobWithInitialDelay and IRecurringJobWithNoInitialDelay * added test to verify amount of cronjob occurrences * update approved public api doc * added registration methods for IRecurringJobWithInitialDelay * Added DelegateRecurringJobWithInitialDelay * updated test to work with new scheduler * removed IRecurringJobWithNoInitialDelay * Public signature update * added more to backgroundservice test * remove unneeded line --------- Co-authored-by: Niels Pilgaard Grøndahl <[email protected]>
1 parent 2c01fe9 commit b42155d

25 files changed

+494
-65
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using Cronos;
2+
using Pilgaard.BackgroundJobs;
3+
4+
namespace BackgroundJobs.WorkerService;
5+
6+
public class CronJob : ICronJob
7+
{
8+
private readonly ILogger<CronJob> _logger;
9+
public CronJob(ILogger<CronJob> logger)
10+
{
11+
_logger = logger;
12+
}
13+
14+
public Task RunJobAsync(CancellationToken cancellationToken = default)
15+
{
16+
_logger.LogInformation("{jobName} executed at {now:G}", nameof(CronJob), DateTime.Now);
17+
18+
return Task.CompletedTask;
19+
}
20+
21+
public CronExpression CronExpression => CronExpression.Parse("* * * * *");
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using Pilgaard.BackgroundJobs;
2+
3+
namespace BackgroundJobs.WorkerService;
4+
5+
public class OneTimeJob : IOneTimeJob
6+
{
7+
private readonly ILogger<OneTimeJob> _logger;
8+
private static readonly DateTime _utcNowAtStartup = DateTime.UtcNow;
9+
public OneTimeJob(ILogger<OneTimeJob> logger)
10+
{
11+
_logger = logger;
12+
}
13+
14+
public Task RunJobAsync(CancellationToken cancellationToken = default)
15+
{
16+
_logger.LogInformation("{jobName} executed at {now:G}", nameof(OneTimeJob), DateTime.Now);
17+
18+
return Task.CompletedTask;
19+
}
20+
21+
public DateTime ScheduledTimeUtc => _utcNowAtStartup.AddMinutes(1);
22+
}
Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
using BackgroundJobs.WorkerService;
22
using Pilgaard.BackgroundJobs;
33

4-
IHost host = Host.CreateDefaultBuilder(args)
4+
Host.CreateDefaultBuilder(args)
55
.ConfigureServices(services =>
66
{
77
services.AddBackgroundJobs()
8-
.AddJob<RecurringJob>(nameof(RecurringJob));
8+
.AddJob<RecurringJobEvery1Minute>()
9+
.AddJob<RecurringJobEvery5Minutes>()
10+
.AddJob<RecurringJobEvery10Minutes>()
11+
.AddJob<RecurringJobEvery30Minutes>()
12+
.AddJob<CronJob>()
13+
.AddJob<OneTimeJob>();
914
})
10-
.Build();
11-
12-
host.Run();
15+
.Build().Run();
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
1-
using Pilgaard.BackgroundJobs;
2-
3-
namespace BackgroundJobs.WorkerService;
4-
5-
public class RecurringJob : IRecurringJob
6-
{
7-
private readonly ILogger<RecurringJob> _logger;
8-
public RecurringJob(ILogger<RecurringJob> logger)
9-
{
10-
_logger = logger;
11-
}
12-
13-
public Task RunJobAsync(CancellationToken cancellationToken = default)
14-
{
15-
_logger.LogInformation("{jobName} executed at {now:G}", nameof(RecurringJob), DateTime.Now);
16-
17-
return Task.CompletedTask;
18-
}
19-
20-
public TimeSpan Interval => TimeSpan.FromMinutes(10);
21-
}
1+
using Pilgaard.BackgroundJobs;
2+
3+
namespace BackgroundJobs.WorkerService;
4+
5+
public class RecurringJobEvery10Minutes : IRecurringJob
6+
{
7+
private readonly ILogger<RecurringJobEvery10Minutes> _logger;
8+
public RecurringJobEvery10Minutes(ILogger<RecurringJobEvery10Minutes> logger)
9+
{
10+
_logger = logger;
11+
}
12+
13+
public Task RunJobAsync(CancellationToken cancellationToken = default)
14+
{
15+
_logger.LogInformation("{jobName} executed at {now:G}", nameof(RecurringJobEvery10Minutes), DateTime.Now);
16+
17+
return Task.CompletedTask;
18+
}
19+
20+
public TimeSpan Interval => TimeSpan.FromMinutes(10);
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Pilgaard.BackgroundJobs;
2+
3+
namespace BackgroundJobs.WorkerService;
4+
5+
public class RecurringJobEvery1Minute : IRecurringJob
6+
{
7+
private readonly ILogger<RecurringJobEvery1Minute> _logger;
8+
public RecurringJobEvery1Minute(ILogger<RecurringJobEvery1Minute> logger)
9+
{
10+
_logger = logger;
11+
}
12+
13+
public Task RunJobAsync(CancellationToken cancellationToken = default)
14+
{
15+
_logger.LogInformation("{jobName} executed at {now:G}", nameof(RecurringJobEvery1Minute), DateTime.Now);
16+
17+
return Task.CompletedTask;
18+
}
19+
20+
public TimeSpan Interval => TimeSpan.FromMinutes(1);
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Pilgaard.BackgroundJobs;
2+
3+
namespace BackgroundJobs.WorkerService;
4+
5+
public class RecurringJobEvery30Minutes : IRecurringJob
6+
{
7+
private readonly ILogger<RecurringJobEvery30Minutes> _logger;
8+
public RecurringJobEvery30Minutes(ILogger<RecurringJobEvery30Minutes> logger)
9+
{
10+
_logger = logger;
11+
}
12+
13+
public Task RunJobAsync(CancellationToken cancellationToken = default)
14+
{
15+
_logger.LogInformation("{jobName} executed at {now:G}", nameof(RecurringJobEvery30Minutes), DateTime.Now);
16+
17+
return Task.CompletedTask;
18+
}
19+
20+
public TimeSpan Interval => TimeSpan.FromMinutes(30);
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Pilgaard.BackgroundJobs;
2+
3+
namespace BackgroundJobs.WorkerService;
4+
5+
public class RecurringJobEvery5Minutes : IRecurringJob
6+
{
7+
private readonly ILogger<RecurringJobEvery5Minutes> _logger;
8+
public RecurringJobEvery5Minutes(ILogger<RecurringJobEvery5Minutes> logger)
9+
{
10+
_logger = logger;
11+
}
12+
13+
public Task RunJobAsync(CancellationToken cancellationToken = default)
14+
{
15+
_logger.LogInformation("{jobName} executed at {now:G}", nameof(RecurringJobEvery5Minutes), DateTime.Now);
16+
17+
return Task.CompletedTask;
18+
}
19+
20+
public TimeSpan Interval => TimeSpan.FromMinutes(5);
21+
}

src/Pilgaard.BackgroundJobs/BackgroundJobScheduler.cs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,14 @@ public BackgroundJobScheduler(IServiceScopeFactory scopeFactory,
3030
_options = options ?? throw new ArgumentNullException(nameof(options));
3131
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
3232

33+
if (registrationsValidator is null)
34+
{
35+
throw new ArgumentNullException(nameof(registrationsValidator));
36+
}
37+
3338
registrationsValidator.Validate(_options.Value.Registrations);
3439
}
3540

36-
/// <summary>
37-
/// Asynchronously retrieves an ordered enumerable of background job registrations.
38-
/// <para>
39-
/// Each background job registration is returned when it should be run.
40-
/// </para>
41-
/// </summary>
42-
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used for cancelling the enumeration.</param>
43-
/// <returns>An asynchronous enumerable of background job registrations.</returns>
4441
public async IAsyncEnumerable<BackgroundJobRegistration> GetBackgroundJobsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
4542
{
4643
var interval = TimeSpan.FromSeconds(30);
@@ -53,7 +50,7 @@ public async IAsyncEnumerable<BackgroundJobRegistration> GetBackgroundJobsAsync(
5350
{
5451
var intervalMinus5Seconds = interval.Subtract(TimeSpan.FromSeconds(5));
5552

56-
_logger.LogDebug("No background job occurrences found in the TimeSpan {interval}, " +
53+
_logger.LogDebug("No CronJob or OneTimeJob occurrences found in the TimeSpan {interval}, " +
5754
"waiting for TimeSpan {interval} until checking again.", interval, intervalMinus5Seconds);
5855

5956
await Task.Delay(intervalMinus5Seconds, cancellationToken);
@@ -78,6 +75,9 @@ public async IAsyncEnumerable<BackgroundJobRegistration> GetBackgroundJobsAsync(
7875
}
7976
}
8077

78+
79+
public IEnumerable<BackgroundJobRegistration> GetRecurringJobs() => _options.Value.Registrations.Where(registration => registration.IsRecurringJob);
80+
8181
/// <summary>
8282
/// Gets an ordered enumerable of background job occurrences within the specified <paramref name="fetchInterval"/>.
8383
/// </summary>
@@ -90,7 +90,8 @@ internal IEnumerable<BackgroundJobOccurrence> GetOrderedBackgroundJobOccurrences
9090
using var scope = _scopeFactory.CreateScope();
9191

9292
var backgroundJobOccurrences = new List<BackgroundJobOccurrence>();
93-
foreach (var registration in _options.Value.Registrations)
93+
94+
foreach (var registration in _options.Value.Registrations.Where(registration => !registration.IsRecurringJob))
9495
{
9596
var backgroundJob = registration.Factory(scope.ServiceProvider);
9697

src/Pilgaard.BackgroundJobs/BackgroundJobService.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ internal sealed class BackgroundJobService : IBackgroundJobService
2929
private readonly ILogger<BackgroundJobService> _logger;
3030
private readonly IBackgroundJobScheduler _backgroundJobScheduler;
3131

32+
private event Func<object, EventArgs, BackgroundJobRegistration, CancellationToken, Task>? RecurringJobTimerTriggered;
33+
private static readonly List<IDisposable> _recurringJobTimers = new();
34+
3235
private static readonly Meter _meter = new(
3336
name: typeof(BackgroundJobService).Assembly.GetName().Name!,
3437
version: typeof(BackgroundJobService).Assembly.GetName().Version?.ToString());
@@ -66,6 +69,8 @@ public BackgroundJobService(
6669
/// </returns>
6770
public async Task RunJobsAsync(CancellationToken cancellationToken = default)
6871
{
72+
ScheduleRecurringJobs(cancellationToken);
73+
6974
while (!cancellationToken.IsCancellationRequested)
7075
{
7176
_logger.LogDebug("Scheduling background jobs.");
@@ -83,6 +88,56 @@ public async Task RunJobsAsync(CancellationToken cancellationToken = default)
8388
}
8489
}
8590

91+
internal void ScheduleRecurringJobs(CancellationToken cancellationToken)
92+
{
93+
var recurringJobRegistrations = _backgroundJobScheduler.GetRecurringJobs();
94+
if (recurringJobRegistrations.Any())
95+
{
96+
RecurringJobTimerTriggered += RunRecurringJobAsync;
97+
}
98+
99+
using var scope = _scopeFactory.CreateScope();
100+
foreach (var jobRegistration in recurringJobRegistrations)
101+
{
102+
if (jobRegistration.Factory(scope.ServiceProvider) is not IRecurringJob recurringJob)
103+
{
104+
_logger.LogError("Failed to schedule recurring job {@jobRegistration}. " +
105+
"It does not implement {recurringJobInterface}",
106+
jobRegistration, typeof(IRecurringJob));
107+
continue;
108+
}
109+
110+
var dueTime = recurringJob switch
111+
{
112+
IRecurringJobWithInitialDelay recurringJobWithInitialDelay => recurringJobWithInitialDelay.InitialDelay,
113+
_ => recurringJob.Interval
114+
};
115+
116+
var recurringJobTimer = new System.Threading.Timer(_ => RecurringJobTimerTriggered?.Invoke(this, EventArgs.Empty, jobRegistration, cancellationToken),
117+
state: null,
118+
dueTime: dueTime,
119+
period: recurringJob.Interval);
120+
121+
_recurringJobTimers.Add(recurringJobTimer);
122+
123+
_logger.LogInformation("RecurringJob {jobName} has been scheduled to run every {interval}. " +
124+
"The first run will be in {dueTime}",
125+
jobRegistration.Name, recurringJob.Interval, dueTime);
126+
}
127+
}
128+
129+
#pragma warning disable IDE0060
130+
/// <summary>
131+
/// Runs the recurring job.
132+
/// </summary>
133+
/// <param name="sender">The sender. This is not used.</param>
134+
/// <param name="eventArgs">The <see cref="EventArgs"/> instance containing the event data. This is not used.</param>
135+
/// <param name="registration">The background job registration.</param>
136+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the background job.</param>
137+
internal async Task RunRecurringJobAsync(object sender, EventArgs eventArgs, BackgroundJobRegistration registration, CancellationToken cancellationToken)
138+
=> await RunJobAsync(registration, cancellationToken);
139+
#pragma warning restore IDE0060
140+
86141
/// <summary>
87142
/// Constructs the background job using <see cref="BackgroundJobRegistration.Factory"/> and runs it.
88143
/// </summary>
@@ -137,4 +192,12 @@ internal async Task RunJobAsync(BackgroundJobRegistration registration, Cancella
137192
timeoutCancellationTokenSource?.Dispose();
138193
}
139194
}
195+
196+
public void Dispose()
197+
{
198+
foreach (var disposable in _recurringJobTimers)
199+
{
200+
disposable.Dispose();
201+
}
202+
}
140203
}

src/Pilgaard.BackgroundJobs/IBackgroundJobScheduler.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,19 @@ internal interface IBackgroundJobScheduler
99
/// <summary>
1010
/// Asynchronously retrieves an ordered enumerable of background job registrations.
1111
/// <para>
12+
/// Jobs that implement <see cref="IRecurringJob"/> are not retrieved, they are scheduled during startup.
13+
/// </para>
14+
/// <para>
1215
/// Each background job registration is returned when it should be run.
1316
/// </para>
1417
/// </summary>
1518
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used for cancelling the enumeration.</param>
1619
/// <returns>An asynchronous enumerable of background job registrations.</returns>
1720
IAsyncEnumerable<BackgroundJobRegistration> GetBackgroundJobsAsync(CancellationToken cancellationToken);
21+
22+
/// <summary>
23+
/// Retrieves all <see cref="BackgroundJobRegistration"/>s where <see cref="BackgroundJobRegistration.IsRecurringJob"/> is <c>true</c>
24+
/// </summary>
25+
/// <returns>An enumerable of background job registrations where <see cref="BackgroundJobRegistration.IsRecurringJob"/> is <c>true</c></returns>
26+
IEnumerable<BackgroundJobRegistration> GetRecurringJobs();
1827
}

0 commit comments

Comments
 (0)