From 76593aa7ca2b49add058efe2825d7183cf7d5d36 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Tue, 25 Jan 2022 06:38:20 +0100 Subject: [PATCH] Add IsRestarting property to Umbraco application notifications (#11883) * Add IsRestarting property to Umbraco application notifications * Add IUmbracoApplicationLifetimeNotification and update constructors * Only subscribe to events on initial startup * Cleanup CoreRuntime * Do not reset StaticApplicationLogging instance after stopping/during restart --- ...IUmbracoApplicationLifetimeNotification.cs | 17 +++ .../UmbracoApplicationStartedNotification.cs | 15 ++- .../UmbracoApplicationStartingNotification.cs | 27 ++++- .../UmbracoApplicationStoppedNotification.cs | 15 ++- .../UmbracoApplicationStoppingNotification.cs | 27 ++++- .../Runtime/CoreRuntime.cs | 106 +++++++++--------- 6 files changed, 139 insertions(+), 68 deletions(-) create mode 100644 src/Umbraco.Core/Notifications/IUmbracoApplicationLifetimeNotification.cs diff --git a/src/Umbraco.Core/Notifications/IUmbracoApplicationLifetimeNotification.cs b/src/Umbraco.Core/Notifications/IUmbracoApplicationLifetimeNotification.cs new file mode 100644 index 0000000000..4b0ea6826a --- /dev/null +++ b/src/Umbraco.Core/Notifications/IUmbracoApplicationLifetimeNotification.cs @@ -0,0 +1,17 @@ +namespace Umbraco.Cms.Core.Notifications +{ + /// + /// Represents an Umbraco application lifetime (starting, started, stopping, stopped) notification. + /// + /// + public interface IUmbracoApplicationLifetimeNotification : INotification + { + /// + /// Gets a value indicating whether Umbraco is restarting (e.g. after an install or upgrade). + /// + /// + /// true if Umbraco is restarting; otherwise, false. + /// + bool IsRestarting { get; } + } +} diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs index a3d38720d7..196af7dfe1 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs @@ -3,7 +3,16 @@ namespace Umbraco.Cms.Core.Notifications /// /// Notification that occurs when Umbraco has completely booted up and the request processing pipeline is configured. /// - /// - public class UmbracoApplicationStartedNotification : INotification - { } + /// + public class UmbracoApplicationStartedNotification : IUmbracoApplicationLifetimeNotification + { + /// + /// Initializes a new instance of the class. + /// + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStartedNotification(bool isRestarting) => IsRestarting = isRestarting; + + /// + public bool IsRestarting { get; } + } } diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs index dd60f9431c..82b87aa3bf 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs @@ -1,16 +1,34 @@ +using System; + namespace Umbraco.Cms.Core.Notifications { /// /// Notification that occurs at the very end of the Umbraco boot process (after all s are initialized). /// - /// - public class UmbracoApplicationStartingNotification : INotification + /// + public class UmbracoApplicationStartingNotification : IUmbracoApplicationLifetimeNotification { /// /// Initializes a new instance of the class. /// /// The runtime level - public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel) => RuntimeLevel = runtimeLevel; + [Obsolete("Use ctor with all params")] + public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel) + : this(runtimeLevel, false) + { + // TODO: Remove this constructor in V10 + } + + /// + /// Initializes a new instance of the class. + /// + /// The runtime level + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel, bool isRestarting) + { + RuntimeLevel = runtimeLevel; + IsRestarting = isRestarting; + } /// /// Gets the runtime level. @@ -19,5 +37,8 @@ namespace Umbraco.Cms.Core.Notifications /// The runtime level. /// public RuntimeLevel RuntimeLevel { get; } + + /// + public bool IsRestarting { get; } } } diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs index be4c6ccfd4..c6dac40a26 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs @@ -3,7 +3,16 @@ namespace Umbraco.Cms.Core.Notifications /// /// Notification that occurs when Umbraco has completely shutdown. /// - /// - public class UmbracoApplicationStoppedNotification : INotification - { } + /// + public class UmbracoApplicationStoppedNotification : IUmbracoApplicationLifetimeNotification + { + /// + /// Initializes a new instance of the class. + /// + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStoppedNotification(bool isRestarting) => IsRestarting = isRestarting; + + /// + public bool IsRestarting { get; } + } } diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs index 6d5234bbcc..062ca954d9 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs @@ -1,9 +1,30 @@ +using System; + namespace Umbraco.Cms.Core.Notifications { /// /// Notification that occurs when Umbraco is shutting down (after all s are terminated). /// - /// - public class UmbracoApplicationStoppingNotification : INotification - { } + /// + public class UmbracoApplicationStoppingNotification : IUmbracoApplicationLifetimeNotification + { + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Use ctor with all params")] + public UmbracoApplicationStoppingNotification() + : this(false) + { + // TODO: Remove this constructor in V10 + } + + /// + /// Initializes a new instance of the class. + /// + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStoppingNotification(bool isRestarting) => IsRestarting = isRestarting; + + /// + public bool IsRestarting { get; } + } } diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs index 5dbe78c2f5..851d67e713 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs @@ -134,60 +134,48 @@ namespace Umbraco.Cms.Infrastructure.Runtime public IRuntimeState State { get; } /// - public async Task RestartAsync() - { - await StopAsync(_cancellationToken); - await _eventAggregator.PublishAsync(new UmbracoApplicationStoppedNotification(), _cancellationToken); - await StartAsync(_cancellationToken); - await _eventAggregator.PublishAsync(new UmbracoApplicationStartedNotification(), _cancellationToken); - } + public async Task StartAsync(CancellationToken cancellationToken) => await StartAsync(cancellationToken, false); /// - public async Task StartAsync(CancellationToken cancellationToken) + public async Task StopAsync(CancellationToken cancellationToken) => await StopAsync(cancellationToken, false); + + /// + public async Task RestartAsync() + { + await StopAsync(_cancellationToken, true); + await _eventAggregator.PublishAsync(new UmbracoApplicationStoppedNotification(true), _cancellationToken); + await StartAsync(_cancellationToken, true); + await _eventAggregator.PublishAsync(new UmbracoApplicationStartedNotification(true), _cancellationToken); + } + + private async Task StartAsync(CancellationToken cancellationToken, bool isRestarting) { // Store token, so we can re-use this during restart _cancellationToken = cancellationToken; - StaticApplicationLogging.Initialize(_loggerFactory); - StaticServiceProvider.Instance = _serviceProvider; - - AppDomain.CurrentDomain.UnhandledException += (_, args) => + if (isRestarting == false) { - var exception = (Exception)args.ExceptionObject; - var isTerminating = args.IsTerminating; // always true? + StaticApplicationLogging.Initialize(_loggerFactory); + StaticServiceProvider.Instance = _serviceProvider; - var msg = "Unhandled exception in AppDomain"; - - if (isTerminating) - { - msg += " (terminating)"; - } - - msg += "."; - - _logger.LogError(exception, msg); - }; - - // Add application started and stopped notifications (only on initial startup, not restarts) - if (_hostApplicationLifetime.ApplicationStarted.IsCancellationRequested == false) - { - _hostApplicationLifetime.ApplicationStarted.Register(() => _eventAggregator.Publish(new UmbracoApplicationStartedNotification())); - _hostApplicationLifetime.ApplicationStopped.Register(() => _eventAggregator.Publish(new UmbracoApplicationStoppedNotification())); + AppDomain.CurrentDomain.UnhandledException += (_, args) + => _logger.LogError(args.ExceptionObject as Exception, $"Unhandled exception in AppDomain{(args.IsTerminating ? " (terminating)" : null)}."); } - // acquire the main domain - if this fails then anything that should be registered with MainDom will not operate + // Acquire the main domain - if this fails then anything that should be registered with MainDom will not operate AcquireMainDom(); // TODO (V10): Remove this obsoleted notification publish. await _eventAggregator.PublishAsync(new UmbracoApplicationMainDomAcquiredNotification(), cancellationToken); - // notify for unattended install + // Notify for unattended install await _eventAggregator.PublishAsync(new RuntimeUnattendedInstallNotification(), cancellationToken); DetermineRuntimeLevel(); if (!State.UmbracoCanBoot()) { - return; // The exception will be rethrown by BootFailedMiddelware + // We cannot continue here, the exception will be rethrown by BootFailedMiddelware + return; } IApplicationShutdownRegistry hostingEnvironmentLifetime = _applicationShutdownRegistry; @@ -196,7 +184,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime throw new InvalidOperationException($"An instance of {typeof(IApplicationShutdownRegistry)} could not be resolved from the container, ensure that one if registered in your runtime before calling {nameof(IRuntime)}.{nameof(StartAsync)}"); } - // if level is Run and reason is UpgradeMigrations, that means we need to perform an unattended upgrade + // If level is Run and reason is UpgradeMigrations, that means we need to perform an unattended upgrade var unattendedUpgradeNotification = new RuntimeUnattendedUpgradeNotification(); await _eventAggregator.PublishAsync(unattendedUpgradeNotification, cancellationToken); switch (unattendedUpgradeNotification.UnattendedUpgradeResult) @@ -207,54 +195,59 @@ namespace Umbraco.Cms.Infrastructure.Runtime throw new InvalidOperationException($"Unattended upgrade result was {RuntimeUnattendedUpgradeNotification.UpgradeResult.HasErrors} but no {nameof(BootFailedException)} was registered"); } - // we cannot continue here, the exception will be rethrown by BootFailedMiddelware + // We cannot continue here, the exception will be rethrown by BootFailedMiddelware return; case RuntimeUnattendedUpgradeNotification.UpgradeResult.CoreUpgradeComplete: case RuntimeUnattendedUpgradeNotification.UpgradeResult.PackageMigrationComplete: - // upgrade is done, set reason to Run + // Upgrade is done, set reason to Run DetermineRuntimeLevel(); break; case RuntimeUnattendedUpgradeNotification.UpgradeResult.NotRequired: break; } - // TODO (V10): Remove this obsoleted notification publish. + // TODO (V10): Remove this obsoleted notification publish await _eventAggregator.PublishAsync(new UmbracoApplicationComponentsInstallingNotification(State.Level), cancellationToken); - // create & initialize the components + // Initialize the components _components.Initialize(); - await _eventAggregator.PublishAsync(new UmbracoApplicationStartingNotification(State.Level), cancellationToken); + await _eventAggregator.PublishAsync(new UmbracoApplicationStartingNotification(State.Level, isRestarting), cancellationToken); + + if (isRestarting == false) + { + // Add application started and stopped notifications last (to ensure they're always published after starting) + _hostApplicationLifetime.ApplicationStarted.Register(() => _eventAggregator.Publish(new UmbracoApplicationStartedNotification(false))); + _hostApplicationLifetime.ApplicationStopped.Register(() => _eventAggregator.Publish(new UmbracoApplicationStoppedNotification(false))); + } } - public async Task StopAsync(CancellationToken cancellationToken) + private async Task StopAsync(CancellationToken cancellationToken, bool isRestarting) { _components.Terminate(); - await _eventAggregator.PublishAsync(new UmbracoApplicationStoppingNotification(), cancellationToken); - StaticApplicationLogging.Initialize(null); + await _eventAggregator.PublishAsync(new UmbracoApplicationStoppingNotification(isRestarting), cancellationToken); } private void AcquireMainDom() { - using (DisposableTimer timer = _profilingLogger.DebugDuration("Acquiring MainDom.", "Acquired.")) + using DisposableTimer timer = _profilingLogger.DebugDuration("Acquiring MainDom.", "Acquired."); + + try { - try - { - _mainDom.Acquire(_applicationShutdownRegistry); - } - catch - { - timer?.Fail(); - throw; - } + _mainDom.Acquire(_applicationShutdownRegistry); + } + catch + { + timer?.Fail(); + throw; } } private void DetermineRuntimeLevel() { - if (State.BootFailedException != null) + if (State.BootFailedException is not null) { - // there's already been an exception so cannot boot and no need to check + // There's already been an exception, so cannot boot and no need to check return; } @@ -277,7 +270,8 @@ namespace Umbraco.Cms.Infrastructure.Runtime State.Configure(RuntimeLevel.BootFailed, RuntimeLevelReason.BootFailedOnException); timer?.Fail(); _logger.LogError(ex, "Boot Failed"); - // We do not throw the exception. It will be rethrown by BootFailedMiddleware + + // We do not throw the exception, it will be rethrown by BootFailedMiddleware } } }