diff --git a/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs b/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs index 01d3b36a5d..2c53407398 100644 --- a/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs @@ -2,7 +2,7 @@ namespace Umbraco.Core.Configuration.Models { public class KeepAliveSettings { - public bool DisableKeepAliveTask => false; + public bool DisableKeepAliveTask { get; set; } = false; public string KeepAlivePingUrl => "{umbracoApplicationUrl}/api/keepalive/ping"; } diff --git a/src/Umbraco.Core/Scheduling/KeepAlive.cs b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs similarity index 66% rename from src/Umbraco.Core/Scheduling/KeepAlive.cs rename to src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs index 9b09a81cc3..337890c799 100644 --- a/src/Umbraco.Core/Scheduling/KeepAlive.cs +++ b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs @@ -1,17 +1,19 @@ using System; using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Core; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Logging; using Umbraco.Core.Sync; -using Microsoft.Extensions.Logging; +using Umbraco.Web; -namespace Umbraco.Web.Scheduling +namespace Umbraco.Infrastructure.HostedServices { - public class KeepAlive : RecurringTaskBase + /// + /// Hosted service implementation for keep alive feature. + /// + public class KeepAlive : RecurringHostedServiceBase { private readonly IRequestAccessor _requestAccessor; private readonly IMainDom _mainDom; @@ -19,11 +21,10 @@ namespace Umbraco.Web.Scheduling private readonly ILogger _logger; private readonly IProfilingLogger _profilingLogger; private readonly IServerRegistrar _serverRegistrar; - private static HttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; - public KeepAlive(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, - IRequestAccessor requestAccessor, IMainDom mainDom, IOptions keepAliveSettings, ILogger logger, IProfilingLogger profilingLogger, IServerRegistrar serverRegistrar) - : base(runner, delayMilliseconds, periodMilliseconds) + public KeepAlive(IRequestAccessor requestAccessor, IMainDom mainDom, IOptions keepAliveSettings, ILogger logger, IProfilingLogger profilingLogger, IServerRegistrar serverRegistrar, IHttpClientFactory httpClientFactory) + : base(TimeSpan.FromMinutes(5), DefaultDelay) { _requestAccessor = requestAccessor; _mainDom = mainDom; @@ -31,30 +32,32 @@ namespace Umbraco.Web.Scheduling _logger = logger; _profilingLogger = profilingLogger; _serverRegistrar = serverRegistrar; - if (_httpClient == null) - { - _httpClient = new HttpClient(); - } + _httpClientFactory = httpClientFactory; } - public override async Task PerformRunAsync(CancellationToken token) + public override async void ExecuteAsync(object state) { - // not on replicas nor unknown role servers + if (_keepAliveSettings.DisableKeepAliveTask) + { + return; + } + + // Don't run on replicas nor unknown role servers switch (_serverRegistrar.GetCurrentServerRole()) { case ServerRole.Replica: _logger.LogDebug("Does not run on replica servers."); - return true; // role may change! + return; case ServerRole.Unknown: _logger.LogDebug("Does not run on servers with unknown role."); - return true; // role may 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; } using (_profilingLogger.DebugDuration("Keep alive executing", "Keep alive complete")) @@ -68,24 +71,21 @@ namespace Umbraco.Web.Scheduling if (umbracoAppUrl.IsNullOrWhiteSpace()) { _logger.LogWarning("No umbracoApplicationUrl for service (yet), skip."); - return true; // repeat + return; } keepAlivePingUrl = keepAlivePingUrl.Replace("{umbracoApplicationUrl}", umbracoAppUrl.TrimEnd('/')); } var request = new HttpRequestMessage(HttpMethod.Get, keepAlivePingUrl); - var result = await _httpClient.SendAsync(request, token); + var httpClient = _httpClientFactory.CreateClient(); + await httpClient.SendAsync(request); } catch (Exception ex) { _logger.LogError(ex, "Keep alive failed (at '{keepAlivePingUrl}').", keepAlivePingUrl); } } - - return true; // repeat } - - public override bool IsAsync => true; } } diff --git a/src/Umbraco.Infrastructure/Scheduling/SchedulerComponent.cs b/src/Umbraco.Infrastructure/Scheduling/SchedulerComponent.cs index 49999c9c56..af7ca33256 100644 --- a/src/Umbraco.Infrastructure/Scheduling/SchedulerComponent.cs +++ b/src/Umbraco.Infrastructure/Scheduling/SchedulerComponent.cs @@ -8,13 +8,11 @@ using Microsoft.Extensions.Options; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Configuration.Models; -using Umbraco.Core.HealthCheck; using Umbraco.Core.Hosting; using Umbraco.Core.Logging; using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Sync; -using Umbraco.Web.HealthCheck; using Umbraco.Web.Routing; namespace Umbraco.Web.Scheduling @@ -41,14 +39,11 @@ namespace Umbraco.Web.Scheduling private readonly IRequestAccessor _requestAccessor; private readonly IBackofficeSecurityFactory _backofficeSecurityFactory; private readonly LoggingSettings _loggingSettings; - private readonly KeepAliveSettings _keepAliveSettings; private readonly IHostingEnvironment _hostingEnvironment; - private BackgroundTaskRunner _keepAliveRunner; private BackgroundTaskRunner _publishingRunner; private BackgroundTaskRunner _scrubberRunner; private BackgroundTaskRunner _fileCleanupRunner; - private BackgroundTaskRunner _healthCheckRunner; private bool _started; private object _locker = new object(); @@ -59,7 +54,7 @@ namespace Umbraco.Web.Scheduling IScopeProvider scopeProvider, IUmbracoContextFactory umbracoContextFactory, IProfilingLogger profilingLogger, ILoggerFactory loggerFactory, IApplicationShutdownRegistry applicationShutdownRegistry, IServerMessenger serverMessenger, IRequestAccessor requestAccessor, - IOptions loggingSettings, IOptions keepAliveSettings, + IOptions loggingSettings, IHostingEnvironment hostingEnvironment, IBackofficeSecurityFactory backofficeSecurityFactory) { @@ -78,7 +73,6 @@ namespace Umbraco.Web.Scheduling _requestAccessor = requestAccessor; _backofficeSecurityFactory = backofficeSecurityFactory; _loggingSettings = loggingSettings.Value; - _keepAliveSettings = keepAliveSettings.Value; _hostingEnvironment = hostingEnvironment; } @@ -86,7 +80,6 @@ namespace Umbraco.Web.Scheduling { var logger = _loggerFactory.CreateLogger>(); // backgrounds runners are web aware, if the app domain dies, these tasks will wind down correctly - _keepAliveRunner = new BackgroundTaskRunner("KeepAlive", logger, _applicationShutdownRegistry); _publishingRunner = new BackgroundTaskRunner("ScheduledPublishing", logger, _applicationShutdownRegistry); _scrubberRunner = new BackgroundTaskRunner("LogScrubber", logger, _applicationShutdownRegistry); _fileCleanupRunner = new BackgroundTaskRunner("TempFileCleanup", logger, _applicationShutdownRegistry); @@ -120,11 +113,6 @@ namespace Umbraco.Web.Scheduling var tasks = new List(); - if (_keepAliveSettings.DisableKeepAliveTask == false) - { - tasks.Add(RegisterKeepAlive(_keepAliveSettings)); - } - tasks.Add(RegisterScheduledPublishing()); tasks.Add(RegisterLogScrubber(_loggingSettings)); tasks.Add(RegisterTempFileCleanup()); @@ -133,15 +121,6 @@ namespace Umbraco.Web.Scheduling }); } - private IBackgroundTask RegisterKeepAlive(KeepAliveSettings keepAliveSettings) - { - // ping/keepalive - // on all servers - var task = new KeepAlive(_keepAliveRunner, DefaultDelayMilliseconds, FiveMinuteMilliseconds, _requestAccessor, _mainDom, Options.Create(keepAliveSettings), _loggerFactory.CreateLogger(), _profilingLogger, _serverRegistrar); - _keepAliveRunner.TryAdd(task); - return task; - } - private IBackgroundTask RegisterScheduledPublishing() { // scheduled publishing/unpublishing diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 2343ea806a..54ac7817e1 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -14,9 +14,10 @@ - + + diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/HealthCheckNotifierTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/HealthCheckNotifierTests.cs index 6506c227fc..372b94d9dd 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/HealthCheckNotifierTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/HealthCheckNotifierTests.cs @@ -23,16 +23,16 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HostedServices { private Mock _mockNotificationMethod; - private const string Check1Id = "00000000-0000-0000-0000-000000000001"; - private const string Check2Id = "00000000-0000-0000-0000-000000000002"; - private const string Check3Id = "00000000-0000-0000-0000-000000000003"; + private const string _check1Id = "00000000-0000-0000-0000-000000000001"; + private const string _check2Id = "00000000-0000-0000-0000-000000000002"; + private const string _check3Id = "00000000-0000-0000-0000-000000000003"; [Test] public void Does_Not_Execute_When_Not_Enabled() { var sut = CreateHealthCheckNotifier(enabled: false); sut.ExecuteAsync(null); - _mockNotificationMethod.Verify(x => x.SendAsync(It.IsAny()), Times.Never); + VerifyNotificationsNotSent(); } [Test] @@ -40,7 +40,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HostedServices { var sut = CreateHealthCheckNotifier(runtimeLevel: RuntimeLevel.Boot); sut.ExecuteAsync(null); - _mockNotificationMethod.Verify(x => x.SendAsync(It.IsAny()), Times.Never); + VerifyNotificationsNotSent(); } [Test] @@ -48,7 +48,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HostedServices { var sut = CreateHealthCheckNotifier(serverRole: ServerRole.Replica); sut.ExecuteAsync(null); - _mockNotificationMethod.Verify(x => x.SendAsync(It.IsAny()), Times.Never); + VerifyNotificationsNotSent(); } [Test] @@ -56,7 +56,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HostedServices { var sut = CreateHealthCheckNotifier(serverRole: ServerRole.Unknown); sut.ExecuteAsync(null); - _mockNotificationMethod.Verify(x => x.SendAsync(It.IsAny()), Times.Never); + VerifyNotificationsNotSent(); } [Test] @@ -64,7 +64,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HostedServices { var sut = CreateHealthCheckNotifier(isMainDom: false); sut.ExecuteAsync(null); - _mockNotificationMethod.Verify(x => x.SendAsync(It.IsAny()), Times.Never); + VerifyNotificationsNotSent(); } [Test] @@ -72,7 +72,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HostedServices { var sut = CreateHealthCheckNotifier(notificationEnabled: false); sut.ExecuteAsync(null); - _mockNotificationMethod.Verify(x => x.SendAsync(It.IsAny()), Times.Never); + VerifyNotificationsNotSent(); } [Test] @@ -80,7 +80,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HostedServices { var sut = CreateHealthCheckNotifier(); sut.ExecuteAsync(null); - _mockNotificationMethod.Verify(x => x.SendAsync(It.IsAny()), Times.Once); + VerifyNotificationsSent(); } [Test] @@ -106,12 +106,12 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HostedServices Enabled = enabled, DisabledChecks = new List { - new DisabledHealthCheckSettings { Id = Guid.Parse(Check3Id) } + new DisabledHealthCheckSettings { Id = Guid.Parse(_check3Id) } } }, DisabledChecks = new List { - new DisabledHealthCheckSettings { Id = Guid.Parse(Check2Id) } + new DisabledHealthCheckSettings { Id = Guid.Parse(_check2Id) } } }; var checks = new HealthCheckCollection(new List @@ -143,17 +143,32 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HostedServices mockLogger.Object, mockProfilingLogger.Object); } - [HealthCheck(Check1Id, "Check1")] + private void VerifyNotificationsNotSent() + { + VerifyNotificationsSentTimes(Times.Never()); + } + + private void VerifyNotificationsSent() + { + VerifyNotificationsSentTimes(Times.Once()); + } + + private void VerifyNotificationsSentTimes(Times times) + { + _mockNotificationMethod.Verify(x => x.SendAsync(It.IsAny()), times); + } + + [HealthCheck(_check1Id, "Check1")] private class TestHealthCheck1 : TestHealthCheck { } - [HealthCheck(Check2Id, "Check2")] + [HealthCheck(_check2Id, "Check2")] private class TestHealthCheck2 : TestHealthCheck { } - [HealthCheck(Check3Id, "Check3")] + [HealthCheck(_check3Id, "Check3")] private class TestHealthCheck3 : TestHealthCheck { } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/KeepAliveTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/KeepAliveTests.cs new file mode 100644 index 0000000000..82ecffc683 --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/KeepAliveTests.cs @@ -0,0 +1,124 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Moq.Protected; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Logging; +using Umbraco.Core.Scoping; +using Umbraco.Core.Sync; +using Umbraco.Infrastructure.HostedServices; +using Umbraco.Web; + +namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HostedServices +{ + [TestFixture] + public class KeepAliveTests + { + private Mock _mockHttpMessageHandler; + + private const string _applicationUrl = "https://mysite.com"; + + [Test] + public void Does_Not_Execute_When_Not_Enabled() + { + var sut = CreateKeepAlive(enabled: false); + sut.ExecuteAsync(null); + VerifyKeepAliveRequestNotSent(); + } + + [Test] + public void Does_Not_Execute_When_Server_Role_Is_Replica() + { + var sut = CreateKeepAlive(serverRole: ServerRole.Replica); + sut.ExecuteAsync(null); + VerifyKeepAliveRequestNotSent(); + } + + [Test] + public void Does_Not_Execute_When_Server_Role_Is_Unknown() + { + var sut = CreateKeepAlive(serverRole: ServerRole.Unknown); + sut.ExecuteAsync(null); + VerifyKeepAliveRequestNotSent(); + } + + [Test] + public void Does_Not_Execute_When_Not_Main_Dom() + { + var sut = CreateKeepAlive(isMainDom: false); + sut.ExecuteAsync(null); + VerifyKeepAliveRequestNotSent(); + } + + [Test] + public void Executes_And_Calls_Ping_Url() + { + var sut = CreateKeepAlive(); + sut.ExecuteAsync(null); + VerifyKeepAliveRequestSent(); + } + + private KeepAlive CreateKeepAlive( + bool enabled = true, + ServerRole serverRole = ServerRole.Single, + bool isMainDom = true) + { + var settings = new KeepAliveSettings + { + DisableKeepAliveTask = !enabled, + }; + + var mockRequestAccessor = new Mock(); + mockRequestAccessor.Setup(x => x.GetApplicationUrl()).Returns(new Uri(_applicationUrl)); + + var mockServerRegistrar = new Mock(); + mockServerRegistrar.Setup(x => x.GetCurrentServerRole()).Returns(serverRole); + + var mockMainDom = new Mock(); + mockMainDom.SetupGet(x => x.IsMainDom).Returns(isMainDom); + + var mockScopeProvider = new Mock(); + var mockLogger = new Mock>(); + var mockProfilingLogger = new Mock(); + + _mockHttpMessageHandler = new Mock(); + _mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)) + .Verifiable(); + _mockHttpMessageHandler.As().Setup(s => s.Dispose()); + var httpClient = new HttpClient(_mockHttpMessageHandler.Object); + + var mockHttpClientFactory = new Mock(MockBehavior.Strict); + mockHttpClientFactory.Setup(x => x.CreateClient(It.IsAny())).Returns(httpClient); + + return new KeepAlive(mockRequestAccessor.Object, mockMainDom.Object, Options.Create(settings), + mockLogger.Object, mockProfilingLogger.Object, mockServerRegistrar.Object, mockHttpClientFactory.Object); + } + + private void VerifyKeepAliveRequestNotSent() + { + VerifyKeepAliveRequestSentTimes(Times.Never()); + } + + private void VerifyKeepAliveRequestSent() + { + VerifyKeepAliveRequestSentTimes(Times.Once()); + } + + private void VerifyKeepAliveRequestSentTimes(Times times) + { + _mockHttpMessageHandler.Protected().Verify("SendAsync", + times, + ItExpr.Is(x => x.RequestUri.ToString() == $"{_applicationUrl}/api/keepalive/ping"), + ItExpr.IsAny()); + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs index 649b8b76b7..c494425274 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs @@ -17,7 +17,8 @@ namespace Umbraco.Extensions .WithMvcAndRazor() .WithWebServer() .WithPreview() - .WithHostedServices(); + .WithHostedServices() + .WithHttpClients(); } public static IUmbracoBuilder WithBackOffice(this IUmbracoBuilder builder) diff --git a/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs index 2ac4130fe4..635594a77d 100644 --- a/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Builder/UmbracoBuilderExtensions.cs @@ -36,6 +36,9 @@ namespace Umbraco.Web.Common.Builder public static IUmbracoBuilder WithHostedServices(this IUmbracoBuilder builder) => builder.AddWith(nameof(WithHostedServices), () => builder.Services.AddUmbracoHostedServices()); + public static IUmbracoBuilder WithHttpClients(this IUmbracoBuilder builder) + => builder.AddWith(nameof(WithHttpClients), () => builder.Services.AddUmbracoHttpClients()); + public static IUmbracoBuilder WithMiniProfiler(this IUmbracoBuilder builder) => builder.AddWith(nameof(WithMiniProfiler), () => builder.Services.AddMiniProfiler(options => diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs index 704a7ce066..2f82f86ba4 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs @@ -293,7 +293,18 @@ namespace Umbraco.Extensions public static IServiceCollection AddUmbracoHostedServices(this IServiceCollection services) { services.AddHostedService(); + services.AddHostedService(); + return services; + } + /// + /// Adds HTTP clients for Umbraco. + /// + /// + /// + public static IServiceCollection AddUmbracoHttpClients(this IServiceCollection services) + { + services.AddHttpClient(); return services; }