using System; using System.Threading; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Logging; using Umbraco.Core.Services; using Umbraco.Core.Services.Changes; using Umbraco.Core.Sync; using Umbraco.Examine; using Umbraco.Web.Cache; using Umbraco.Web.Routing; using Umbraco.Web.Scheduling; using Umbraco.Web.Search; using Current = Umbraco.Web.Composing.Current; namespace Umbraco.Web.Compose { /// /// Ensures that servers are automatically registered in the database, when using the database server registrar. /// /// /// At the moment servers are automatically registered upon first request and then on every /// request but not more than once per (configurable) period. This really is "for information & debug" purposes so /// we can look at the table and see what servers are registered - but the info is not used anywhere. /// Should we actually want to use this, we would need a better and more deterministic way of figuring /// out the "server address" ie the address to which server-to-server requests should be sent - because it /// probably is not the "current request address" - especially in multi-domains configurations. /// [RuntimeLevel(MinLevel = RuntimeLevel.Run)] // during Initialize / Startup, we end up checking Examine, which needs to be initialized beforehand // TODO: should not be a strong dependency on "examine" but on an "indexing component" [ComposeAfter(typeof(ExamineComposer))] public sealed class DatabaseServerRegistrarAndMessengerComposer : ComponentComposer, ICoreComposer { public static DatabaseServerMessengerOptions GetDefaultOptions(IFactory factory) { var logger = factory.GetInstance(); var indexRebuilder = factory.GetInstance(); return new DatabaseServerMessengerOptions { //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) InitializingCallbacks = new Action[] { //rebuild the xml cache file if the server is not synced () => { // rebuild the published snapshot caches entirely, if the server is not synced // this is equivalent to DistributedCache RefreshAll... but local only // (we really should have a way to reuse RefreshAll... locally) // note: refresh all content & media caches does refresh content types too var svc = Current.PublishedSnapshotService; svc.Notify(new[] { new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll) }); svc.Notify(new[] { new ContentCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out _, out _); svc.Notify(new[] { new MediaCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out _); }, //rebuild indexes if the server is not synced // NOTE: This will rebuild ALL indexes including the members, if developers want to target specific // indexes then they can adjust this logic themselves. () => { ExamineComponent.RebuildIndexes(indexRebuilder, logger, false, 5000); } } }; } public override void Compose(Composition composition) { base.Compose(composition); composition.SetDatabaseServerMessengerOptions(GetDefaultOptions); composition.SetServerMessenger(); } } public sealed class DatabaseServerRegistrarAndMessengerComponent : IComponent { private object _locker = new object(); private readonly DatabaseServerRegistrar _registrar; private readonly BatchedDatabaseServerMessenger _messenger; private readonly IRuntimeState _runtime; private readonly ILogger _logger; private readonly IServerRegistrationService _registrationService; private readonly BackgroundTaskRunner _touchTaskRunner; private readonly BackgroundTaskRunner _processTaskRunner; private bool _started; private IBackgroundTask[] _tasks; private IndexRebuilder _indexRebuilder; public DatabaseServerRegistrarAndMessengerComponent(IRuntimeState runtime, IServerRegistrar serverRegistrar, IServerMessenger serverMessenger, IServerRegistrationService registrationService, ILogger logger, IndexRebuilder indexRebuilder) { _runtime = runtime; _logger = logger; _registrationService = registrationService; _indexRebuilder = indexRebuilder; // create task runner for DatabaseServerRegistrar _registrar = serverRegistrar as DatabaseServerRegistrar; if (_registrar != null) { _touchTaskRunner = new BackgroundTaskRunner("ServerRegistration", new BackgroundTaskRunnerOptions { AutoStart = true }, logger); } // create task runner for BatchedDatabaseServerMessenger _messenger = serverMessenger as BatchedDatabaseServerMessenger; if (_messenger != null) { _processTaskRunner = new BackgroundTaskRunner("ServerInstProcess", new BackgroundTaskRunnerOptions { AutoStart = true }, logger); } } public void Initialize() { //We will start the whole process when a successful request is made if (_registrar != null || _messenger != null) UmbracoModule.RouteAttempt += RegisterBackgroundTasksOnce; // 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 UmbracoModule.RouteAttempt which is triggered in ProcessRequest /// we are safe, UmbracoApplicationUrl has been initialized /// private void RegisterBackgroundTasksOnce(object sender, RoutableAttemptEventArgs e) { switch (e.Outcome) { case EnsureRoutableOutcome.IsRoutable: case EnsureRoutableOutcome.NotDocumentRequest: UmbracoModule.RouteAttempt -= RegisterBackgroundTasksOnce; RegisterBackgroundTasks(); break; } } private void RegisterBackgroundTasks() { // only perform this one time ever LazyInitializer.EnsureInitialized(ref _tasks, ref _started, ref _locker, () => { var serverAddress = _runtime.ApplicationUrl.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 DatabaseServerMessenger _messenger; private readonly ILogger _logger; public InstructionProcessTask(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, DatabaseServerMessenger 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.Error("Failed (will repeat).", e); } 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.Error(ex, "Failed to update server record in database."); return false; // probably stop if we have an error } } } } }