Merge pull request #9332 from AndyButland/feature/further-hosted-services-2
NetCore: Migrated scheduled publishing task to a hosted service.
This commit is contained in:
@@ -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
|
||||
/// <summary>
|
||||
/// Hosted service implementation for scheduled publishing feature.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Runs only on non-replica servers.</remarks>
|
||||
public class ScheduledPublishing : RecurringHostedServiceBase
|
||||
{
|
||||
private readonly IContentService _contentService;
|
||||
private readonly ILogger<ScheduledPublishing> _logger;
|
||||
@@ -18,11 +25,10 @@ namespace Umbraco.Web.Scheduling
|
||||
private readonly IServerRegistrar _serverRegistrar;
|
||||
private readonly IUmbracoContextFactory _umbracoContextFactory;
|
||||
|
||||
public ScheduledPublishing(IBackgroundTaskRunner<RecurringTaskBase> runner, int delayMilliseconds,
|
||||
int periodMilliseconds,
|
||||
public ScheduledPublishing(
|
||||
IRuntimeState runtime, IMainDom mainDom, IServerRegistrar serverRegistrar, IContentService contentService,
|
||||
IUmbracoContextFactory umbracoContextFactory, ILogger<ScheduledPublishing> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<SchedulerComponent> _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<IBackgroundTask> _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<SchedulerComponent>();
|
||||
_applicationShutdownRegistry = applicationShutdownRegistry;
|
||||
_umbracoContextFactory = umbracoContextFactory;
|
||||
_serverMessenger = serverMessenger;
|
||||
_requestAccessor = requestAccessor;
|
||||
_backofficeSecurityFactory = backofficeSecurityFactory;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
var logger = _loggerFactory.CreateLogger<BackgroundTaskRunner<IBackgroundTask>>();
|
||||
// backgrounds runners are web aware, if the app domain dies, these tasks will wind down correctly
|
||||
_publishingRunner = new BackgroundTaskRunner<IBackgroundTask>("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<IBackgroundTask>();
|
||||
|
||||
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<ScheduledPublishing>(), _serverMessenger, _backofficeSecurityFactory);
|
||||
_publishingRunner.TryAdd(task);
|
||||
return task;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using System;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Composing;
|
||||
|
||||
namespace Umbraco.Web.Scheduling
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to do the scheduling for tasks, publishing, etc...
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
|
||||
internal sealed class SchedulerComposer : ComponentComposer<SchedulerComponent>, ICoreComposer
|
||||
{ }
|
||||
}
|
||||
@@ -42,7 +42,6 @@ namespace Umbraco.Tests.Integration.Testing
|
||||
{
|
||||
base.Compose(composition);
|
||||
|
||||
composition.Components().Remove<SchedulerComponent>();
|
||||
composition.Components().Remove<DatabaseServerRegistrarAndMessengerComponent>();
|
||||
composition.Services.AddUnique<BackgroundIndexRebuilder, TestBackgroundIndexRebuilder>();
|
||||
composition.Services.AddUnique<IRuntimeMinifier>(factory => Mock.Of<IRuntimeMinifier>());
|
||||
|
||||
@@ -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<IContentService> _mockContentService;
|
||||
private Mock<ILogger<ScheduledPublishing>> _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<IRuntimeState>();
|
||||
mockRunTimeState.SetupGet(x => x.Level).Returns(runtimeLevel);
|
||||
|
||||
var mockServerRegistrar = new Mock<IServerRegistrar>();
|
||||
mockServerRegistrar.Setup(x => x.GetCurrentServerRole()).Returns(serverRole);
|
||||
|
||||
var mockMainDom = new Mock<IMainDom>();
|
||||
mockMainDom.SetupGet(x => x.IsMainDom).Returns(isMainDom);
|
||||
|
||||
_mockContentService = new Mock<IContentService>();
|
||||
|
||||
var mockUmbracoContextFactory = new Mock<IUmbracoContextFactory>();
|
||||
mockUmbracoContextFactory.Setup(x => x.EnsureUmbracoContext()).Returns(new UmbracoContextReference(null, false, null));
|
||||
|
||||
_mockLogger = new Mock<ILogger<ScheduledPublishing>>();
|
||||
|
||||
var mockServerMessenger = new Mock<IServerMessenger>();
|
||||
|
||||
var mockBackOfficeSecurityFactory = new Mock<IBackofficeSecurityFactory>();
|
||||
|
||||
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<DateTime>()), times);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,6 +295,7 @@ namespace Umbraco.Extensions
|
||||
services.AddHostedService<HealthCheckNotifier>();
|
||||
services.AddHostedService<KeepAlive>();
|
||||
services.AddHostedService<LogScrubber>();
|
||||
services.AddHostedService<ScheduledPublishing>();
|
||||
services.AddHostedService<TempFileCleanup>();
|
||||
|
||||
return services;
|
||||
|
||||
Reference in New Issue
Block a user