Skip to content

Commit 20de48a

Browse files
ZeegaanCopilotnikolajlauridsen
authored
Load Balancing: Implement distributed background jobs (#20397)
* Start work * Introduce dto * Start making repository * Add migrations * Implement fetchable first job * Fix up to also finish tasks * Refactor jobs to distributed background jobs * Filter jobs correctly on LastRun * Hardcode delay * Add settings to configure delay and period * Fix formatting * Add default data * Add update on startup, which will update periods on startup * Refactor service to return job directly * Update src/Umbraco.Infrastructure/Services/Implement/DistributedJobService.cs Co-authored-by: Copilot <[email protected]> * Update src/Umbraco.Infrastructure/BackgroundJobs/DistributedBackgroundJobHostedService.cs Co-authored-by: Copilot <[email protected]> * Update src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs Co-authored-by: Copilot <[email protected]> * Update src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs Co-authored-by: Copilot <[email protected]> * Update src/Umbraco.Infrastructure/BackgroundJobs/DistributedBackgroundJobHostedService.cs Co-authored-by: Copilot <[email protected]> * Remove unused * Move jobs and make internal * make OpenIddictCleanupJob.cs public, as it is used elsewhere * Minor docstring changes * Update src/Umbraco.Core/Persistence/Constants-Locks.cs Co-authored-by: Mole <[email protected]> * ´Throw correct exceptions * Update xml doc * Remove business logic from repository * Remove more business logic from repository into service * Remove adding jobs from migration * fix creation * Rename to ExecuteAsync --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: mole <[email protected]>
1 parent f0cf470 commit 20de48a

File tree

39 files changed

+776
-177
lines changed

39 files changed

+776
-177
lines changed

src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
using Umbraco.Cms.Core;
1010
using Umbraco.Cms.Core.Configuration.Models;
1111
using Umbraco.Cms.Core.DependencyInjection;
12-
using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs;
13-
using Umbraco.Extensions;
12+
using Umbraco.Cms.Infrastructure.BackgroundJobs;
13+
using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs.DistributedJobs;
1414

1515
namespace Umbraco.Cms.Api.Common.DependencyInjection;
1616

@@ -139,7 +139,7 @@ private static void ConfigureOpenIddict(IUmbracoBuilder builder)
139139
});
140140
});
141141

142-
builder.Services.AddRecurringBackgroundJob<OpenIddictCleanupJob>();
142+
builder.Services.AddSingleton<IDistributedBackgroundJob, OpenIddictCleanupJob>();
143143
builder.Services.ConfigureOptions<ConfigureOpenIddict>();
144144
}
145145
}

src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.BackOffice.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public static IUmbracoBuilder
3030
.AddMembersIdentity()
3131
.AddUmbracoProfiler()
3232
.AddMvcAndRazor(configureMvc)
33-
.AddRecurringBackgroundJobs()
33+
.AddBackgroundJobs()
3434
.AddUmbracoHybridCache()
3535
.AddDistributedCache()
3636
.AddCoreNotifications();
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System.ComponentModel;
2+
3+
namespace Umbraco.Cms.Core.Configuration.Models;
4+
5+
/// <summary>
6+
/// Settings for distributed jobs.
7+
/// </summary>
8+
[UmbracoOptions(Constants.Configuration.ConfigDistributedJobs)]
9+
public class DistributedJobSettings
10+
{
11+
internal const string StaticPeriod = "00:00:10";
12+
internal const string StaticDelay = "00:01:00";
13+
14+
/// <summary>
15+
/// Gets or sets a value for the period of checking if there are any runnable distributed jobs.
16+
/// </summary>
17+
[DefaultValue(StaticPeriod)]
18+
public TimeSpan Period { get; set; } = TimeSpan.Parse(StaticPeriod);
19+
20+
/// <summary>
21+
/// Gets or sets a value for the delay of when to start checking for distributed jobs.
22+
/// </summary>
23+
[DefaultValue(StaticDelay)]
24+
public TimeSpan Delay { get; set; } = TimeSpan.Parse(StaticDelay);
25+
}

