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:
Paul Johnson
2022-02-24 10:17:34 +00:00
committed by GitHub
parent dafd7f298d
commit 860c8e8ae2
15 changed files with 457 additions and 36 deletions

View File

@@ -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>

View File

@@ -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;
}
}
}

View 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();
}
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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" });
}

View File

@@ -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}");
}
}
}

View File

@@ -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" });
}
}

View File

@@ -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>();
}
}
}

View 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);
}
}
}
}

View File

@@ -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)

View File

@@ -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);
});
}
}
}

View File

@@ -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());

View File

@@ -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);
}
}
}