Skip to content

Commit 860c8e8

Browse files
author
Paul Johnson
authored
Filesystem based MainDomLock & extract interface for MainDomKey generation (#12037)
* Extract MainDomKey generation to its own class to ease customization. Also add discriminator config value to GlobalSettings for advanced users. Prevents a mandatory custom implementation, should be good enough for the vast majority of use cases. * Prevent duplicate runs of ScheduledPublishing during slot swap. * Add filesystem based MainDomLock
1 parent dafd7f2 commit 860c8e8

File tree

15 files changed

+457
-36
lines changed

15 files changed

+457
-36
lines changed

src/Umbraco.Core/Configuration/Models/GlobalSettings.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,14 @@ public class GlobalSettings
137137
/// </summary>
138138
public string MainDomLock { get; set; } = string.Empty;
139139

140+
/// <summary>
141+
/// Gets or sets a value to discriminate MainDom boundaries.
142+
/// <para>
143+
/// Generally the default should suffice but useful for advanced scenarios e.g. azure deployment slot based zero downtime deployments.
144+
/// </para>
145+
/// </summary>
146+
public string MainDomKeyDiscriminator { get; set; } = string.Empty;
147+
140148
/// <summary>
141149
/// Gets or sets the telemetry ID.
142150
/// </summary>

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ public static class Locks
6565
/// All languages.
6666
/// </summary>
6767
public const int Languages = -340;
68+
69+
/// <summary>
70+
/// ScheduledPublishing job.
71+
/// </summary>
72+
public const int ScheduledPublishing = -341;
6873
}
6974
}
7075
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Umbraco.Cms.Core.Runtime
2+
{
3+
/// <summary>
4+
/// Defines a class which can generate a distinct key for a MainDom boundary.
5+
/// </summary>
6+
public interface IMainDomKeyGenerator
7+
{
8+
/// <summary>
9+
/// Returns a key that signifies a MainDom boundary.
10+
/// </summary>
11+
string GenerateKey();
12+
}
13+
}

