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:
Bjarke Berg
2020-11-03 07:33:58 +01:00
committed by GitHub
6 changed files with 162 additions and 152 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -295,6 +295,7 @@ namespace Umbraco.Extensions
services.AddHostedService<HealthCheckNotifier>();
services.AddHostedService<KeepAlive>();
services.AddHostedService<LogScrubber>();
services.AddHostedService<ScheduledPublishing>();
services.AddHostedService<TempFileCleanup>();
return services;