/
CronBackgroundService.cs
155 lines (130 loc) · 6.12 KB
/
CronBackgroundService.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
using System.Diagnostics.Metrics;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Pilgaard.CronJobs.Extensions;
namespace Pilgaard.CronJobs;
/// <summary>
/// This class is responsible for running a <see cref="ICronJob"/>,
/// by hosting it as a <see cref="BackgroundService"/>,
/// <para>
/// The <see cref="CronBackgroundService"/> runs <see cref="ICronJob.ExecuteAsync"/>
/// whenever <see cref="ICronJob.CronSchedule"/> triggers.
/// </para>
/// </summary>
/// <remarks>
/// See also: <seealso cref="BackgroundService" />
/// </remarks>
public class CronBackgroundService : BackgroundService
{
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly ICronJob _cronJob;
private readonly ILogger<CronBackgroundService> _logger;
private readonly string _jobName;
private static readonly Meter _meter = new(
name: typeof(CronBackgroundService).Assembly.GetName().Name!,
version: typeof(CronBackgroundService).Assembly.GetName().Version?.ToString());
private static readonly Histogram<double> _histogram =
_meter.CreateHistogram<double>(
name: $"{nameof(ICronJob)}.{nameof(ExecuteAsync)}".ToLower(),
unit: "milliseconds",
description: $"Histogram over duration and count of {nameof(ICronJob)}.{nameof(ICronJob.ExecuteAsync)}.");
/// <summary>
/// Initializes a new instance of the <see cref="CronBackgroundService"/> class.
/// </summary>
/// <param name="cronJob">The cronJob.</param>
/// <param name="serviceScopeFactory">The service scope factory.</param>
/// <param name="logger">The logger.</param>
public CronBackgroundService(
ICronJob cronJob,
IServiceScopeFactory serviceScopeFactory,
ILogger<CronBackgroundService> logger)
{
_serviceScopeFactory = serviceScopeFactory;
_logger = logger;
_cronJob = cronJob;
_jobName = _cronJob.GetType().Name;
_logger.LogInformation("Started {className} with Job {jobName}",
nameof(CronBackgroundService), _jobName);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var nextTaskOccurrence = GetNextOccurrence();
while (nextTaskOccurrence is not null &&
stoppingToken.IsCancellationRequested is false)
{
_logger.LogDebug("The next time {jobName} will execute is {nextTaskOccurrence}", _jobName, nextTaskOccurrence);
await PerformTaskOnNextOccurrenceAsync(nextTaskOccurrence, stoppingToken).ConfigureAwait(false);
nextTaskOccurrence = GetNextOccurrence();
}
}
/// <summary>
/// Performs the task on next occurrence.
/// </summary>
/// <param name="nextTaskExecution">The next task execution.</param>
/// <param name="stoppingToken">The stopping token.</param>
private async Task PerformTaskOnNextOccurrenceAsync(
DateTime? nextTaskExecution,
CancellationToken stoppingToken)
{
// If UtcNow is higher than the time to execute next, we've already passed it
if (NextOccurrenceIsInThePast(nextTaskExecution))
{
return;
}
var delay = TimeUntilNextOccurrence(nextTaskExecution);
await Task.Delay(delay, stoppingToken).ConfigureAwait(false);
// Measure duration of ExecuteAsync
using var timer = _histogram.NewTimer(tags:
new[]{
new KeyValuePair<string, object?>("job_name", _jobName)
});
// If ServiceLifetime is Transient or Scoped, we need to re-fetch the
// CronJob from the ServiceProvider on every execution.
if (_cronJob.ServiceLifetime is not ServiceLifetime.Singleton)
{
_logger.LogDebug("Fetching a {serviceLifetime} instance of {jobName} from the ServiceProvider.",
_cronJob.ServiceLifetime,
_jobName);
using var scope = _serviceScopeFactory.CreateScope();
var cronJob = (ICronJob)scope.ServiceProvider.GetRequiredService(_cronJob.GetType());
await cronJob.ExecuteAsync(stoppingToken).ConfigureAwait(false);
_logger.LogDebug("Successfully executed the Job {jobName}", _jobName);
return;
}
await _cronJob.ExecuteAsync(stoppingToken).ConfigureAwait(false);
_logger.LogDebug("Successfully executed the Job {jobName}", _jobName);
}
/// <summary>
/// Gets the <see cref="DateTime"/> of the next time
/// <see cref="ICronJob.ExecuteAsync"/> should trigger.
/// </summary>
/// <returns>
/// The <see cref="DateTime"/> of the next time
/// <see cref="ICronJob.ExecuteAsync"/> should trigger.
/// </returns>
private DateTime? GetNextOccurrence()
=> _cronJob.CronSchedule.GetNextOccurrence(DateTime.UtcNow, _cronJob.TimeZoneInfo);
/// <summary>
/// Checks whether the <paramref name="nextTaskExecution"/> is before
/// <see cref="DateTime.UtcNow"/>.
/// </summary>
/// <param name="nextTaskExecution">The next task execution.</param>
/// <returns>
/// <c>true</c> if <paramref name="nextTaskExecution"/>
/// is before <see cref="DateTime.UtcNow"/>, otherwise <c>false</c>
/// </returns>
private static bool NextOccurrenceIsInThePast(DateTime? nextTaskExecution)
=> DateTime.UtcNow > nextTaskExecution.GetValueOrDefault();
/// <summary>
/// Gets the <see cref="TimeSpan"/> until the next
/// <see cref="ICronJob.ExecuteAsync"/> should be triggered.
/// </summary>
/// <param name="nextTaskExecutionTime">The next task execution.</param>
/// <returns>
/// The <see cref="TimeSpan"/> until the next
/// <see cref="ICronJob.ExecuteAsync"/> should be triggered.
/// </returns>
private static TimeSpan TimeUntilNextOccurrence(DateTime? nextTaskExecutionTime)
=> nextTaskExecutionTime.GetValueOrDefault() - DateTime.UtcNow;
}