diff --git a/src/Umbraco.Core/Configuration/Models/DatabaseServerMessengerSettings.cs b/src/Umbraco.Core/Configuration/Models/DatabaseServerMessengerSettings.cs
new file mode 100644
index 0000000000..32b957d5d0
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/Models/DatabaseServerMessengerSettings.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace Umbraco.Core.Configuration.Models
+{
+ public class DatabaseServerMessengerSettings
+ {
+ ///
+ /// The maximum number of instructions that can be processed at startup; otherwise the server cold-boots (rebuilds its caches).
+ ///
+ public int MaxProcessingInstructionCount { get; set; } = 1000;
+
+ ///
+ /// The time to keep instructions in the database; records older than this number will be pruned.
+ ///
+ public TimeSpan TimeToRetainInstructions { get; set; } = TimeSpan.FromDays(2);
+
+ ///
+ /// The time to wait between each sync operations.
+ ///
+ public TimeSpan TimeBetweenSyncOperations { get; set; } = TimeSpan.FromSeconds(5);
+
+ ///
+ /// The time to wait between each prune operations.
+ ///
+ public TimeSpan TimeBetweenPruneOperations { get; set; } = TimeSpan.FromMinutes(1);
+ }
+}
diff --git a/src/Umbraco.Core/Configuration/Models/DatabaseServerRegistrarSettings.cs b/src/Umbraco.Core/Configuration/Models/DatabaseServerRegistrarSettings.cs
new file mode 100644
index 0000000000..1aa670fae6
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/Models/DatabaseServerRegistrarSettings.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace Umbraco.Core.Configuration.Models
+{
+ public class DatabaseServerRegistrarSettings
+ {
+ ///
+ /// The amount of time to wait between calls to the database on the background thread.
+ ///
+ public TimeSpan WaitTimeBetweenCalls { get; set; } = TimeSpan.FromMinutes(1);
+
+ ///
+ /// The time span to wait before considering a server stale, after it has last been accessed.
+ ///
+ public TimeSpan StaleServerTimeout { get; set; } = TimeSpan.FromMinutes(2);
+ }
+}
diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs
index 37d0f4a8a2..0a566cd7fe 100644
--- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs
@@ -46,6 +46,10 @@
public bool DisableElectionForSingleServer { get; set; } = false;
+ public DatabaseServerRegistrarSettings DatabaseServerRegistrar { get; set; } = new DatabaseServerRegistrarSettings();
+
+ public DatabaseServerMessengerSettings DatabaseServerMessenger { get; set; } = new DatabaseServerMessengerSettings();
+
public string RegisterType { get; set; } = string.Empty;
public string DatabaseFactoryServerVersion { get; set; } = string.Empty;
diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessengerCallbacks.cs b/src/Umbraco.Core/Sync/DatabaseServerMessengerCallbacks.cs
new file mode 100644
index 0000000000..7438762295
--- /dev/null
+++ b/src/Umbraco.Core/Sync/DatabaseServerMessengerCallbacks.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+
+namespace Umbraco.Core.Sync
+{
+ ///
+ /// Holds a list of callbacks associated with implementations of .
+ ///
+ public class DatabaseServerMessengerCallbacks
+ {
+ ///
+ /// A list of callbacks that will be invoked if the lastsynced.txt file does not exist.
+ ///
+ ///
+ /// These callbacks will typically be for e.g. rebuilding the xml cache file, or examine indexes, based on
+ /// the data in the database to get this particular server node up to date.
+ ///
+ public IEnumerable InitializingCallbacks { get; set; }
+ }
+}
diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs b/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs
deleted file mode 100644
index 6bfd6bff4a..0000000000
--- a/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-namespace Umbraco.Core.Sync
-{
- ///
- /// Provides options to the .
- ///
- public class DatabaseServerMessengerOptions
- {
- ///
- /// Initializes a new instance of the with default values.
- ///
- public DatabaseServerMessengerOptions()
- {
- DaysToRetainInstructions = 2; // 2 days
- ThrottleSeconds = 5; // 5 second
- MaxProcessingInstructionCount = 1000;
- PruneThrottleSeconds = 60; // 1 minute
- }
-
- ///
- /// The maximum number of instructions that can be processed at startup; otherwise the server cold-boots (rebuilds its caches).
- ///
- public int MaxProcessingInstructionCount { get; set; }
-
- ///
- /// A list of callbacks that will be invoked if the lastsynced.txt file does not exist.
- ///
- ///
- /// These callbacks will typically be for eg rebuilding the xml cache file, or examine indexes, based on
- /// the data in the database to get this particular server node up to date.
- ///
- public IEnumerable InitializingCallbacks { get; set; }
-
- ///
- /// The number of days to keep instructions in the database; records older than this number will be pruned.
- ///
- public int DaysToRetainInstructions { get; set; }
-
- ///
- /// The number of seconds to wait between each sync operations.
- ///
- public int ThrottleSeconds { get; set; }
-
- ///
- /// The number of seconds to wait between each prune operations.
- ///
- public int PruneThrottleSeconds { get; set; }
- }
-}
diff --git a/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs b/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs
index 390e89843e..f361eb7a67 100644
--- a/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs
+++ b/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs
@@ -15,19 +15,13 @@ namespace Umbraco.Core.Sync
{
private readonly Lazy _registrationService;
- ///
- /// Gets or sets the registrar options.
- ///
- public DatabaseServerRegistrarOptions Options { get; }
-
///
/// Initializes a new instance of the class.
///
/// The registration service.
/// Some options.
- public DatabaseServerRegistrar(Lazy registrationService, DatabaseServerRegistrarOptions options)
+ public DatabaseServerRegistrar(Lazy registrationService)
{
- Options = options ?? throw new ArgumentNullException(nameof(options));
_registrationService = registrationService ?? throw new ArgumentNullException(nameof(registrationService));
}
diff --git a/src/Umbraco.Core/Sync/DatabaseServerRegistrarOptions.cs b/src/Umbraco.Core/Sync/DatabaseServerRegistrarOptions.cs
deleted file mode 100644
index 58b66ca8e6..0000000000
--- a/src/Umbraco.Core/Sync/DatabaseServerRegistrarOptions.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using System;
-using System.ComponentModel;
-
-namespace Umbraco.Core.Sync
-{
- ///
- /// Provides options to the .
- ///
- public sealed class DatabaseServerRegistrarOptions
- {
- ///
- /// Initializes a new instance of the class with default values.
- ///
- public DatabaseServerRegistrarOptions()
- {
- StaleServerTimeout = TimeSpan.FromMinutes(2); // 2 minutes
- RecurringSeconds = 60; // do it every minute
- }
-
- ///
- /// The amount of seconds to wait between calls to the database on the background thread
- ///
- public int RecurringSeconds { get; set; }
-
- ///
- /// The time span to wait before considering a server stale, after it has last been accessed.
- ///
- public TimeSpan StaleServerTimeout { get; set; }
- }
-}
diff --git a/src/Umbraco.Core/Sync/IBatchedDatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/IBatchedDatabaseServerMessenger.cs
index d8ec82818d..560b7fb235 100644
--- a/src/Umbraco.Core/Sync/IBatchedDatabaseServerMessenger.cs
+++ b/src/Umbraco.Core/Sync/IBatchedDatabaseServerMessenger.cs
@@ -6,7 +6,7 @@ namespace Umbraco.Core.Sync
public interface IBatchedDatabaseServerMessenger : IDatabaseServerMessenger
{
void FlushBatch();
- DatabaseServerMessengerOptions Options { get; }
+ DatabaseServerMessengerCallbacks Callbacks { get; }
void Startup();
}
}
diff --git a/src/Umbraco.Infrastructure/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Infrastructure/BatchedDatabaseServerMessenger.cs
index e022219d15..187eced6e4 100644
--- a/src/Umbraco.Infrastructure/BatchedDatabaseServerMessenger.cs
+++ b/src/Umbraco.Infrastructure/BatchedDatabaseServerMessenger.cs
@@ -1,17 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using Newtonsoft.Json;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Newtonsoft.Json;
using Umbraco.Core;
using Umbraco.Core.Cache;
-using Umbraco.Core.Sync;
-using Umbraco.Web.Routing;
+using Umbraco.Core.Configuration.Models;
+using Umbraco.Core.Hosting;
using Umbraco.Core.Logging;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.Dtos;
using Umbraco.Core.Scoping;
-using Umbraco.Core.Hosting;
+using Umbraco.Core.Sync;
+using Umbraco.Web.Routing;
namespace Umbraco.Web
{
@@ -34,12 +36,13 @@ namespace Umbraco.Web
IProfilingLogger proflog,
ILogger logger,
IServerRegistrar serverRegistrar,
- DatabaseServerMessengerOptions options,
+ DatabaseServerMessengerCallbacks callbacks,
IHostingEnvironment hostingEnvironment,
CacheRefresherCollection cacheRefreshers,
IRequestCache requestCache,
- IRequestAccessor requestAccessor)
- : base(mainDom, scopeProvider, databaseFactory, proflog, logger, serverRegistrar, true, options, hostingEnvironment, cacheRefreshers)
+ IRequestAccessor requestAccessor,
+ IOptions globalSettings)
+ : base(mainDom, scopeProvider, databaseFactory, proflog, logger, serverRegistrar, true, callbacks, hostingEnvironment, cacheRefreshers, globalSettings)
{
_databaseFactory = databaseFactory;
_requestCache = requestCache;
@@ -89,7 +92,7 @@ namespace Umbraco.Web
//Write the instructions but only create JSON blobs with a max instruction count equal to MaxProcessingInstructionCount
using (var scope = ScopeProvider.CreateScope())
{
- foreach (var instructionsBatch in instructions.InGroupsOf(Options.MaxProcessingInstructionCount))
+ foreach (var instructionsBatch in instructions.InGroupsOf(GlobalSettings.DatabaseServerMessenger.MaxProcessingInstructionCount))
{
WriteInstructions(scope, instructionsBatch);
}
@@ -143,7 +146,7 @@ namespace Umbraco.Web
//only write the json blob with a maximum count of the MaxProcessingInstructionCount
using (var scope = ScopeProvider.CreateScope())
{
- foreach (var maxBatch in instructions.InGroupsOf(Options.MaxProcessingInstructionCount))
+ foreach (var maxBatch in instructions.InGroupsOf(GlobalSettings.DatabaseServerMessenger.MaxProcessingInstructionCount))
{
WriteInstructions(scope, maxBatch);
}
diff --git a/src/Umbraco.Infrastructure/Compose/DatabaseServerRegistrarAndMessengerComponent.cs b/src/Umbraco.Infrastructure/Compose/DatabaseServerRegistrarAndMessengerComponent.cs
index 9cd2b187d5..3309c347db 100644
--- a/src/Umbraco.Infrastructure/Compose/DatabaseServerRegistrarAndMessengerComponent.cs
+++ b/src/Umbraco.Infrastructure/Compose/DatabaseServerRegistrarAndMessengerComponent.cs
@@ -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, 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();
}
}
public sealed class DatabaseServerRegistrarAndMessengerComponent : IComponent
{
- private object _locker = new object();
- private readonly DatabaseServerRegistrar _registrar;
private readonly IBatchedDatabaseServerMessenger _messenger;
- private readonly ILogger _logger;
- private readonly ILoggerFactory _loggerFactory;
- private readonly IServerRegistrationService _registrationService;
- private readonly BackgroundTaskRunner _touchTaskRunner;
- private readonly BackgroundTaskRunner _processTaskRunner;
- private bool _started;
- private IBackgroundTask[] _tasks;
private readonly IRequestAccessor _requestAccessor;
public DatabaseServerRegistrarAndMessengerComponent(
- IServerRegistrar serverRegistrar,
IServerMessenger serverMessenger,
- IServerRegistrationService registrationService,
- ILogger 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("ServerRegistration",
- new BackgroundTaskRunnerOptions { AutoStart = true }, _loggerFactory.CreateLogger>(), hostingEnvironment);
- }
-
- // create task runner for BatchedDatabaseServerMessenger
_messenger = serverMessenger as IBatchedDatabaseServerMessenger;
- if (_messenger != null)
- {
- _processTaskRunner = new BackgroundTaskRunner("ServerInstProcess",
- new BackgroundTaskRunnerOptions { AutoStart = true }, _loggerFactory.CreateLogger>(), 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()
{ }
- ///
- /// Handle when a request is made
- ///
- ///
- ///
- ///
- /// 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
- ///
- 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 runner, int delayMilliseconds, int periodMilliseconds,
- IDatabaseServerMessenger messenger, ILogger logger)
- : base(runner, delayMilliseconds, periodMilliseconds)
- {
- _messenger = messenger;
- _logger = logger;
- }
-
- public override bool IsAsync => false;
-
- ///
- /// Runs the background task.
- ///
- /// A value indicating whether to repeat the task.
- 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;
-
- ///
- /// Initializes a new instance of the class.
- ///
- public TouchServerTask(IBackgroundTaskRunner 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;
-
- ///
- /// Runs the background task.
- ///
- /// A value indicating whether to repeat the task.
- 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();
}
}
}
diff --git a/src/Umbraco.Infrastructure/CompositionExtensions.cs b/src/Umbraco.Infrastructure/CompositionExtensions.cs
index aa351657c5..3a7537d346 100644
--- a/src/Umbraco.Infrastructure/CompositionExtensions.cs
+++ b/src/Umbraco.Infrastructure/CompositionExtensions.cs
@@ -236,7 +236,7 @@ namespace Umbraco.Core
/// The composition.
/// A function creating the options.
/// Use DatabaseServerRegistrarAndMessengerComposer.GetDefaultOptions to get the options that Umbraco would use by default.
- public static void SetDatabaseServerMessengerOptions(this Composition composition, Func factory)
+ public static void SetDatabaseServerMessengerCallbacks(this Composition composition, Func factory)
{
composition.Services.AddUnique(factory);
}
@@ -247,7 +247,7 @@ namespace Umbraco.Core
/// The composition.
/// Options.
/// Use DatabaseServerRegistrarAndMessengerComposer.GetDefaultOptions to get the options that Umbraco would use by default.
- public static void SetDatabaseServerMessengerOptions(this Composition composition, DatabaseServerMessengerOptions options)
+ public static void SetDatabaseServerMessengerOptions(this Composition composition, DatabaseServerMessengerCallbacks options)
{
composition.Services.AddUnique(_ => options);
}
diff --git a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs
new file mode 100644
index 0000000000..8d669178b0
--- /dev/null
+++ b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Umbraco.Core;
+using Umbraco.Core.Configuration.Models;
+using Umbraco.Core.Sync;
+
+namespace Umbraco.Infrastructure.HostedServices.ServerRegistration
+{
+ ///
+ /// Implements periodic database instruction processing as a hosted service.
+ ///
+ public class InstructionProcessTask : RecurringHostedServiceBase
+ {
+ private readonly IRuntimeState _runtimeState;
+ private readonly IDatabaseServerMessenger _messenger;
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public InstructionProcessTask(IRuntimeState runtimeState, IServerMessenger messenger, ILogger logger, IOptions globalSettings)
+ : base(globalSettings.Value.DatabaseServerMessenger.TimeBetweenSyncOperations, TimeSpan.FromMinutes(1))
+ {
+ _runtimeState = runtimeState;
+ _messenger = messenger as IDatabaseServerMessenger ?? throw new ArgumentNullException(nameof(messenger));
+ _logger = logger;
+ }
+
+ internal override async Task PerformExecuteAsync(object state)
+ {
+ if (_runtimeState.Level != RuntimeLevel.Run)
+ {
+ return;
+ }
+
+ try
+ {
+ _messenger.Sync();
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Failed (will repeat).");
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs
new file mode 100644
index 0000000000..0384a071c6
--- /dev/null
+++ b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Umbraco.Core;
+using Umbraco.Core.Configuration.Models;
+using Umbraco.Core.Services;
+using Umbraco.Web;
+
+namespace Umbraco.Infrastructure.HostedServices.ServerRegistration
+{
+ ///
+ /// Implements periodic server "touching" (to mark as active/deactive) as a hosted service.
+ ///
+ public class TouchServerTask : RecurringHostedServiceBase
+ {
+ private readonly IRuntimeState _runtimeState;
+ private readonly IServerRegistrationService _serverRegistrationService;
+ private readonly IRequestAccessor _requestAccessor;
+ private readonly ILogger _logger;
+ private GlobalSettings _globalSettings;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TouchServerTask(IRuntimeState runtimeState, IServerRegistrationService serverRegistrationService, IRequestAccessor requestAccessor, ILogger logger, IOptions globalSettings)
+ : base(globalSettings.Value.DatabaseServerRegistrar.WaitTimeBetweenCalls, TimeSpan.FromSeconds(15))
+ {
+ _runtimeState = runtimeState;
+ _serverRegistrationService = serverRegistrationService ?? throw new ArgumentNullException(nameof(serverRegistrationService));
+ _requestAccessor = requestAccessor;
+ _logger = logger;
+ _globalSettings = globalSettings.Value;
+ }
+
+ internal override async Task PerformExecuteAsync(object state)
+ {
+ if (_runtimeState.Level != RuntimeLevel.Run)
+ {
+ return;
+ }
+
+ var serverAddress = _requestAccessor.GetApplicationUrl()?.ToString();
+ if (serverAddress.IsNullOrWhiteSpace())
+ {
+ _logger.LogWarning("No umbracoApplicationUrl for service (yet), skip.");
+ return;
+ }
+
+ 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.
+ _serverRegistrationService.TouchServer(serverAddress, _serverRegistrationService.CurrentServerIdentity, _globalSettings.DatabaseServerRegistrar.StaleServerTimeout);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to update server record in database.");
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs
index 081c1b4d10..2ae6f74465 100644
--- a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs
+++ b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs
@@ -139,8 +139,7 @@ namespace Umbraco.Core.Runtime
return singleServer
? (IServerRegistrar) new SingleServerRegistrar(f.GetRequiredService())
: new DatabaseServerRegistrar(
- new Lazy(f.GetRequiredService),
- new DatabaseServerRegistrarOptions());
+ new Lazy(f.GetRequiredService));
});
// by default we'll use the database server messenger with default options (no callbacks),
@@ -154,9 +153,11 @@ namespace Umbraco.Core.Runtime
factory.GetRequiredService(),
factory.GetRequiredService>(),
factory.GetRequiredService(),
- true, new DatabaseServerMessengerOptions(),
+ true,
+ new DatabaseServerMessengerCallbacks(),
factory.GetRequiredService(),
- factory.GetRequiredService()
+ factory.GetRequiredService(),
+ factory.GetRequiredService>()
));
composition.CacheRefreshers()
diff --git a/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs
index 7066e5d806..856772148f 100644
--- a/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs
+++ b/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs
@@ -5,16 +5,17 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NPoco;
-using Microsoft.Extensions.Logging;
using Umbraco.Core.Cache;
-using Umbraco.Core.Composing;
+using Umbraco.Core.Configuration.Models;
+using Umbraco.Core.Hosting;
using Umbraco.Core.Logging;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.Dtos;
-using Umbraco.Core.Hosting;
using Umbraco.Core.Scoping;
namespace Umbraco.Core.Sync
@@ -46,11 +47,13 @@ namespace Umbraco.Core.Sync
private bool _syncing;
private bool _released;
- public DatabaseServerMessengerOptions Options { get; }
+ public DatabaseServerMessengerCallbacks Callbacks { get; }
+
+ public GlobalSettings GlobalSettings { get; }
public DatabaseServerMessenger(
IMainDom mainDom, IScopeProvider scopeProvider, IUmbracoDatabaseFactory umbracoDatabaseFactory, IProfilingLogger proflog, ILogger logger, IServerRegistrar serverRegistrar,
- bool distributedEnabled, DatabaseServerMessengerOptions options, IHostingEnvironment hostingEnvironment, CacheRefresherCollection cacheRefreshers)
+ bool distributedEnabled, DatabaseServerMessengerCallbacks callbacks, IHostingEnvironment hostingEnvironment, CacheRefresherCollection cacheRefreshers, IOptions globalSettings)
: base(distributedEnabled)
{
ScopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider));
@@ -61,7 +64,8 @@ namespace Umbraco.Core.Sync
_hostingEnvironment = hostingEnvironment;
_cacheRefreshers = cacheRefreshers;
Logger = logger;
- Options = options ?? throw new ArgumentNullException(nameof(options));
+ Callbacks = callbacks ?? throw new ArgumentNullException(nameof(callbacks));
+ GlobalSettings = globalSettings.Value;
_lastPruned = _lastSync = DateTime.UtcNow;
_syncIdle = new ManualResetEvent(true);
_distCacheFilePath = new Lazy(() => GetDistCacheFilePath(hostingEnvironment));
@@ -200,14 +204,14 @@ namespace Umbraco.Core.Sync
//check for how many instructions there are to process, each row contains a count of the number of instructions contained in each
//row so we will sum these numbers to get the actual count.
var count = database.ExecuteScalar("SELECT SUM(instructionCount) FROM umbracoCacheInstruction WHERE id > @lastId", new {lastId = _lastId});
- if (count > Options.MaxProcessingInstructionCount)
+ if (count > GlobalSettings.DatabaseServerMessenger.MaxProcessingInstructionCount)
{
//too many instructions, proceed to cold boot
Logger.LogWarning(
"The instruction count ({InstructionCount}) exceeds the specified MaxProcessingInstructionCount ({MaxProcessingInstructionCount})."
+ " The server will skip existing instructions, rebuild its caches and indexes entirely, adjust its last synced Id"
+ " to the latest found in the database and maintain cache updates based on that Id.",
- count, Options.MaxProcessingInstructionCount);
+ count, GlobalSettings.DatabaseServerMessenger.MaxProcessingInstructionCount);
coldboot = true;
}
@@ -225,8 +229,8 @@ namespace Umbraco.Core.Sync
SaveLastSynced(maxId);
// execute initializing callbacks
- if (Options.InitializingCallbacks != null)
- foreach (var callback in Options.InitializingCallbacks)
+ if (Callbacks.InitializingCallbacks != null)
+ foreach (var callback in Callbacks.InitializingCallbacks)
callback();
}
@@ -248,7 +252,7 @@ namespace Umbraco.Core.Sync
if (_released)
return;
- if ((DateTime.UtcNow - _lastSync).TotalSeconds <= Options.ThrottleSeconds)
+ if ((DateTime.UtcNow - _lastSync) <= GlobalSettings.DatabaseServerMessenger.TimeBetweenSyncOperations)
return;
//Set our flag and the lock to be in it's original state (i.e. it can be awaited)
@@ -265,7 +269,7 @@ namespace Umbraco.Core.Sync
ProcessDatabaseInstructions(scope.Database);
//Check for pruning throttling
- if (_released || (DateTime.UtcNow - _lastPruned).TotalSeconds <= Options.PruneThrottleSeconds)
+ if (_released || (DateTime.UtcNow - _lastPruned) <= GlobalSettings.DatabaseServerMessenger.TimeBetweenPruneOperations)
{
scope.Complete();
return;
@@ -447,7 +451,7 @@ namespace Umbraco.Core.Sync
///
private void PruneOldInstructions(IUmbracoDatabase database)
{
- var pruneDate = DateTime.UtcNow.AddDays(-Options.DaysToRetainInstructions);
+ var pruneDate = DateTime.UtcNow - GlobalSettings.DatabaseServerMessenger.TimeToRetainInstructions;
// using 2 queries is faster than convoluted joins
diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/HealthCheckNotifierTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/HealthCheckNotifierTests.cs
index 6c1284854e..cdb98f7fa5 100644
--- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/HealthCheckNotifierTests.cs
+++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/HealthCheckNotifierTests.cs
@@ -37,10 +37,14 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HostedServices
VerifyNotificationsNotSent();
}
- [Test]
- public async Task Does_Not_Execute_When_Runtime_State_Is_Not_Run()
+ [TestCase(RuntimeLevel.Boot)]
+ [TestCase(RuntimeLevel.Install)]
+ [TestCase(RuntimeLevel.Unknown)]
+ [TestCase(RuntimeLevel.Upgrade)]
+ [TestCase(RuntimeLevel.BootFailed)]
+ public async Task Does_Not_Execute_When_Runtime_State_Is_Not_Run(RuntimeLevel runtimeLevel)
{
- var sut = CreateHealthCheckNotifier(runtimeLevel: RuntimeLevel.Boot);
+ var sut = CreateHealthCheckNotifier(runtimeLevel: runtimeLevel);
await sut.PerformExecuteAsync(null);
VerifyNotificationsNotSent();
}
diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ScheduledPublishingTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ScheduledPublishingTests.cs
index e0fd2a4acc..c016e52e54 100644
--- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ScheduledPublishingTests.cs
+++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ScheduledPublishingTests.cs
@@ -27,10 +27,14 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HostedServices
VerifyScheduledPublishingNotPerformed();
}
- [Test]
- public async Task Does_Not_Execute_When_Runtime_State_Is_Not_Run()
+ [TestCase(RuntimeLevel.Boot)]
+ [TestCase(RuntimeLevel.Install)]
+ [TestCase(RuntimeLevel.Unknown)]
+ [TestCase(RuntimeLevel.Upgrade)]
+ [TestCase(RuntimeLevel.BootFailed)]
+ public async Task Does_Not_Execute_When_Runtime_State_Is_Not_Run(RuntimeLevel runtimeLevel)
{
- var sut = CreateScheduledPublishing(runtimeLevel: RuntimeLevel.Boot);
+ var sut = CreateScheduledPublishing(runtimeLevel: runtimeLevel);
await sut.PerformExecuteAsync(null);
VerifyScheduledPublishingNotPerformed();
}
diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTaskTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTaskTests.cs
new file mode 100644
index 0000000000..86644afc77
--- /dev/null
+++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTaskTests.cs
@@ -0,0 +1,67 @@
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Moq;
+using NUnit.Framework;
+using Umbraco.Core;
+using Umbraco.Core.Configuration.Models;
+using Umbraco.Core.Sync;
+using Umbraco.Infrastructure.HostedServices.ServerRegistration;
+
+namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HostedServices.ServerRegistration
+{
+ [TestFixture]
+ public class InstructionProcessTaskTests
+ {
+ private Mock _mockDatabaseServerMessenger;
+
+ [TestCase(RuntimeLevel.Boot)]
+ [TestCase(RuntimeLevel.Install)]
+ [TestCase(RuntimeLevel.Unknown)]
+ [TestCase(RuntimeLevel.Upgrade)]
+ [TestCase(RuntimeLevel.BootFailed)]
+ public async Task Does_Not_Execute_When_Runtime_State_Is_Not_Run(RuntimeLevel runtimeLevel)
+ {
+ var sut = CreateInstructionProcessTask(runtimeLevel: runtimeLevel);
+ await sut.PerformExecuteAsync(null);
+ VerifyMessengerNotSynced();
+ }
+
+ [Test]
+ public async Task Executes_And_Touches_Server()
+ {
+ var sut = CreateInstructionProcessTask();
+ await sut.PerformExecuteAsync(null);
+ VerifyMessengerSynced();
+ }
+
+ private InstructionProcessTask CreateInstructionProcessTask(RuntimeLevel runtimeLevel = RuntimeLevel.Run)
+ {
+ var mockRunTimeState = new Mock();
+ mockRunTimeState.SetupGet(x => x.Level).Returns(runtimeLevel);
+
+ var mockLogger = new Mock>();
+
+ _mockDatabaseServerMessenger = new Mock();
+
+ var settings = new GlobalSettings();
+
+ return new InstructionProcessTask(mockRunTimeState.Object, _mockDatabaseServerMessenger.Object, mockLogger.Object, Options.Create(settings));
+ }
+
+ private void VerifyMessengerNotSynced()
+ {
+ VerifyMessengerSyncedTimes(Times.Never());
+ }
+
+ private void VerifyMessengerSynced()
+ {
+ VerifyMessengerSyncedTimes(Times.Once());
+ }
+
+ private void VerifyMessengerSyncedTimes(Times times)
+ {
+ _mockDatabaseServerMessenger.Verify(x => x.Sync(), times);
+ }
+ }
+}
diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTaskTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTaskTests.cs
new file mode 100644
index 0000000000..499ff05f04
--- /dev/null
+++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTaskTests.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Moq;
+using NUnit.Framework;
+using Umbraco.Core;
+using Umbraco.Core.Configuration.Models;
+using Umbraco.Core.Services;
+using Umbraco.Infrastructure.HostedServices.ServerRegistration;
+using Umbraco.Web;
+
+namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HostedServices.ServerRegistration
+{
+ [TestFixture]
+ public class TouchServerTaskTests
+ {
+ private Mock _mockServerRegistrationService;
+
+ private const string _applicationUrl = "https://mysite.com/";
+ private const string _serverIdentity = "Test/1";
+ private readonly TimeSpan _staleServerTimeout = TimeSpan.FromMinutes(2);
+
+ [TestCase(RuntimeLevel.Boot)]
+ [TestCase(RuntimeLevel.Install)]
+ [TestCase(RuntimeLevel.Unknown)]
+ [TestCase(RuntimeLevel.Upgrade)]
+ [TestCase(RuntimeLevel.BootFailed)]
+ public async Task Does_Not_Execute_When_Runtime_State_Is_Not_Run(RuntimeLevel runtimeLevel)
+ {
+ var sut = CreateTouchServerTask(runtimeLevel: runtimeLevel);
+ await sut.PerformExecuteAsync(null);
+ VerifyServerNotTouched();
+ }
+
+ [Test]
+ public async Task Does_Not_Execute_When_Application_Url_Is_Not_Available()
+ {
+ var sut = CreateTouchServerTask(applicationUrl: string.Empty);
+ await sut.PerformExecuteAsync(null);
+ VerifyServerNotTouched();
+ }
+
+ [Test]
+ public async Task Executes_And_Touches_Server()
+ {
+ var sut = CreateTouchServerTask();
+ await sut.PerformExecuteAsync(null);
+ VerifyServerTouched();
+ }
+
+ private TouchServerTask CreateTouchServerTask(RuntimeLevel runtimeLevel = RuntimeLevel.Run, string applicationUrl = _applicationUrl)
+ {
+ var mockRequestAccessor = new Mock();
+ mockRequestAccessor.Setup(x => x.GetApplicationUrl()).Returns(!string.IsNullOrEmpty(applicationUrl) ? new Uri(_applicationUrl) : null);
+
+ var mockRunTimeState = new Mock();
+ mockRunTimeState.SetupGet(x => x.Level).Returns(runtimeLevel);
+
+ var mockLogger = new Mock>();
+
+ _mockServerRegistrationService = new Mock();
+ _mockServerRegistrationService.SetupGet(x => x.CurrentServerIdentity).Returns(_serverIdentity);
+
+ var settings = new GlobalSettings
+ {
+ DatabaseServerRegistrar = new DatabaseServerRegistrarSettings
+ {
+ StaleServerTimeout = _staleServerTimeout,
+ }
+ };
+
+ return new TouchServerTask(mockRunTimeState.Object, _mockServerRegistrationService.Object, mockRequestAccessor.Object,
+ mockLogger.Object, Options.Create(settings));
+ }
+
+ private void VerifyServerNotTouched()
+ {
+ VerifyServerTouchedTimes(Times.Never());
+ }
+
+ private void VerifyServerTouched()
+ {
+ VerifyServerTouchedTimes(Times.Once());
+ }
+
+ private void VerifyServerTouchedTimes(Times times)
+ {
+ _mockServerRegistrationService
+ .Verify(x => x.TouchServer(
+ It.Is(y => y == _applicationUrl),
+ It.Is(y => y == _serverIdentity),
+ It.Is(y => y == _staleServerTimeout)),
+ times);
+ }
+ }
+}
diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreRequestAccessor.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreRequestAccessor.cs
index 985c568a08..c5104c0fdc 100644
--- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreRequestAccessor.cs
+++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreRequestAccessor.cs
@@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Options;
using Umbraco.Core.Configuration.Models;
-using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Extensions;
using Umbraco.Web.Common.Lifetime;
using Umbraco.Web.Routing;
@@ -43,9 +42,8 @@ namespace Umbraco.Web.Common.AspNetCore
RouteAttempt?.Invoke(sender, new RoutableAttemptEventArgs(reason, _umbracoContextAccessor.UmbracoContext));
}
-
-
public string GetRequestValue(string name) => GetFormValue(name) ?? GetQueryStringValue(name);
+
public string GetFormValue(string name)
{
var request = _httpContextAccessor.GetRequiredHttpContext().Request;
@@ -58,14 +56,15 @@ namespace Umbraco.Web.Common.AspNetCore
public event EventHandler EndRequest;
public event EventHandler RouteAttempt;
+
public Uri GetRequestUrl() => _httpContextAccessor.HttpContext != null ? new Uri(_httpContextAccessor.HttpContext.Request.GetEncodedUrl()) : null;
+
public Uri GetApplicationUrl()
{
//Fixme: This causes problems with site swap on azure because azure pre-warms a site by calling into `localhost` and when it does that
// it changes the URL to `localhost:80` which actually doesn't work for pinging itself, it only works internally in Azure. The ironic part
// about this is that this is here specifically for the slot swap scenario https://issues.umbraco.org/issue/U4-10626
-
// see U4-10626 - in some cases we want to reset the application url
// (this is a simplified version of what was in 7.x)
// note: should this be optional? is it expensive?
@@ -77,7 +76,10 @@ namespace Umbraco.Web.Common.AspNetCore
var request = _httpContextAccessor.HttpContext?.Request;
- if (request is null) return _currentApplicationUrl;
+ if (request is null)
+ {
+ return _currentApplicationUrl;
+ }
var url = UriHelper.BuildAbsolute(request.Scheme, request.Host);
var change = url != null && !_applicationUrls.Contains(url);
diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs
index 48ef00e1c8..c98cbca39e 100644
--- a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs
+++ b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs
@@ -25,6 +25,7 @@ using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.SqlSyntax;
using Umbraco.Core.Runtime;
using Umbraco.Infrastructure.HostedServices;
+using Umbraco.Infrastructure.HostedServices.ServerRegistration;
using Umbraco.Web.Common.AspNetCore;
using Umbraco.Web.Common.Profiler;
using ConnectionStrings = Umbraco.Core.Configuration.Models.ConnectionStrings;
@@ -298,6 +299,9 @@ namespace Umbraco.Extensions
services.AddHostedService();
services.AddHostedService();
+ services.AddHostedService();
+ services.AddHostedService();
+
return services;
}