src/Umbraco.Core/Constants-Configuration.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public static class Configuration
6565
public const string ConfigWebhook = ConfigPrefix + "Webhook";
6666
public const string ConfigWebhookPayloadType = ConfigWebhook + ":PayloadType";
6767
public const string ConfigCache = ConfigPrefix + "Cache";
68+
public const string ConfigDistributedJobs = ConfigPrefix + "DistributedJobs";
6869

6970
public static class NamedOptions
7071
{

src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ public static IUmbracoBuilder AddConfiguration(this IUmbracoBuilder builder)
8787
.AddUmbracoOptions<DataTypesSettings>()
8888
.AddUmbracoOptions<WebhookSettings>()
8989
.AddUmbracoOptions<CacheSettings>()
90-
.AddUmbracoOptions<SystemDateMigrationSettings>();
90+
.AddUmbracoOptions<SystemDateMigrationSettings>()
91+
.AddUmbracoOptions<DistributedJobSettings>();
9192

9293
// Configure connection string and ensure it's updated when the configuration changes
9394
builder.Services.AddSingleton<IConfigureOptions<ConnectionStrings>, ConfigureConnectionStrings>();

src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ public static class Tables
103103
public const string Webhook2Headers = Webhook + "2Headers";
104104
public const string WebhookLog = Webhook + "Log";
105105
public const string WebhookRequest = Webhook + "Request";
106-
107106
public const string LongRunningOperation = TableNamePrefix + "LongRunningOperation";
107+
public const string DistributedJob = TableNamePrefix + "DistributedJob";
108108
}
109109
}
110110
}

