NetCore: Migrated database server registrar and messenger tasks into hosted services (#9353)

* Migrated database server registrar and messenger tasks into hosted services.
Moved DatabaseServerRegistrar Options into injectable configuration.

* Added further cases for unit tests on hosted services checking execution based on runtime level.

* Migrated DatabaseServerMessengerOptions into configuration.
This commit is contained in:
Andy Butland
2020-11-10 20:02:09 +01:00
committed by GitHub
parent 3f5f85880e
commit b4ce2873cc
21 changed files with 424 additions and 306 deletions

View File

@@ -1,18 +1,12 @@
using System;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Hosting;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Changes;
using Umbraco.Core.Sync;
using Umbraco.Examine;
using Umbraco.Web.Cache;
using Umbraco.Web.PublishedCache;
using Umbraco.Web.Routing;
using Umbraco.Web.Scheduling;
using Umbraco.Web.Search;
namespace Umbraco.Web.Compose
@@ -36,9 +30,9 @@ namespace Umbraco.Web.Compose
public sealed class DatabaseServerRegistrarAndMessengerComposer : ComponentComposer<DatabaseServerRegistrarAndMessengerComponent>, ICoreComposer
{
public static DatabaseServerMessengerOptions GetDefaultOptions(IServiceProvider factory)
public static DatabaseServerMessengerCallbacks GetCallbacks(IServiceProvider factory)
{
return new DatabaseServerMessengerOptions
return new DatabaseServerMessengerCallbacks
{
//These callbacks will be executed if the server has not been synced
// (i.e. it is a new server or the lastsynced.txt file has been removed)
@@ -74,208 +68,55 @@ namespace Umbraco.Web.Compose
{
base.Compose(composition);
composition.SetDatabaseServerMessengerOptions(GetDefaultOptions);
composition.SetDatabaseServerMessengerCallbacks(GetCallbacks);
composition.SetServerMessenger<BatchedDatabaseServerMessenger>();
}
}
public sealed class DatabaseServerRegistrarAndMessengerComponent : IComponent
{
private object _locker = new object();
private readonly DatabaseServerRegistrar _registrar;
private readonly IBatchedDatabaseServerMessenger _messenger;
private readonly ILogger<DatabaseServerRegistrarAndMessengerComponent> _logger;
private readonly ILoggerFactory _loggerFactory;
private readonly IServerRegistrationService _registrationService;
private readonly BackgroundTaskRunner<IBackgroundTask> _touchTaskRunner;
private readonly BackgroundTaskRunner<IBackgroundTask> _processTaskRunner;
private bool _started;
private IBackgroundTask[] _tasks;
private readonly IRequestAccessor _requestAccessor;
public DatabaseServerRegistrarAndMessengerComponent(
IServerRegistrar serverRegistrar,
IServerMessenger serverMessenger,
IServerRegistrationService registrationService,
ILogger<DatabaseServerRegistrarAndMessengerComponent> logger,
ILoggerFactory loggerFactory,
IApplicationShutdownRegistry hostingEnvironment,
IRequestAccessor requestAccessor)
{
_logger = logger;
_loggerFactory = loggerFactory;
_registrationService = registrationService;
_requestAccessor = requestAccessor;
// create task runner for DatabaseServerRegistrar
_registrar = serverRegistrar as DatabaseServerRegistrar;
if (_registrar != null)
{
_touchTaskRunner = new BackgroundTaskRunner<IBackgroundTask>("ServerRegistration",
new BackgroundTaskRunnerOptions { AutoStart = true }, _loggerFactory.CreateLogger<BackgroundTaskRunner<IBackgroundTask>>(), hostingEnvironment);
}
// create task runner for BatchedDatabaseServerMessenger
_messenger = serverMessenger as IBatchedDatabaseServerMessenger;
if (_messenger != null)
{
_processTaskRunner = new BackgroundTaskRunner<IBackgroundTask>("ServerInstProcess",
new BackgroundTaskRunnerOptions { AutoStart = true }, _loggerFactory.CreateLogger<BackgroundTaskRunner<IBackgroundTask>>(), hostingEnvironment);
}
}
public void Initialize()
{
//We will start the whole process when a successful request is made
if (_registrar != null || _messenger != null)
_requestAccessor.RouteAttempt += RegisterBackgroundTasksOnce;
// The scheduled tasks - TouchServerTask and InstructionProcessTask - run as .NET Core hosted services.
// The former (as well as other hosted services that run outside of an HTTP request context) depends on the application URL
// being available (via IRequestAccessor), which can only be retrieved within an HTTP request (unless it's explicitly configured).
// Hence we hook up a one-off task on an HTTP request to ensure this is retrieved, which caches the value and makes it available
// for the hosted services to use when the HTTP request is not available.
_requestAccessor.RouteAttempt += EnsureApplicationUrlOnce;
// must come last, as it references some _variables
// Must come last, as it references some _variables
_messenger?.Startup();
}
public void Terminate()
{ }
/// <summary>
/// Handle when a request is made
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks>
/// We require this because:
/// - ApplicationContext.UmbracoApplicationUrl is initialized by UmbracoModule in BeginRequest
/// - RegisterServer is called on _requestAccessor.RouteAttempt which is triggered in ProcessRequest
/// we are safe, UmbracoApplicationUrl has been initialized
/// </remarks>
private void RegisterBackgroundTasksOnce(object sender, RoutableAttemptEventArgs e)
private void EnsureApplicationUrlOnce(object sender, RoutableAttemptEventArgs e)
{
switch (e.Outcome)
if (e.Outcome == EnsureRoutableOutcome.IsRoutable || e.Outcome == EnsureRoutableOutcome.NotDocumentRequest)
{
case EnsureRoutableOutcome.IsRoutable:
case EnsureRoutableOutcome.NotDocumentRequest:
_requestAccessor.RouteAttempt -= RegisterBackgroundTasksOnce;
RegisterBackgroundTasks();
break;
_requestAccessor.RouteAttempt -= EnsureApplicationUrlOnce;
EnsureApplicationUrl();
}
}
private void RegisterBackgroundTasks()
private void EnsureApplicationUrl()
{
// only perform this one time ever
LazyInitializer.EnsureInitialized(ref _tasks, ref _started, ref _locker, () =>
{
var serverAddress = _requestAccessor.GetApplicationUrl().ToString();
return new[]
{
RegisterInstructionProcess(),
RegisterTouchServer(_registrationService, serverAddress)
};
});
}
private IBackgroundTask RegisterInstructionProcess()
{
if (_messenger == null)
return null;
var task = new InstructionProcessTask(_processTaskRunner,
60000, //delay before first execution
_messenger.Options.ThrottleSeconds*1000, //amount of ms between executions
_messenger,
_logger);
_processTaskRunner.TryAdd(task);
return task;
}
private IBackgroundTask RegisterTouchServer(IServerRegistrationService registrationService, string serverAddress)
{
if (_registrar == null)
return null;
var task = new TouchServerTask(_touchTaskRunner,
15000, //delay before first execution
_registrar.Options.RecurringSeconds*1000, //amount of ms between executions
registrationService, _registrar, serverAddress, _logger);
_touchTaskRunner.TryAdd(task);
return task;
}
private class InstructionProcessTask : RecurringTaskBase
{
private readonly IDatabaseServerMessenger _messenger;
private readonly ILogger _logger;
public InstructionProcessTask(IBackgroundTaskRunner<RecurringTaskBase> runner, int delayMilliseconds, int periodMilliseconds,
IDatabaseServerMessenger messenger, ILogger logger)
: base(runner, delayMilliseconds, periodMilliseconds)
{
_messenger = messenger;
_logger = logger;
}
public override bool IsAsync => false;
/// <summary>
/// Runs the background task.
/// </summary>
/// <returns>A value indicating whether to repeat the task.</returns>
public override bool PerformRun()
{
try
{
_messenger.Sync();
}
catch (Exception e)
{
_logger.LogError(e, "Failed (will repeat).");
}
return true; // repeat
}
}
private class TouchServerTask : RecurringTaskBase
{
private readonly IServerRegistrationService _svc;
private readonly DatabaseServerRegistrar _registrar;
private readonly string _serverAddress;
private readonly ILogger _logger;
/// <summary>
/// Initializes a new instance of the <see cref="TouchServerTask"/> class.
/// </summary>
public TouchServerTask(IBackgroundTaskRunner<RecurringTaskBase> runner, int delayMilliseconds, int periodMilliseconds,
IServerRegistrationService svc, DatabaseServerRegistrar registrar, string serverAddress, ILogger logger)
: base(runner, delayMilliseconds, periodMilliseconds)
{
_svc = svc ?? throw new ArgumentNullException(nameof(svc));
_registrar = registrar;
_serverAddress = serverAddress;
_logger = logger;
}
public override bool IsAsync => false;
/// <summary>
/// Runs the background task.
/// </summary>
/// <returns>A value indicating whether to repeat the task.</returns>
public override bool PerformRun()
{
try
{
// TouchServer uses a proper unit of work etc underneath so even in a
// background task it is safe to call it without dealing with any scope
_svc.TouchServer(_serverAddress, _svc.CurrentServerIdentity, _registrar.Options.StaleServerTimeout);
return true; // repeat
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to update server record in database.");
return false; // probably stop if we have an error
}
}
// By retrieving the application URL within the context of a request (as we are here in responding
// to the IRequestAccessor's RouteAttempt event), we'll get it from the HTTP context and save it for
// future requests that may not be within an HTTP request (e.g. from hosted services).
_requestAccessor.GetApplicationUrl();
}
}
}