src/Umbraco.Core/Runtime/MainDom.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public bool Register(Action install = null, Action release = null, int weight =
8787

8888
if (_isMainDom.HasValue == false)
8989
{
90-
throw new InvalidOperationException("Register called when MainDom has not been acquired");
90+
throw new InvalidOperationException("Register called before IsMainDom has been established");
9191
}
9292
else if (_isMainDom == false)
9393
{
@@ -225,7 +225,7 @@ public bool IsMainDom
225225
{
226226
if (!_isMainDom.HasValue)
227227
{
228-
throw new InvalidOperationException("MainDom has not been acquired yet");
228+
throw new InvalidOperationException("IsMainDom has not been established yet");
229229
}
230230
return _isMainDom.Value;
231231
}

src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ private static IUmbracoBuilder AddLogging(this IUmbracoBuilder builder)
218218

219219
private static IUmbracoBuilder AddMainDom(this IUmbracoBuilder builder)
220220
{
221+
builder.Services.AddSingleton<IMainDomKeyGenerator, DefaultMainDomKeyGenerator>();
221222
builder.Services.AddSingleton<IMainDomLock>(factory =>
222223
{
223224
var globalSettings = factory.GetRequiredService<IOptions<GlobalSettings>>();
@@ -229,15 +230,20 @@ private static IUmbracoBuilder AddMainDom(this IUmbracoBuilder builder)
229230
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
230231
var loggerFactory = factory.GetRequiredService<ILoggerFactory>();
231232
var npocoMappers = factory.GetRequiredService<NPocoMapperCollection>();
233+
var mainDomKeyGenerator = factory.GetRequiredService<IMainDomKeyGenerator>();
234+
235+
if (globalSettings.Value.MainDomLock == "FileSystemMainDomLock")
236+
{
237+
return new FileSystemMainDomLock(loggerFactory.CreateLogger<FileSystemMainDomLock>(), mainDomKeyGenerator, hostingEnvironment);
238+
}
232239

233240
return globalSettings.Value.MainDomLock.Equals("SqlMainDomLock") || isWindows == false
234241
? (IMainDomLock)new SqlMainDomLock(
235-
loggerFactory.CreateLogger<SqlMainDomLock>(),
236242
loggerFactory,
237243
globalSettings,
238244
connectionStrings,
239245
dbCreator,
240-
hostingEnvironment,
246+
mainDomKeyGenerator,
241247
databaseSchemaCreatorFactory,
242248
npocoMappers)
243249
: new MainDomSemaphoreLock(loggerFactory.CreateLogger<MainDomSemaphoreLock>(), hostingEnvironment);

src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
using System.Collections.Generic;
66
using System.Linq;
77
using System.Threading.Tasks;
8+
using Microsoft.Extensions.DependencyInjection;
89
using Microsoft.Extensions.Logging;
910
using Umbraco.Cms.Core;
1011
using Umbraco.Cms.Core.Runtime;
11-
using Umbraco.Cms.Core.Security;
12+
using Umbraco.Cms.Core.Scoping;
1213
using Umbraco.Cms.Core.Services;
1314
using Umbraco.Cms.Core.Sync;
1415
using Umbraco.Cms.Core.Web;
16+
using Umbraco.Cms.Web.Common.DependencyInjection;
1517

1618
namespace Umbraco.Cms.Infrastructure.HostedServices
1719
{
@@ -27,20 +29,16 @@ public class ScheduledPublishing : RecurringHostedServiceBase
2729
private readonly IMainDom _mainDom;
2830
private readonly IRuntimeState _runtimeState;
2931
private readonly IServerMessenger _serverMessenger;
32+
private readonly IScopeProvider _scopeProvider;
3033
private readonly IServerRoleAccessor _serverRegistrar;
3134
private readonly IUmbracoContextFactory _umbracoContextFactory;
3235

3336
/// <summary>
3437
/// Initializes a new instance of the <see cref="ScheduledPublishing"/> class.
3538
/// </summary>
36-
/// <param name="runtimeState">Representation of the state of the Umbraco runtime.</param>
37-
/// <param name="mainDom">Representation of the main application domain.</param>
38-
/// <param name="serverRegistrar">Provider of server registrations to the distributed cache.</param>
39-
/// <param name="contentService">Service for handling content operations.</param>
40-
/// <param name="umbracoContextFactory">Service for creating and managing Umbraco context.</param>
41-
/// <param name="logger">The typed logger.</param>
42-
/// <param name="serverMessenger">Service broadcasting cache notifications to registered servers.</param>
43-
/// <param name="backofficeSecurityFactory">Creates and manages <see cref="IBackOfficeSecurity"/> instances.</param>
39+
// Note: Ignoring the two version notice rule as this class should probably be internal.
40+
// We don't expect anyone downstream to be instantiating a HostedService
41+
[Obsolete("This constructor will be removed in version 10, please use an alternative constructor.")]
4442
public ScheduledPublishing(
4543
IRuntimeState runtimeState,
4644
IMainDom mainDom,
@@ -49,6 +47,30 @@ public ScheduledPublishing(
4947
IUmbracoContextFactory umbracoContextFactory,
5048
ILogger<ScheduledPublishing> logger,
5149
IServerMessenger serverMessenger)
50+
: this(
51+
runtimeState,
52+
mainDom,
53+
serverRegistrar,
54+
contentService,
55+
umbracoContextFactory,
56+
logger,
57+
serverMessenger,
58+
StaticServiceProvider.Instance.GetRequiredService<IScopeProvider>())
59+
{
60+
}
61+
62+
/// <summary>
63+
/// Initializes a new instance of the <see cref="ScheduledPublishing"/> class.
64+
/// </summary>
65+
public ScheduledPublishing(
66+
IRuntimeState runtimeState,
67+
IMainDom mainDom,
68+
IServerRoleAccessor serverRegistrar,
69+
IContentService contentService,
70+
IUmbracoContextFactory umbracoContextFactory,
71+
ILogger<ScheduledPublishing> logger,
72+
IServerMessenger serverMessenger,
73+
IScopeProvider scopeProvider)
5274
: base(TimeSpan.FromMinutes(1), DefaultDelay)
5375
{
5476
_runtimeState = runtimeState;
@@ -58,6 +80,7 @@ public ScheduledPublishing(
5880
_umbracoContextFactory = umbracoContextFactory;
5981
_logger = logger;
6082
_serverMessenger = serverMessenger;
83+
_scopeProvider = scopeProvider;
6184
}
6285

6386
public override Task PerformExecuteAsync(object state)
@@ -93,8 +116,6 @@ public override Task PerformExecuteAsync(object state)
93116

94117
try
95118
{
96-
// We don't need an explicit scope here because PerformScheduledPublish creates it's own scope
97-
// so it's safe as it will create it's own ambient scope.
98119
// Ensure we run with an UmbracoContext, because this will run in a background task,
99120
// and developers may be using the UmbracoContext in the event handlers.
100121

@@ -105,6 +126,14 @@ public override Task PerformExecuteAsync(object state)
105126
// - and we should definitively *not* have to flush it here (should be auto)
106127

107128
using UmbracoContextReference contextReference = _umbracoContextFactory.EnsureUmbracoContext();
129+
using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
130+
131+
/* We used to assume that there will never be two instances running concurrently where (IsMainDom && ServerRole == SchedulingPublisher)
132+
* However this is possible during an azure deployment slot swap for the SchedulingPublisher instance when trying to achieve zero downtime deployments.
133+
* If we take a distributed write lock, we are certain that the multiple instances of the job will not run in parallel.
134+
* It's possible that during the swapping process we may run this job more frequently than intended but this is not of great concern and it's
135+
* only until the old SchedulingPublisher shuts down. */
136+
scope.EagerWriteLock(Constants.Locks.ScheduledPublishing);
108137
try
109138
{
110139
// Run

src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ private void CreateLockData()
175175
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.Domains, Name = "Domains" });
176176
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.KeyValues, Name = "KeyValues" });
177177
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.Languages, Name = "Languages" });
178+
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.ScheduledPublishing, Name = "ScheduledPublishing" });
178179

179180
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.MainDom, Name = "MainDom" });
180181
}