src/Umbraco.Core/Persistence/Constants-Locks.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,5 +90,10 @@ public static class Locks
9090
/// All document URLs.
9191
/// </summary>
9292
public const int DocumentUrls = -345;
93+
94+
/// <summary>
95+
/// All distributed jobs.
96+
/// </summary>
97+
public const int DistributedJobs = -347;
9398
}
9499
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using System.Diagnostics;
2+
using Microsoft.Extensions.Hosting;
3+
using Microsoft.Extensions.Logging;
4+
using Microsoft.Extensions.Options;
5+
using Umbraco.Cms.Core;
6+
using Umbraco.Cms.Core.Configuration.Models;
7+
using Umbraco.Cms.Core.Services;
8+
using Umbraco.Cms.Infrastructure.Services;
9+
10+
namespace Umbraco.Cms.Infrastructure.BackgroundJobs;
11+
12+
/// <summary>
13+
/// A hosted service that checks for any runnable distributed background jobs on a timer.
14+
/// </summary>
15+
public class DistributedBackgroundJobHostedService : BackgroundService
16+
{
17+
private readonly ILogger<DistributedBackgroundJobHostedService> _logger;
18+
private readonly IRuntimeState _runtimeState;
19+
private readonly IDistributedJobService _distributedJobService;
20+
private DistributedJobSettings _distributedJobSettings;
21+
22+
/// <summary>
23+
/// Initializes a new instance of the <see cref="DistributedBackgroundJobHostedService"/> class.
24+
/// </summary>
25+
/// <param name="logger"></param>
26+
/// <param name="runtimeState"></param>
27+
/// <param name="distributedJobService"></param>
28+
/// <param name="distributedJobSettings"></param>
29+
public DistributedBackgroundJobHostedService(
30+
ILogger<DistributedBackgroundJobHostedService> logger,
31+
IRuntimeState runtimeState,
32+
IDistributedJobService distributedJobService,
33+
IOptionsMonitor<DistributedJobSettings> distributedJobSettings)
34+
{
35+
_logger = logger;
36+
_runtimeState = runtimeState;
37+
_distributedJobService = distributedJobService;
38+
_distributedJobSettings = distributedJobSettings.CurrentValue;
39+
distributedJobSettings.OnChange(options =>
40+
{
41+
_distributedJobSettings = options;
42+
});
43+
}
44+
45+
/// <inheritdoc />
46+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
47+
{
48+
await Task.Delay(_distributedJobSettings.Delay, stoppingToken);
49+
50+
while (_runtimeState.Level != RuntimeLevel.Run)
51+
{
52+
await Task.Delay(_distributedJobSettings.Delay, stoppingToken);
53+
}
54+
55+
// Update all jobs, periods might have changed when restarting.
56+
await _distributedJobService.EnsureJobsAsync();
57+
58+
using PeriodicTimer timer = new(_distributedJobSettings.Period);
59+
60+
try
61+
{
62+
while (await timer.WaitForNextTickAsync(stoppingToken))
63+
{
64+
await RunRunnableJob();
65+
}
66+
}
67+
catch (OperationCanceledException)
68+
{
69+
_logger.LogInformation("Timed Hosted Service is stopping.");
70+
}
71+
}
72+
73+
private async Task RunRunnableJob()
74+
{
75+
IDistributedBackgroundJob? job = await _distributedJobService.TryTakeRunnableAsync();
76+
77+
if (job is null)
78+
{
79+
// No runnable jobs for now, return
80+
return;
81+
}
82+
83+
try
84+
{
85+
await job.ExecuteAsync();
86+
}
87+
catch (Exception ex)
88+
{
89+
_logger.LogError(ex, "An exception occurred while running distributed background job '{JobName}'.", job.Name);
90+
}
91+
finally
92+
{
93+
try
94+
{
95+
await _distributedJobService.FinishAsync(job.Name);
96+
}
97+
catch (Exception ex)
98+
{
99+
_logger.LogError(ex, "An exception occurred while finishing distributed background job '{JobName}'.", job.Name);
100+
}
101+
}
102+
}
103+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace Umbraco.Cms.Infrastructure.BackgroundJobs;
2+
3+
/// <summary>
4+
/// A background job that will be executed by an available server. With a single server setup this will always be the same.
5+
/// With a load balanced setup, the executing server might change every time this needs to be executed.
6+
/// </summary>
7+
public interface IDistributedBackgroundJob
8+
{
9+
/// <summary>
10+
/// Name of the job.
11+
/// </summary>
12+
string Name { get; }
13+
14+
/// <summary>
15+
/// Timespan representing how often the task should recur.
16+
/// </summary>
17+
TimeSpan Period { get; }
18+
19+
/// <summary>
20+
/// Run the job.
21+
/// </summary>
22+
Task ExecuteAsync();
23+
}

src/Umbraco.Infrastructure/BackgroundJobs/Jobs/CacheInstructionsPruningJob.cs renamed to src/Umbraco.Infrastructure/BackgroundJobs/Jobs/DistributedJobs/CacheInstructionsPruningJob.cs

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22
using Umbraco.Cms.Core.Configuration.Models;
33
using Umbraco.Cms.Core.Persistence.Repositories;
44
using Umbraco.Cms.Core.Scoping;
5-
using Umbraco.Cms.Infrastructure.Scoping;
65

7-
namespace Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs;
6+
namespace Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs.DistributedJobs;
87

98
/// <summary>
109
/// A background job that prunes cache instructions from the database.
1110
/// </summary>
12-
public class CacheInstructionsPruningJob : IRecurringBackgroundJob
11+
internal class CacheInstructionsPruningJob : IDistributedBackgroundJob
1312
{
1413
private readonly IOptions<GlobalSettings> _globalSettings;
1514
private readonly ICacheInstructionRepository _cacheInstructionRepository;
@@ -36,18 +35,13 @@ public CacheInstructionsPruningJob(
3635
Period = globalSettings.Value.DatabaseServerMessenger.TimeBetweenPruneOperations;
3736
}
3837

39-
/// <inheritdoc />
40-
public event EventHandler PeriodChanged
41-
{
42-
add { }
43-
remove { }
44-
}
38+
public string Name => "CacheInstructionsPruningJob";
4539

4640
/// <inheritdoc />
4741
public TimeSpan Period { get; }
4842

4943
/// <inheritdoc />
50-
public Task RunJobAsync()
44+
public Task ExecuteAsync()
5145
{
5246
DateTimeOffset pruneDate = _timeProvider.GetUtcNow() - _globalSettings.Value.DatabaseServerMessenger.TimeToRetainInstructions;
5347
using (ICoreScope scope = _scopeProvider.CreateCoreScope())

0 commit comments

Comments
 (0)