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
This commit is contained in:
@@ -137,6 +137,14 @@ namespace Umbraco.Cms.Core.Configuration.Models
|
||||
/// </summary>
|
||||
public string MainDomLock { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value to discriminate MainDom boundaries.
|
||||
/// <para>
|
||||
/// Generally the default should suffice but useful for advanced scenarios e.g. azure deployment slot based zero downtime deployments.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public string MainDomKeyDiscriminator { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the telemetry ID.
|
||||
/// </summary>
|
||||
|
||||
@@ -65,6 +65,11 @@ namespace Umbraco.Cms.Core
|
||||
/// All languages.
|
||||
/// </summary>
|
||||
public const int Languages = -340;
|
||||
|
||||
/// <summary>
|
||||
/// ScheduledPublishing job.
|
||||
/// </summary>
|
||||
public const int ScheduledPublishing = -341;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
src/Umbraco.Core/Runtime/IMainDomKeyGenerator.cs
Normal file
13
src/Umbraco.Core/Runtime/IMainDomKeyGenerator.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Umbraco.Cms.Core.Runtime
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a class which can generate a distinct key for a MainDom boundary.
|
||||
/// </summary>
|
||||
public interface IMainDomKeyGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a key that signifies a MainDom boundary.
|
||||
/// </summary>
|
||||
string GenerateKey();
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ namespace Umbraco.Cms.Core.Runtime
|
||||
|
||||
if (_isMainDom.HasValue == false)
|
||||
{
|
||||
throw new InvalidOperationException("Register called when MainDom has not been acquired");
|
||||
throw new InvalidOperationException("Register called before IsMainDom has been established");
|
||||
}
|
||||
else if (_isMainDom == false)
|
||||
{
|
||||
@@ -225,7 +225,7 @@ namespace Umbraco.Cms.Core.Runtime
|
||||
{
|
||||
if (!_isMainDom.HasValue)
|
||||
{
|
||||
throw new InvalidOperationException("MainDom has not been acquired yet");
|
||||
throw new InvalidOperationException("IsMainDom has not been established yet");
|
||||
}
|
||||
return _isMainDom.Value;
|
||||
}
|
||||
|
||||
@@ -218,6 +218,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
|
||||
|
||||
private static IUmbracoBuilder AddMainDom(this IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.AddSingleton<IMainDomKeyGenerator, DefaultMainDomKeyGenerator>();
|
||||
builder.Services.AddSingleton<IMainDomLock>(factory =>
|
||||
{
|
||||
var globalSettings = factory.GetRequiredService<IOptions<GlobalSettings>>();
|
||||
@@ -229,15 +230,20 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
|
||||
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
var loggerFactory = factory.GetRequiredService<ILoggerFactory>();
|
||||
var npocoMappers = factory.GetRequiredService<NPocoMapperCollection>();
|
||||
var mainDomKeyGenerator = factory.GetRequiredService<IMainDomKeyGenerator>();
|
||||
|
||||
if (globalSettings.Value.MainDomLock == "FileSystemMainDomLock")
|
||||
{
|
||||
return new FileSystemMainDomLock(loggerFactory.CreateLogger<FileSystemMainDomLock>(), mainDomKeyGenerator, hostingEnvironment);
|
||||
}
|
||||
|
||||
return globalSettings.Value.MainDomLock.Equals("SqlMainDomLock") || isWindows == false
|
||||
? (IMainDomLock)new SqlMainDomLock(
|
||||
loggerFactory.CreateLogger<SqlMainDomLock>(),
|
||||
loggerFactory,
|
||||
globalSettings,
|
||||
connectionStrings,
|
||||
dbCreator,
|
||||
hostingEnvironment,
|
||||
mainDomKeyGenerator,
|
||||
databaseSchemaCreatorFactory,
|
||||
npocoMappers)
|
||||
: new MainDomSemaphoreLock(loggerFactory.CreateLogger<MainDomSemaphoreLock>(), hostingEnvironment);
|
||||
|
||||
@@ -5,13 +5,15 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Runtime;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Sync;
|
||||
using Umbraco.Cms.Core.Web;
|
||||
using Umbraco.Cms.Web.Common.DependencyInjection;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HostedServices
|
||||
{
|
||||
@@ -27,20 +29,16 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
|
||||
private readonly IMainDom _mainDom;
|
||||
private readonly IRuntimeState _runtimeState;
|
||||
private readonly IServerMessenger _serverMessenger;
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
private readonly IServerRoleAccessor _serverRegistrar;
|
||||
private readonly IUmbracoContextFactory _umbracoContextFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ScheduledPublishing"/> class.
|
||||
/// </summary>
|
||||
/// <param name="runtimeState">Representation of the state of the Umbraco runtime.</param>
|
||||
/// <param name="mainDom">Representation of the main application domain.</param>
|
||||
/// <param name="serverRegistrar">Provider of server registrations to the distributed cache.</param>
|
||||
/// <param name="contentService">Service for handling content operations.</param>
|
||||
/// <param name="umbracoContextFactory">Service for creating and managing Umbraco context.</param>
|
||||
/// <param name="logger">The typed logger.</param>
|
||||
/// <param name="serverMessenger">Service broadcasting cache notifications to registered servers.</param>
|
||||
/// <param name="backofficeSecurityFactory">Creates and manages <see cref="IBackOfficeSecurity"/> instances.</param>
|
||||
// Note: Ignoring the two version notice rule as this class should probably be internal.
|
||||
// We don't expect anyone downstream to be instantiating a HostedService
|
||||
[Obsolete("This constructor will be removed in version 10, please use an alternative constructor.")]
|
||||
public ScheduledPublishing(
|
||||
IRuntimeState runtimeState,
|
||||
IMainDom mainDom,
|
||||
@@ -49,6 +47,30 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
|
||||
IUmbracoContextFactory umbracoContextFactory,
|
||||
ILogger<ScheduledPublishing> logger,
|
||||
IServerMessenger serverMessenger)
|
||||
: this(
|
||||
runtimeState,
|
||||
mainDom,
|
||||
serverRegistrar,
|
||||
contentService,
|
||||
umbracoContextFactory,
|
||||
logger,
|
||||
serverMessenger,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IScopeProvider>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ScheduledPublishing"/> class.
|
||||
/// </summary>
|
||||
public ScheduledPublishing(
|
||||
IRuntimeState runtimeState,
|
||||
IMainDom mainDom,
|
||||
IServerRoleAccessor serverRegistrar,
|
||||
IContentService contentService,
|
||||
IUmbracoContextFactory umbracoContextFactory,
|
||||
ILogger<ScheduledPublishing> logger,
|
||||
IServerMessenger serverMessenger,
|
||||
IScopeProvider scopeProvider)
|
||||
: base(TimeSpan.FromMinutes(1), DefaultDelay)
|
||||
{
|
||||
_runtimeState = runtimeState;
|
||||
@@ -58,6 +80,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
|
||||
_umbracoContextFactory = umbracoContextFactory;
|
||||
_logger = logger;
|
||||
_serverMessenger = serverMessenger;
|
||||
_scopeProvider = scopeProvider;
|
||||
}
|
||||
|
||||
public override Task PerformExecuteAsync(object state)
|
||||
@@ -93,8 +116,6 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
|
||||
|
||||
try
|
||||
{
|
||||
// We don't need an explicit scope here because PerformScheduledPublish creates it's own scope
|
||||
// so it's safe as it will create it's own ambient scope.
|
||||
// Ensure we run with an UmbracoContext, because this will run in a background task,
|
||||
// and developers may be using the UmbracoContext in the event handlers.
|
||||
|
||||
@@ -105,6 +126,14 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
|
||||
// - and we should definitively *not* have to flush it here (should be auto)
|
||||
|
||||
using UmbracoContextReference contextReference = _umbracoContextFactory.EnsureUmbracoContext();
|
||||
using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
|
||||
|
||||
/* We used to assume that there will never be two instances running concurrently where (IsMainDom && ServerRole == SchedulingPublisher)
|
||||
* However this is possible during an azure deployment slot swap for the SchedulingPublisher instance when trying to achieve zero downtime deployments.
|
||||
* If we take a distributed write lock, we are certain that the multiple instances of the job will not run in parallel.
|
||||
* 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
|
||||
* only until the old SchedulingPublisher shuts down. */
|
||||
scope.EagerWriteLock(Constants.Locks.ScheduledPublishing);
|
||||
try
|
||||
{
|
||||
// Run
|
||||
|
||||
@@ -175,6 +175,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
|
||||
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.Domains, Name = "Domains" });
|
||||
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.KeyValues, Name = "KeyValues" });
|
||||
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.Languages, Name = "Languages" });
|
||||
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.ScheduledPublishing, Name = "ScheduledPublishing" });
|
||||
|
||||
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.MainDom, Name = "MainDom" });
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_1_0;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_2_0;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade
|
||||
@@ -280,6 +281,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade
|
||||
To<UpdateExternalLoginToUseKeyInsteadOfId>("{CA7A1D9D-C9D4-4914-BC0A-459E7B9C3C8C}");
|
||||
To<AddTwoFactorLoginTable>("{0828F206-DCF7-4F73-ABBB-6792275532EB}");
|
||||
|
||||
// TO 9.4.0
|
||||
To<AddScheduledPublishingLock>("{DBBA1EA0-25A1-4863-90FB-5D306FB6F1E1}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0
|
||||
{
|
||||
internal class AddScheduledPublishingLock : MigrationBase
|
||||
{
|
||||
public AddScheduledPublishingLock(IMigrationContext context)
|
||||
: base(context)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Migrate() =>
|
||||
Database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.ScheduledPublishing, Name = "ScheduledPublishing" });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Hosting;
|
||||
using Umbraco.Cms.Core.Runtime;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Runtime
|
||||
{
|
||||
|
||||
internal class DefaultMainDomKeyGenerator : IMainDomKeyGenerator
|
||||
{
|
||||
private readonly IHostingEnvironment _hostingEnvironment;
|
||||
private readonly IOptionsMonitor<GlobalSettings> _globalSettings;
|
||||
|
||||
public DefaultMainDomKeyGenerator(IHostingEnvironment hostingEnvironment, IOptionsMonitor<GlobalSettings> globalSettings)
|
||||
{
|
||||
_hostingEnvironment = hostingEnvironment;
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
public string GenerateKey()
|
||||
{
|
||||
var machineName = Environment.MachineName;
|
||||
var mainDomId = MainDom.GetMainDomId(_hostingEnvironment);
|
||||
var discriminator = _globalSettings.CurrentValue.MainDomKeyDiscriminator;
|
||||
|
||||
var rawKey = $"{machineName}{mainDomId}{discriminator}";
|
||||
|
||||
return rawKey.GenerateHash<SHA1>();
|
||||
}
|
||||
}
|
||||
}
|
||||
131
src/Umbraco.Infrastructure/Runtime/FileSystemMainDomLock.cs
Normal file
131
src/Umbraco.Infrastructure/Runtime/FileSystemMainDomLock.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Hosting;
|
||||
using Umbraco.Cms.Core.Runtime;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Runtime
|
||||
{
|
||||
internal class FileSystemMainDomLock : IMainDomLock
|
||||
{
|
||||
private readonly ILogger<FileSystemMainDomLock> _log;
|
||||
|
||||
private readonly CancellationTokenSource _cancellationTokenSource = new();
|
||||
|
||||
private readonly string _lockFilePath;
|
||||
private readonly string _releaseSignalFilePath;
|
||||
|
||||
private FileStream _lockFileStream;
|
||||
|
||||
public FileSystemMainDomLock(
|
||||
ILogger<FileSystemMainDomLock> log,
|
||||
IMainDomKeyGenerator mainDomKeyGenerator,
|
||||
IHostingEnvironment hostingEnvironment)
|
||||
{
|
||||
_log = log;
|
||||
|
||||
var lockFileName = $"MainDom_{mainDomKeyGenerator.GenerateKey()}.lock";
|
||||
_lockFilePath = Path.Combine(hostingEnvironment.LocalTempPath, lockFileName);
|
||||
_releaseSignalFilePath = $"{_lockFilePath}_release";
|
||||
}
|
||||
|
||||
public Task<bool> AcquireLockAsync(int millisecondsTimeout)
|
||||
{
|
||||
var stopwatch = new Stopwatch();
|
||||
stopwatch.Start();
|
||||
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
_log.LogDebug("Attempting to obtain MainDom lock file handle {lockFilePath}", _lockFilePath);
|
||||
_lockFileStream = File.Open(_lockFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
|
||||
DeleteLockReleaseFile();
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
_log.LogDebug("Couldn't obtain MainDom lock file handle, signalling for release of {lockFilePath}", _lockFilePath);
|
||||
CreateLockReleaseFile();
|
||||
Thread.Sleep(500);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogError(ex, "Unexpected exception attempting to obtain MainDom lock file handle {lockFilePath}, giving up", _lockFilePath);
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
while (stopwatch.ElapsedMilliseconds < millisecondsTimeout);
|
||||
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
// Create a long running task to poll to check if anyone has created a lock release file.
|
||||
public Task ListenAsync() =>
|
||||
Task.Factory.StartNew(
|
||||
ListeningLoop,
|
||||
_cancellationTokenSource.Token,
|
||||
TaskCreationOptions.LongRunning,
|
||||
TaskScheduler.Default);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_lockFileStream?.Close();
|
||||
_lockFileStream = null;
|
||||
}
|
||||
|
||||
private void CreateLockReleaseFile()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Dispose immediately to release the file handle so it's easier to cleanup in any process.
|
||||
using FileStream releaseFileStream = File.Open(_releaseSignalFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogError(ex, "Unexpected exception attempting to create lock release signal file {file}", _releaseSignalFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteLockReleaseFile()
|
||||
{
|
||||
while (File.Exists(_releaseSignalFilePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(_releaseSignalFilePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogError(ex, "Unexpected exception attempting to delete release signal file {file}", _releaseSignalFilePath);
|
||||
Thread.Sleep(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ListeningLoop()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (_cancellationTokenSource.IsCancellationRequested)
|
||||
{
|
||||
_log.LogDebug("ListenAsync Task canceled, exiting loop");
|
||||
return;
|
||||
}
|
||||
|
||||
if (File.Exists(_releaseSignalFilePath))
|
||||
{
|
||||
_log.LogDebug("Found lock release signal file, releasing lock on {lockFilePath}", _lockFilePath);
|
||||
_lockFileStream?.Close();
|
||||
_lockFileStream = null;
|
||||
break;
|
||||
}
|
||||
|
||||
Thread.Sleep(2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NPoco;
|
||||
@@ -18,6 +19,7 @@ using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Mappers;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax;
|
||||
using Umbraco.Cms.Web.Common.DependencyInjection;
|
||||
using Umbraco.Extensions;
|
||||
using MapperCollection = Umbraco.Cms.Infrastructure.Persistence.Mappers.MapperCollection;
|
||||
|
||||
@@ -30,7 +32,6 @@ namespace Umbraco.Cms.Infrastructure.Runtime
|
||||
private const string UpdatedSuffix = "_updated";
|
||||
private readonly ILogger<SqlMainDomLock> _logger;
|
||||
private readonly IOptions<GlobalSettings> _globalSettings;
|
||||
private readonly IHostingEnvironment _hostingEnvironment;
|
||||
private readonly IUmbracoDatabase _db;
|
||||
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||
private SqlServerSyntaxProvider _sqlServerSyntax;
|
||||
@@ -41,6 +42,9 @@ namespace Umbraco.Cms.Infrastructure.Runtime
|
||||
private bool _hasTable = false;
|
||||
private bool _acquireWhenTablesNotAvailable = false;
|
||||
|
||||
// Note: Ignoring the two version notice rule as this class should probably be internal.
|
||||
// We don't expect anyone downstream to be instantiating a SqlMainDomLock, only resolving IMainDomLock
|
||||
[Obsolete("This constructor will be removed in version 10, please use an alternative constructor.")]
|
||||
public SqlMainDomLock(
|
||||
ILogger<SqlMainDomLock> logger,
|
||||
ILoggerFactory loggerFactory,
|
||||
@@ -51,25 +55,20 @@ namespace Umbraco.Cms.Infrastructure.Runtime
|
||||
DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory,
|
||||
NPocoMapperCollection npocoMappers,
|
||||
string connectionStringName)
|
||||
{
|
||||
// unique id for our appdomain, this is more unique than the appdomain id which is just an INT counter to its safer
|
||||
_lockId = Guid.NewGuid().ToString();
|
||||
_logger = logger;
|
||||
_globalSettings = globalSettings;
|
||||
_sqlServerSyntax = new SqlServerSyntaxProvider(_globalSettings);
|
||||
_hostingEnvironment = hostingEnvironment;
|
||||
_dbFactory = new UmbracoDatabaseFactory(
|
||||
loggerFactory.CreateLogger<UmbracoDatabaseFactory>(),
|
||||
: this(
|
||||
loggerFactory,
|
||||
_globalSettings,
|
||||
new MapperCollection(() => Enumerable.Empty<BaseMapper>()),
|
||||
globalSettings,
|
||||
connectionStrings,
|
||||
dbProviderFactoryCreator,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IMainDomKeyGenerator>(),
|
||||
databaseSchemaCreatorFactory,
|
||||
npocoMappers,
|
||||
connectionStringName);
|
||||
MainDomKey = MainDomKeyPrefix + "-" + (Environment.MachineName + MainDom.GetMainDomId(_hostingEnvironment)).GenerateHash<SHA1>();
|
||||
npocoMappers)
|
||||
{
|
||||
}
|
||||
|
||||
// Note: Ignoring the two version notice rule as this class should probably be internal.
|
||||
// We don't expect anyone downstream to be instantiating a SqlMainDomLock, only resolving IMainDomLock
|
||||
[Obsolete("This constructor will be removed in version 10, please use an alternative constructor.")]
|
||||
public SqlMainDomLock(
|
||||
ILogger<SqlMainDomLock> logger,
|
||||
ILoggerFactory loggerFactory,
|
||||
@@ -80,18 +79,42 @@ namespace Umbraco.Cms.Infrastructure.Runtime
|
||||
DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory,
|
||||
NPocoMapperCollection npocoMappers)
|
||||
: this(
|
||||
logger,
|
||||
loggerFactory,
|
||||
globalSettings,
|
||||
connectionStrings,
|
||||
dbProviderFactoryCreator,
|
||||
hostingEnvironment,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IMainDomKeyGenerator>(),
|
||||
databaseSchemaCreatorFactory,
|
||||
npocoMappers,
|
||||
connectionStrings.CurrentValue.UmbracoConnectionString.ConnectionString
|
||||
)
|
||||
npocoMappers)
|
||||
{
|
||||
}
|
||||
|
||||
public SqlMainDomLock(
|
||||
ILoggerFactory loggerFactory,
|
||||
IOptions<GlobalSettings> globalSettings,
|
||||
IOptionsMonitor<ConnectionStrings> connectionStrings,
|
||||
IDbProviderFactoryCreator dbProviderFactoryCreator,
|
||||
IMainDomKeyGenerator mainDomKeyGenerator,
|
||||
DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory,
|
||||
NPocoMapperCollection npocoMappers)
|
||||
{
|
||||
// unique id for our appdomain, this is more unique than the appdomain id which is just an INT counter to its safer
|
||||
_lockId = Guid.NewGuid().ToString();
|
||||
_logger = loggerFactory.CreateLogger<SqlMainDomLock>();
|
||||
_globalSettings = globalSettings;
|
||||
_sqlServerSyntax = new SqlServerSyntaxProvider(_globalSettings);
|
||||
|
||||
_dbFactory = new UmbracoDatabaseFactory(
|
||||
loggerFactory.CreateLogger<UmbracoDatabaseFactory>(),
|
||||
loggerFactory,
|
||||
_globalSettings,
|
||||
new MapperCollection(() => Enumerable.Empty<BaseMapper>()),
|
||||
dbProviderFactoryCreator,
|
||||
databaseSchemaCreatorFactory,
|
||||
npocoMappers,
|
||||
connectionStrings.CurrentValue.UmbracoConnectionString.ConnectionString);
|
||||
|
||||
MainDomKey = MainDomKeyPrefix + "-" + mainDomKeyGenerator.GenerateKey();
|
||||
}
|
||||
|
||||
public async Task<bool> AcquireLockAsync(int millisecondsTimeout)
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Hosting;
|
||||
using Umbraco.Cms.Core.Runtime;
|
||||
using Umbraco.Cms.Infrastructure.Runtime;
|
||||
using Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Runtime
|
||||
{
|
||||
[TestFixture]
|
||||
internal class FileSystemMainDomLockTests : UmbracoIntegrationTest
|
||||
{
|
||||
private IMainDomKeyGenerator MainDomKeyGenerator { get; set; }
|
||||
|
||||
private IHostingEnvironment HostingEnvironment { get; set; }
|
||||
|
||||
private FileSystemMainDomLock FileSystemMainDomLock { get; set; }
|
||||
|
||||
private string LockFilePath { get; set; }
|
||||
private string LockReleaseFilePath { get; set; }
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
MainDomKeyGenerator = GetRequiredService<IMainDomKeyGenerator>();
|
||||
HostingEnvironment = GetRequiredService<IHostingEnvironment>();
|
||||
|
||||
var lockFileName = $"MainDom_{MainDomKeyGenerator.GenerateKey()}.lock";
|
||||
LockFilePath = Path.Combine(HostingEnvironment.LocalTempPath, lockFileName);
|
||||
LockReleaseFilePath = LockFilePath + "_release";
|
||||
|
||||
var log = GetRequiredService<ILogger<FileSystemMainDomLock>>();
|
||||
FileSystemMainDomLock = new FileSystemMainDomLock(log, MainDomKeyGenerator, HostingEnvironment);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
while (File.Exists(LockFilePath))
|
||||
{
|
||||
File.Delete(LockFilePath);
|
||||
}
|
||||
while (File.Exists(LockReleaseFilePath))
|
||||
{
|
||||
File.Delete(LockReleaseFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AcquireLockAsync_WhenNoOtherHoldsLockFileHandle_ReturnsTrue()
|
||||
{
|
||||
using var sut = FileSystemMainDomLock;
|
||||
|
||||
var result = await sut.AcquireLockAsync(1000);
|
||||
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AcquireLockAsync_WhenTimeoutExceeded_ReturnsFalse()
|
||||
{
|
||||
await using var lockFile = File.Open(LockFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
|
||||
|
||||
using var sut = FileSystemMainDomLock;
|
||||
|
||||
var result = await sut.AcquireLockAsync(1000);
|
||||
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ListenAsync_WhenLockReleaseSignalFileFound_DropsLockFileHandle()
|
||||
{
|
||||
using var sut = FileSystemMainDomLock;
|
||||
|
||||
await sut.AcquireLockAsync(1000);
|
||||
|
||||
var before = await sut.AcquireLockAsync(1000);
|
||||
|
||||
await using (_ = File.Open(LockReleaseFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite))
|
||||
{
|
||||
}
|
||||
|
||||
await sut.ListenAsync();
|
||||
|
||||
var after = await sut.AcquireLockAsync(1000);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.False(before);
|
||||
Assert.True(after);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,15 @@
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Runtime;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Sync;
|
||||
@@ -108,6 +111,11 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.HostedServices
|
||||
|
||||
var mockServerMessenger = new Mock<IServerMessenger>();
|
||||
|
||||
var mockScopeProvider = new Mock<IScopeProvider>();
|
||||
mockScopeProvider
|
||||
.Setup(x => x.CreateScope(It.IsAny<IsolationLevel>(), It.IsAny<RepositoryCacheMode>(), It.IsAny<IEventDispatcher>(), It.IsAny<IScopedNotificationPublisher>(), It.IsAny<bool?>(), It.IsAny<bool>(), It.IsAny<bool>()))
|
||||
.Returns(Mock.Of<IScope>());
|
||||
|
||||
return new ScheduledPublishing(
|
||||
mockRunTimeState.Object,
|
||||
mockMainDom.Object,
|
||||
@@ -115,7 +123,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.HostedServices
|
||||
_mockContentService.Object,
|
||||
mockUmbracoContextFactory.Object,
|
||||
_mockLogger.Object,
|
||||
mockServerMessenger.Object);
|
||||
mockServerMessenger.Object,
|
||||
mockScopeProvider.Object);
|
||||
}
|
||||
|
||||
private void VerifyScheduledPublishingNotPerformed() => VerifyScheduledPublishingPerformed(Times.Never());
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using AutoFixture.NUnit3;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Hosting;
|
||||
using Umbraco.Cms.Infrastructure.Runtime;
|
||||
using Umbraco.Cms.Tests.UnitTests.AutoFixture;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Runtime
|
||||
{
|
||||
[TestFixture]
|
||||
internal class DefaultMainDomKeyGeneratorTests
|
||||
{
|
||||
[Test]
|
||||
[AutoMoqData]
|
||||
public void GenerateKey_WithConfiguredDiscriminatorValue_AltersHash(
|
||||
[Frozen] IHostingEnvironment hostingEnvironment,
|
||||
[Frozen] GlobalSettings globalSettings,
|
||||
[Frozen] IOptionsMonitor<GlobalSettings> globalSettingsMonitor,
|
||||
DefaultMainDomKeyGenerator sut,
|
||||
string aDiscriminator)
|
||||
{
|
||||
var withoutDiscriminator = sut.GenerateKey();
|
||||
globalSettings.MainDomKeyDiscriminator = aDiscriminator;
|
||||
var withDiscriminator = sut.GenerateKey();
|
||||
|
||||
Assert.AreNotEqual(withoutDiscriminator, withDiscriminator);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[AutoMoqData]
|
||||
public void GenerateKey_WithUnchangedDiscriminatorValue_ReturnsSameValue(
|
||||
[Frozen] IHostingEnvironment hostingEnvironment,
|
||||
[Frozen] GlobalSettings globalSettings,
|
||||
[Frozen] IOptionsMonitor<GlobalSettings> globalSettingsMonitor,
|
||||
DefaultMainDomKeyGenerator sut,
|
||||
string aDiscriminator)
|
||||
{
|
||||
globalSettings.MainDomKeyDiscriminator = aDiscriminator;
|
||||
|
||||
var a = sut.GenerateKey();
|
||||
var b = sut.GenerateKey();
|
||||
|
||||
Assert.AreEqual(a, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user