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;
}