src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_1_0;
1717
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_2_0;
1818
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0;
19+
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0;
1920
using Umbraco.Extensions;
2021

2122
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade
@@ -280,6 +281,8 @@ protected void DefinePlan()
280281
To<UpdateExternalLoginToUseKeyInsteadOfId>("{CA7A1D9D-C9D4-4914-BC0A-459E7B9C3C8C}");
281282
To<AddTwoFactorLoginTable>("{0828F206-DCF7-4F73-ABBB-6792275532EB}");
282283

284+
// TO 9.4.0
285+
To<AddScheduledPublishingLock>("{DBBA1EA0-25A1-4863-90FB-5D306FB6F1E1}");
283286
}
284287
}
285288
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
2+
3+
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0
4+
{
5+
internal class AddScheduledPublishingLock : MigrationBase
6+
{
7+
public AddScheduledPublishingLock(IMigrationContext context)
8+
: base(context)
9+
{
10+
}
11+
12+
protected override void Migrate() =>
13+
Database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.ScheduledPublishing, Name = "ScheduledPublishing" });
14+
}
15+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System;
2+
using System.Security.Cryptography;
3+
using Microsoft.Extensions.Options;
4+
using Umbraco.Cms.Core.Configuration.Models;
5+
using Umbraco.Cms.Core.Hosting;
6+
using Umbraco.Cms.Core.Runtime;
7+
using Umbraco.Extensions;
8+
9+
namespace Umbraco.Cms.Infrastructure.Runtime
10+
{
11+
12+
internal class DefaultMainDomKeyGenerator : IMainDomKeyGenerator
13+
{
14+
private readonly IHostingEnvironment _hostingEnvironment;
15+
private readonly IOptionsMonitor<GlobalSettings> _globalSettings;
16+
17+
public DefaultMainDomKeyGenerator(IHostingEnvironment hostingEnvironment, IOptionsMonitor<GlobalSettings> globalSettings)
18+
{
19+
_hostingEnvironment = hostingEnvironment;
20+
_globalSettings = globalSettings;
21+
}
22+
23+
public string GenerateKey()
24+
{
25+
var machineName = Environment.MachineName;
26+
var mainDomId = MainDom.GetMainDomId(_hostingEnvironment);
27+
var discriminator = _globalSettings.CurrentValue.MainDomKeyDiscriminator;
28+
29+
var rawKey = $"{machineName}{mainDomId}{discriminator}";
30+
31+
return rawKey.GenerateHash<SHA1>();
32+
}
33+
}
34+
}

0 commit comments

Comments
 (0)