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;