diff --git a/src/Umbraco.Infrastructure/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs similarity index 66% rename from src/Umbraco.Infrastructure/Scheduling/ScheduledPublishing.cs rename to src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs index 81dd8f92af..9c32a80fe2 100644 --- a/src/Umbraco.Infrastructure/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs @@ -1,13 +1,20 @@ using System; using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using Umbraco.Core; using Umbraco.Core.Services; using Umbraco.Core.Sync; -using Microsoft.Extensions.Logging; +using Umbraco.Web; -namespace Umbraco.Web.Scheduling +namespace Umbraco.Infrastructure.HostedServices { - public class ScheduledPublishing : RecurringTaskBase + /// + /// Hosted service implementation for scheduled publishing feature. + /// + /// + /// Runs only on non-replica servers. + public class ScheduledPublishing : RecurringHostedServiceBase { private readonly IContentService _contentService; private readonly ILogger _logger; @@ -18,11 +25,10 @@ namespace Umbraco.Web.Scheduling private readonly IServerRegistrar _serverRegistrar; private readonly IUmbracoContextFactory _umbracoContextFactory; - public ScheduledPublishing(IBackgroundTaskRunner runner, int delayMilliseconds, - int periodMilliseconds, + public ScheduledPublishing( IRuntimeState runtime, IMainDom mainDom, IServerRegistrar serverRegistrar, IContentService contentService, IUmbracoContextFactory umbracoContextFactory, ILogger logger, IServerMessenger serverMessenger, IBackofficeSecurityFactory backofficeSecurityFactory) - : base(runner, delayMilliseconds, periodMilliseconds) + : base(TimeSpan.FromMinutes(1), DefaultDelay) { _runtime = runtime; _mainDom = mainDom; @@ -34,35 +40,35 @@ namespace Umbraco.Web.Scheduling _backofficeSecurityFactory = backofficeSecurityFactory; } - public override bool IsAsync => false; - - public override bool PerformRun() + internal override async Task PerformExecuteAsync(object state) { if (Suspendable.ScheduledPublishing.CanRun == false) - return true; // repeat, later + { + return; + } switch (_serverRegistrar.GetCurrentServerRole()) { case ServerRole.Replica: _logger.LogDebug("Does not run on replica servers."); - return true; // DO repeat, server role can change + return; case ServerRole.Unknown: _logger.LogDebug("Does not run on servers with unknown role."); - return true; // DO repeat, server role can change + return; } - // ensure we do not run if not main domain, but do NOT lock it + // Ensure we do not run if not main domain, but do NOT lock it if (_mainDom.IsMainDom == false) { _logger.LogDebug("Does not run if not MainDom."); - return false; // do NOT repeat, going down + return; } - // do NOT run publishing if not properly running + // Do NOT run publishing if not properly running if (_runtime.Level != RuntimeLevel.Run) { _logger.LogDebug("Does not run if run level is not Run."); - return true; // repeat/wait + return; } try @@ -79,22 +85,24 @@ namespace Umbraco.Web.Scheduling // - and we should definitively *not* have to flush it here (should be auto) // _backofficeSecurityFactory.EnsureBackofficeSecurity(); - using (var contextReference = _umbracoContextFactory.EnsureUmbracoContext()) + using var contextReference = _umbracoContextFactory.EnsureUmbracoContext(); + try { - try + // Run + var result = _contentService.PerformScheduledPublish(DateTime.Now); + foreach (var grouped in result.GroupBy(x => x.Result)) { - // run - var result = _contentService.PerformScheduledPublish(DateTime.Now); - foreach (var grouped in result.GroupBy(x => x.Result)) - _logger.LogInformation( - "Scheduled publishing result: '{StatusCount}' items with status {Status}", - grouped.Count(), grouped.Key); + _logger.LogInformation( + "Scheduled publishing result: '{StatusCount}' items with status {Status}", + grouped.Count(), grouped.Key); } - finally + } + finally + { + // If running on a temp context, we have to flush the messenger + if (contextReference.IsRoot && _serverMessenger is IBatchedDatabaseServerMessenger m) { - // if running on a temp context, we have to flush the messenger - if (contextReference.IsRoot && _serverMessenger is IBatchedDatabaseServerMessenger m) - m.FlushBatch(); + m.FlushBatch(); } } } @@ -104,7 +112,7 @@ namespace Umbraco.Web.Scheduling _logger.LogError(ex, "Failed."); } - return true; // repeat + return; } } } diff --git a/src/Umbraco.Infrastructure/Scheduling/SchedulerComponent.cs b/src/Umbraco.Infrastructure/Scheduling/SchedulerComponent.cs deleted file mode 100644 index 6346673697..0000000000 --- a/src/Umbraco.Infrastructure/Scheduling/SchedulerComponent.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using Microsoft.Extensions.Logging; -using Umbraco.Core; -using Umbraco.Core.Composing; -using Umbraco.Core.Hosting; -using Umbraco.Core.Services; -using Umbraco.Core.Sync; -using Umbraco.Web.Routing; - -namespace Umbraco.Web.Scheduling -{ - public sealed class SchedulerComponent : IComponent - { - private const int DefaultDelayMilliseconds = 180000; // 3 mins - private const int OneMinuteMilliseconds = 60000; - - private readonly IRuntimeState _runtime; - private readonly IMainDom _mainDom; - private readonly IServerRegistrar _serverRegistrar; - private readonly IContentService _contentService; - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - private readonly IApplicationShutdownRegistry _applicationShutdownRegistry; - private readonly IUmbracoContextFactory _umbracoContextFactory; - private readonly IServerMessenger _serverMessenger; - private readonly IRequestAccessor _requestAccessor; - private readonly IBackofficeSecurityFactory _backofficeSecurityFactory; - - private BackgroundTaskRunner _publishingRunner; - - private bool _started; - private object _locker = new object(); - private IBackgroundTask[] _tasks; - - public SchedulerComponent(IRuntimeState runtime, IMainDom mainDom, IServerRegistrar serverRegistrar, - IContentService contentService, IUmbracoContextFactory umbracoContextFactory, ILoggerFactory loggerFactory, - IApplicationShutdownRegistry applicationShutdownRegistry, - IServerMessenger serverMessenger, IRequestAccessor requestAccessor, - IBackofficeSecurityFactory backofficeSecurityFactory) - { - _runtime = runtime; - _mainDom = mainDom; - _serverRegistrar = serverRegistrar; - _contentService = contentService; - _loggerFactory = loggerFactory; - _logger = loggerFactory.CreateLogger(); - _applicationShutdownRegistry = applicationShutdownRegistry; - _umbracoContextFactory = umbracoContextFactory; - _serverMessenger = serverMessenger; - _requestAccessor = requestAccessor; - _backofficeSecurityFactory = backofficeSecurityFactory; - } - - public void Initialize() - { - var logger = _loggerFactory.CreateLogger>(); - // backgrounds runners are web aware, if the app domain dies, these tasks will wind down correctly - _publishingRunner = new BackgroundTaskRunner("ScheduledPublishing", logger, _applicationShutdownRegistry); - - // we will start the whole process when a successful request is made - _requestAccessor.RouteAttempt += RegisterBackgroundTasksOnce; - } - - public void Terminate() - { - // the AppDomain / maindom / whatever takes care of stopping background task runners - } - - private void RegisterBackgroundTasksOnce(object sender, RoutableAttemptEventArgs e) - { - switch (e.Outcome) - { - case EnsureRoutableOutcome.IsRoutable: - case EnsureRoutableOutcome.NotDocumentRequest: - _requestAccessor.RouteAttempt -= RegisterBackgroundTasksOnce; - RegisterBackgroundTasks(); - break; - } - } - - private void RegisterBackgroundTasks() - { - LazyInitializer.EnsureInitialized(ref _tasks, ref _started, ref _locker, () => - { - _logger.LogDebug("Initializing the scheduler"); - - var tasks = new List(); - - tasks.Add(RegisterScheduledPublishing()); - - return tasks.ToArray(); - }); - } - - private IBackgroundTask RegisterScheduledPublishing() - { - // scheduled publishing/unpublishing - // install on all, will only run on non-replica servers - var task = new ScheduledPublishing(_publishingRunner, DefaultDelayMilliseconds, OneMinuteMilliseconds, _runtime, _mainDom, _serverRegistrar, _contentService, _umbracoContextFactory, _loggerFactory.CreateLogger(), _serverMessenger, _backofficeSecurityFactory); - _publishingRunner.TryAdd(task); - return task; - } - } -} diff --git a/src/Umbraco.Infrastructure/Scheduling/SchedulerComposer.cs b/src/Umbraco.Infrastructure/Scheduling/SchedulerComposer.cs deleted file mode 100644 index 5c56f3d314..0000000000 --- a/src/Umbraco.Infrastructure/Scheduling/SchedulerComposer.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using Umbraco.Core; -using Umbraco.Core.Composing; - -namespace Umbraco.Web.Scheduling -{ - /// - /// Used to do the scheduling for tasks, publishing, etc... - /// - /// - /// All tasks are run in a background task runner which is web aware and will wind down - /// the task correctly instead of killing it completely when the app domain shuts down. - /// - [RuntimeLevel(MinLevel = RuntimeLevel.Run)] - internal sealed class SchedulerComposer : ComponentComposer, ICoreComposer - { } -} diff --git a/src/Umbraco.Tests.Integration/Testing/IntegrationTestComposer.cs b/src/Umbraco.Tests.Integration/Testing/IntegrationTestComposer.cs index 1fc086c019..f64c2c48b1 100644 --- a/src/Umbraco.Tests.Integration/Testing/IntegrationTestComposer.cs +++ b/src/Umbraco.Tests.Integration/Testing/IntegrationTestComposer.cs @@ -42,7 +42,6 @@ namespace Umbraco.Tests.Integration.Testing { base.Compose(composition); - composition.Components().Remove(); composition.Components().Remove(); composition.Services.AddUnique(); composition.Services.AddUnique(factory => Mock.Of()); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ScheduledPublishingTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ScheduledPublishingTests.cs new file mode 100644 index 0000000000..e0fd2a4acc --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ScheduledPublishingTests.cs @@ -0,0 +1,124 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Services; +using Umbraco.Core.Sync; +using Umbraco.Infrastructure.HostedServices; +using Umbraco.Web; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HostedServices +{ + [TestFixture] + public class ScheduledPublishingTests + { + private Mock _mockContentService; + private Mock> _mockLogger; + + [Test] + public async Task Does_Not_Execute_When_Not_Enabled() + { + var sut = CreateScheduledPublishing(enabled: false); + await sut.PerformExecuteAsync(null); + VerifyScheduledPublishingNotPerformed(); + } + + [Test] + public async Task Does_Not_Execute_When_Runtime_State_Is_Not_Run() + { + var sut = CreateScheduledPublishing(runtimeLevel: RuntimeLevel.Boot); + await sut.PerformExecuteAsync(null); + VerifyScheduledPublishingNotPerformed(); + } + + [Test] + public async Task Does_Not_Execute_When_Server_Role_Is_Replica() + { + var sut = CreateScheduledPublishing(serverRole: ServerRole.Replica); + await sut.PerformExecuteAsync(null); + VerifyScheduledPublishingNotPerformed(); + } + + [Test] + public async Task Does_Not_Execute_When_Server_Role_Is_Unknown() + { + var sut = CreateScheduledPublishing(serverRole: ServerRole.Unknown); + await sut.PerformExecuteAsync(null); + VerifyScheduledPublishingNotPerformed(); + } + + [Test] + public async Task Does_Not_Execute_When_Not_Main_Dom() + { + var sut = CreateScheduledPublishing(isMainDom: false); + await sut.PerformExecuteAsync(null); + VerifyScheduledPublishingNotPerformed(); + } + + [Test] + public async Task Executes_And_Performs_Scheduled_Publishing() + { + var sut = CreateScheduledPublishing(); + await sut.PerformExecuteAsync(null); + VerifyScheduledPublishingPerformed(); + } + + private ScheduledPublishing CreateScheduledPublishing( + bool enabled = true, + RuntimeLevel runtimeLevel = RuntimeLevel.Run, + ServerRole serverRole = ServerRole.Single, + bool isMainDom = true) + { + if (enabled) + { + Suspendable.ScheduledPublishing.Resume(); + } + else + { + Suspendable.ScheduledPublishing.Suspend(); + } + + var mockRunTimeState = new Mock(); + mockRunTimeState.SetupGet(x => x.Level).Returns(runtimeLevel); + + var mockServerRegistrar = new Mock(); + mockServerRegistrar.Setup(x => x.GetCurrentServerRole()).Returns(serverRole); + + var mockMainDom = new Mock(); + mockMainDom.SetupGet(x => x.IsMainDom).Returns(isMainDom); + + _mockContentService = new Mock(); + + var mockUmbracoContextFactory = new Mock(); + mockUmbracoContextFactory.Setup(x => x.EnsureUmbracoContext()).Returns(new UmbracoContextReference(null, false, null)); + + _mockLogger = new Mock>(); + + var mockServerMessenger = new Mock(); + + var mockBackOfficeSecurityFactory = new Mock(); + + return new ScheduledPublishing(mockRunTimeState.Object, mockMainDom.Object, mockServerRegistrar.Object, _mockContentService.Object, + mockUmbracoContextFactory.Object, _mockLogger.Object, mockServerMessenger.Object, mockBackOfficeSecurityFactory.Object); + } + + private void VerifyScheduledPublishingNotPerformed() + { + VerifyScheduledPublishingPerformed(Times.Never()); + } + + private void VerifyScheduledPublishingPerformed() + { + VerifyScheduledPublishingPerformed(Times.Once()); + } + + private void VerifyScheduledPublishingPerformed(Times times) + { + _mockContentService.Verify(x => x.PerformScheduledPublish(It.IsAny()), times); + } + } +} diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs index 4b5b2ee866..80be943eb7 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs @@ -295,6 +295,7 @@ namespace Umbraco.Extensions services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); + services.AddHostedService(); services.AddHostedService(); return services;