merge v10 to v11

This commit is contained in:
Bjarke Berg
2022-08-18 14:38:28 +02:00
4076 changed files with 320268 additions and 303657 deletions

View File

@@ -1,42 +1,37 @@
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
namespace Umbraco.Cms.Infrastructure.HostedServices
namespace Umbraco.Cms.Infrastructure.HostedServices;
/// <summary>
/// A Background Task Queue, to enqueue tasks for executing in the background.
/// </summary>
/// <remarks>
/// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0
/// </remarks>
public class BackgroundTaskQueue : IBackgroundTaskQueue
{
/// <summary>
/// A Background Task Queue, to enqueue tasks for executing in the background.
/// </summary>
/// <remarks>
/// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0
/// </remarks>
public class BackgroundTaskQueue : IBackgroundTaskQueue
private readonly SemaphoreSlim _signal = new(0);
private readonly ConcurrentQueue<Func<CancellationToken, Task>> _workItems = new();
/// <inheritdoc />
public void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem)
{
private readonly ConcurrentQueue<Func<CancellationToken, Task>> _workItems =
new ConcurrentQueue<Func<CancellationToken, Task>>();
private readonly SemaphoreSlim _signal = new SemaphoreSlim(0);
/// <inheritdoc/>
public void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem)
if (workItem == null)
{
if (workItem == null)
{
throw new ArgumentNullException(nameof(workItem));
}
_workItems.Enqueue(workItem);
_signal.Release();
throw new ArgumentNullException(nameof(workItem));
}
/// <inheritdoc/>
public async Task<Func<CancellationToken, Task>?> DequeueAsync(CancellationToken cancellationToken)
{
await _signal.WaitAsync(cancellationToken);
_workItems.TryDequeue(out Func<CancellationToken, Task>? workItem);
_workItems.Enqueue(workItem);
_signal.Release();
}
return workItem;
}
/// <inheritdoc />
public async Task<Func<CancellationToken, Task>?> DequeueAsync(CancellationToken cancellationToken)
{
await _signal.WaitAsync(cancellationToken);
_workItems.TryDequeue(out Func<CancellationToken, Task>? workItem);
return workItem;
}
}

View File

@@ -1,5 +1,3 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
@@ -8,89 +6,89 @@ using Umbraco.Cms.Core.Runtime;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Infrastructure.HostedServices
namespace Umbraco.Cms.Infrastructure.HostedServices;
/// <summary>
/// Recurring hosted service that executes the content history cleanup.
/// </summary>
public class ContentVersionCleanup : RecurringHostedServiceBase
{
private readonly ILogger<ContentVersionCleanup> _logger;
private readonly IMainDom _mainDom;
private readonly IRuntimeState _runtimeState;
private readonly IServerRoleAccessor _serverRoleAccessor;
private readonly IContentVersionService _service;
private readonly IOptionsMonitor<ContentSettings> _settingsMonitor;
/// <summary>
/// Recurring hosted service that executes the content history cleanup.
/// Initializes a new instance of the <see cref="ContentVersionCleanup" /> class.
/// </summary>
public class ContentVersionCleanup : RecurringHostedServiceBase
public ContentVersionCleanup(
IRuntimeState runtimeState,
ILogger<ContentVersionCleanup> logger,
IOptionsMonitor<ContentSettings> settingsMonitor,
IContentVersionService service,
IMainDom mainDom,
IServerRoleAccessor serverRoleAccessor)
: base(logger, TimeSpan.FromHours(1), TimeSpan.FromMinutes(3))
{
private readonly IRuntimeState _runtimeState;
private readonly ILogger<ContentVersionCleanup> _logger;
private readonly IOptionsMonitor<ContentSettings> _settingsMonitor;
private readonly IContentVersionService _service;
private readonly IMainDom _mainDom;
private readonly IServerRoleAccessor _serverRoleAccessor;
_runtimeState = runtimeState;
_logger = logger;
_settingsMonitor = settingsMonitor;
_service = service;
_mainDom = mainDom;
_serverRoleAccessor = serverRoleAccessor;
}
/// <summary>
/// Initializes a new instance of the <see cref="ContentVersionCleanup"/> class.
/// </summary>
public ContentVersionCleanup(
IRuntimeState runtimeState,
ILogger<ContentVersionCleanup> logger,
IOptionsMonitor<ContentSettings> settingsMonitor,
IContentVersionService service,
IMainDom mainDom,
IServerRoleAccessor serverRoleAccessor)
: base(logger, TimeSpan.FromHours(1), TimeSpan.FromMinutes(3))
/// <inheritdoc />
public override Task PerformExecuteAsync(object? state)
{
// Globally disabled by feature flag
if (!_settingsMonitor.CurrentValue.ContentVersionCleanupPolicy.EnableCleanup)
{
_runtimeState = runtimeState;
_logger = logger;
_settingsMonitor = settingsMonitor;
_service = service;
_mainDom = mainDom;
_serverRoleAccessor = serverRoleAccessor;
_logger.LogInformation(
"ContentVersionCleanup task will not run as it has been globally disabled via configuration");
return Task.CompletedTask;
}
/// <inheritdoc />
public override Task PerformExecuteAsync(object? state)
if (_runtimeState.Level != RuntimeLevel.Run)
{
// Globally disabled by feature flag
if (!_settingsMonitor.CurrentValue.ContentVersionCleanupPolicy.EnableCleanup)
{
_logger.LogInformation("ContentVersionCleanup task will not run as it has been globally disabled via configuration");
return Task.FromResult(true); // repeat...
}
// Don't run on replicas nor unknown role servers
switch (_serverRoleAccessor.CurrentServerRole)
{
case ServerRole.Subscriber:
_logger.LogDebug("Does not run on subscriber servers");
return Task.CompletedTask;
}
if (_runtimeState.Level != RuntimeLevel.Run)
{
return Task.FromResult(true); // repeat...
}
// Don't run on replicas nor unknown role servers
switch (_serverRoleAccessor.CurrentServerRole)
{
case ServerRole.Subscriber:
_logger.LogDebug("Does not run on subscriber servers");
return Task.CompletedTask;
case ServerRole.Unknown:
_logger.LogDebug("Does not run on servers with unknown role");
return Task.CompletedTask;
case ServerRole.Single:
case ServerRole.SchedulingPublisher:
default:
break;
}
// Ensure we do not run if not main domain, but do NOT lock it
if (!_mainDom.IsMainDom)
{
_logger.LogDebug("Does not run if not MainDom");
return Task.FromResult(false); // do NOT repeat, going down
}
var count = _service.PerformContentVersionCleanup(DateTime.Now).Count;
if (count > 0)
{
_logger.LogInformation("Deleted {count} ContentVersion(s)", count);
}
else
{
_logger.LogDebug("Task complete, no items were Deleted");
}
return Task.FromResult(true);
case ServerRole.Unknown:
_logger.LogDebug("Does not run on servers with unknown role");
return Task.CompletedTask;
case ServerRole.Single:
case ServerRole.SchedulingPublisher:
default:
break;
}
// Ensure we do not run if not main domain, but do NOT lock it
if (!_mainDom.IsMainDom)
{
_logger.LogDebug("Does not run if not MainDom");
return Task.FromResult(false); // do NOT repeat, going down
}
var count = _service.PerformContentVersionCleanup(DateTime.Now).Count;
if (count > 0)
{
_logger.LogInformation("Deleted {count} ContentVersion(s)", count);
}
else
{
_logger.LogDebug("Task complete, no items were Deleted");
}
return Task.FromResult(true);
}
}

View File

@@ -1,10 +1,6 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
@@ -19,123 +15,122 @@ using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Sync;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.HostedServices
namespace Umbraco.Cms.Infrastructure.HostedServices;
/// <summary>
/// Hosted service implementation for recurring health check notifications.
/// </summary>
public class HealthCheckNotifier : RecurringHostedServiceBase
{
private readonly HealthCheckCollection _healthChecks;
private readonly ILogger<HealthCheckNotifier> _logger;
private readonly IMainDom _mainDom;
private readonly HealthCheckNotificationMethodCollection _notifications;
private readonly IProfilingLogger _profilingLogger;
private readonly IRuntimeState _runtimeState;
private readonly ICoreScopeProvider _scopeProvider;
private readonly IServerRoleAccessor _serverRegistrar;
private HealthChecksSettings _healthChecksSettings;
/// <summary>
/// Hosted service implementation for recurring health check notifications.
/// Initializes a new instance of the <see cref="HealthCheckNotifier" /> class.
/// </summary>
public class HealthCheckNotifier : RecurringHostedServiceBase
/// <param name="healthChecksSettings">The configuration for health check settings.</param>
/// <param name="healthChecks">The collection of healthchecks.</param>
/// <param name="notifications">The collection of healthcheck notification methods.</param>
/// <param name="runtimeState">Representation of the state of the Umbraco runtime.</param>
/// <param name="serverRegistrar">Provider of server registrations to the distributed cache.</param>
/// <param name="mainDom">Representation of the main application domain.</param>
/// <param name="scopeProvider">Provides scopes for database operations.</param>
/// <param name="logger">The typed logger.</param>
/// <param name="profilingLogger">The profiling logger.</param>
/// <param name="cronTabParser">Parser of crontab expressions.</param>
public HealthCheckNotifier(
IOptionsMonitor<HealthChecksSettings> healthChecksSettings,
HealthCheckCollection healthChecks,
HealthCheckNotificationMethodCollection notifications,
IRuntimeState runtimeState,
IServerRoleAccessor serverRegistrar,
IMainDom mainDom,
ICoreScopeProvider scopeProvider,
ILogger<HealthCheckNotifier> logger,
IProfilingLogger profilingLogger,
ICronTabParser cronTabParser)
: base(
logger,
healthChecksSettings.CurrentValue.Notification.Period,
GetDelay(healthChecksSettings.CurrentValue.Notification.FirstRunTime, cronTabParser, logger, DefaultDelay))
{
private HealthChecksSettings _healthChecksSettings;
private readonly HealthCheckCollection _healthChecks;
private readonly HealthCheckNotificationMethodCollection _notifications;
private readonly IRuntimeState _runtimeState;
private readonly IServerRoleAccessor _serverRegistrar;
private readonly IMainDom _mainDom;
private readonly ICoreScopeProvider _scopeProvider;
private readonly ILogger<HealthCheckNotifier> _logger;
private readonly IProfilingLogger _profilingLogger;
_healthChecksSettings = healthChecksSettings.CurrentValue;
_healthChecks = healthChecks;
_notifications = notifications;
_runtimeState = runtimeState;
_serverRegistrar = serverRegistrar;
_mainDom = mainDom;
_scopeProvider = scopeProvider;
_logger = logger;
_profilingLogger = profilingLogger;
/// <summary>
/// Initializes a new instance of the <see cref="HealthCheckNotifier"/> class.
/// </summary>
/// <param name="healthChecksSettings">The configuration for health check settings.</param>
/// <param name="healthChecks">The collection of healthchecks.</param>
/// <param name="notifications">The collection of healthcheck notification methods.</param>
/// <param name="runtimeState">Representation of the state of the Umbraco runtime.</param>
/// <param name="serverRegistrar">Provider of server registrations to the distributed cache.</param>
/// <param name="mainDom">Representation of the main application domain.</param>
/// <param name="scopeProvider">Provides scopes for database operations.</param>
/// <param name="logger">The typed logger.</param>
/// <param name="profilingLogger">The profiling logger.</param>
/// <param name="cronTabParser">Parser of crontab expressions.</param>
public HealthCheckNotifier(
IOptionsMonitor<HealthChecksSettings> healthChecksSettings,
HealthCheckCollection healthChecks,
HealthCheckNotificationMethodCollection notifications,
IRuntimeState runtimeState,
IServerRoleAccessor serverRegistrar,
IMainDom mainDom,
ICoreScopeProvider scopeProvider,
ILogger<HealthCheckNotifier> logger,
IProfilingLogger profilingLogger,
ICronTabParser cronTabParser)
: base(
logger,
healthChecksSettings.CurrentValue.Notification.Period,
healthChecksSettings.CurrentValue.GetNotificationDelay(cronTabParser, DateTime.Now, DefaultDelay))
healthChecksSettings.OnChange(x =>
{
_healthChecksSettings = healthChecksSettings.CurrentValue;
_healthChecks = healthChecks;
_notifications = notifications;
_runtimeState = runtimeState;
_serverRegistrar = serverRegistrar;
_mainDom = mainDom;
_scopeProvider = scopeProvider;
_logger = logger;
_profilingLogger = profilingLogger;
_healthChecksSettings = x;
ChangePeriod(x.Notification.Period);
});
}
healthChecksSettings.OnChange(x =>
{
_healthChecksSettings = x;
ChangePeriod(x.Notification.Period);
});
public override async Task PerformExecuteAsync(object? state)
{
if (_healthChecksSettings.Notification.Enabled == false)
{
return;
}
public override async Task PerformExecuteAsync(object? state)
if (_runtimeState.Level != RuntimeLevel.Run)
{
if (_healthChecksSettings.Notification.Enabled == false)
{
return;
}
switch (_serverRegistrar.CurrentServerRole)
{
case ServerRole.Subscriber:
_logger.LogDebug("Does not run on subscriber servers.");
return;
}
if (_runtimeState.Level != RuntimeLevel.Run)
{
case ServerRole.Unknown:
_logger.LogDebug("Does not run on servers with unknown role.");
return;
}
}
switch (_serverRegistrar.CurrentServerRole)
// 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;
}
// Ensure we use an explicit scope since we are running on a background thread and plugin health
// checks can be making service/database calls so we want to ensure the CallContext/Ambient scope
// isn't used since that can be problematic.
using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true))
using (_profilingLogger.DebugDuration<HealthCheckNotifier>("Health checks executing", "Health checks complete"))
{
// Don't notify for any checks that are disabled, nor for any disabled just for notifications.
Guid[] disabledCheckIds = _healthChecksSettings.Notification.DisabledChecks
.Select(x => x.Id)
.Union(_healthChecksSettings.DisabledChecks
.Select(x => x.Id))
.Distinct()
.ToArray();
IEnumerable<HealthCheck> checks = _healthChecks
.Where(x => disabledCheckIds.Contains(x.Id) == false);
HealthCheckResults results = await HealthCheckResults.Create(checks);
results.LogResults();
// Send using registered notification methods that are enabled.
foreach (IHealthCheckNotificationMethod notificationMethod in _notifications.Where(x => x.Enabled))
{
case ServerRole.Subscriber:
_logger.LogDebug("Does not run on subscriber servers.");
return;
case ServerRole.Unknown:
_logger.LogDebug("Does not run on servers with unknown role.");
return;
}
// 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;
}
// Ensure we use an explicit scope since we are running on a background thread and plugin health
// checks can be making service/database calls so we want to ensure the CallContext/Ambient scope
// isn't used since that can be problematic.
using (ICoreScope scope = _scopeProvider.CreateCoreScope())
using (_profilingLogger.DebugDuration<HealthCheckNotifier>("Health checks executing", "Health checks complete"))
{
// Don't notify for any checks that are disabled, nor for any disabled just for notifications.
Guid[] disabledCheckIds = _healthChecksSettings.Notification.DisabledChecks
.Select(x => x.Id)
.Union(_healthChecksSettings.DisabledChecks
.Select(x => x.Id))
.Distinct()
.ToArray();
IEnumerable<HealthCheck> checks = _healthChecks
.Where(x => disabledCheckIds.Contains(x.Id) == false);
var results = await HealthCheckResults.Create(checks);
results.LogResults();
// Send using registered notification methods that are enabled.
foreach (IHealthCheckNotificationMethod notificationMethod in _notifications.Where(x => x.Enabled))
{
await notificationMethod.SendAsync(results);
}
await notificationMethod.SendAsync(results);
}
}
}

View File

@@ -1,25 +1,20 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Umbraco.Cms.Infrastructure.HostedServices;
namespace Umbraco.Cms.Infrastructure.HostedServices
/// <summary>
/// A Background Task Queue, to enqueue tasks for executing in the background.
/// </summary>
/// <remarks>
/// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0
/// </remarks>
public interface IBackgroundTaskQueue
{
/// <summary>
/// A Background Task Queue, to enqueue tasks for executing in the background.
/// Enqueue a work item to be executed on in the background.
/// </summary>
/// <remarks>
/// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0
/// </remarks>
public interface IBackgroundTaskQueue
{
/// <summary>
/// Enqueue a work item to be executed on in the background.
/// </summary>
void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
/// <summary>
/// Dequeue the first item on the queue.
/// </summary>
Task<Func<CancellationToken, Task>?> DequeueAsync(CancellationToken cancellationToken);
}
/// <summary>
/// Dequeue the first item on the queue.
/// </summary>
Task<Func<CancellationToken, Task>?> DequeueAsync(CancellationToken cancellationToken);
}

View File

@@ -1,10 +1,6 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
@@ -16,99 +12,100 @@ using Umbraco.Cms.Core.Runtime;
using Umbraco.Cms.Core.Sync;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.HostedServices
namespace Umbraco.Cms.Infrastructure.HostedServices;
/// <summary>
/// Hosted service implementation for keep alive feature.
/// </summary>
public class KeepAlive : RecurringHostedServiceBase
{
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<KeepAlive> _logger;
private readonly IMainDom _mainDom;
private readonly IProfilingLogger _profilingLogger;
private readonly IServerRoleAccessor _serverRegistrar;
private KeepAliveSettings _keepAliveSettings;
/// <summary>
/// Hosted service implementation for keep alive feature.
/// Initializes a new instance of the <see cref="KeepAlive" /> class.
/// </summary>
public class KeepAlive : RecurringHostedServiceBase
/// <param name="hostingEnvironment">The current hosting environment</param>
/// <param name="mainDom">Representation of the main application domain.</param>
/// <param name="keepAliveSettings">The configuration for keep alive settings.</param>
/// <param name="logger">The typed logger.</param>
/// <param name="profilingLogger">The profiling logger.</param>
/// <param name="serverRegistrar">Provider of server registrations to the distributed cache.</param>
/// <param name="httpClientFactory">Factory for <see cref="HttpClient" /> instances.</param>
public KeepAlive(
IHostingEnvironment hostingEnvironment,
IMainDom mainDom,
IOptionsMonitor<KeepAliveSettings> keepAliveSettings,
ILogger<KeepAlive> logger,
IProfilingLogger profilingLogger,
IServerRoleAccessor serverRegistrar,
IHttpClientFactory httpClientFactory)
: base(logger, TimeSpan.FromMinutes(5), DefaultDelay)
{
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IMainDom _mainDom;
private KeepAliveSettings _keepAliveSettings;
private readonly ILogger<KeepAlive> _logger;
private readonly IProfilingLogger _profilingLogger;
private readonly IServerRoleAccessor _serverRegistrar;
private readonly IHttpClientFactory _httpClientFactory;
_hostingEnvironment = hostingEnvironment;
_mainDom = mainDom;
_keepAliveSettings = keepAliveSettings.CurrentValue;
_logger = logger;
_profilingLogger = profilingLogger;
_serverRegistrar = serverRegistrar;
_httpClientFactory = httpClientFactory;
/// <summary>
/// Initializes a new instance of the <see cref="KeepAlive"/> class.
/// </summary>
/// <param name="hostingEnvironment">The current hosting environment</param>
/// <param name="mainDom">Representation of the main application domain.</param>
/// <param name="keepAliveSettings">The configuration for keep alive settings.</param>
/// <param name="logger">The typed logger.</param>
/// <param name="profilingLogger">The profiling logger.</param>
/// <param name="serverRegistrar">Provider of server registrations to the distributed cache.</param>
/// <param name="httpClientFactory">Factory for <see cref="HttpClient" /> instances.</param>
public KeepAlive(
IHostingEnvironment hostingEnvironment,
IMainDom mainDom,
IOptionsMonitor<KeepAliveSettings> keepAliveSettings,
ILogger<KeepAlive> logger,
IProfilingLogger profilingLogger,
IServerRoleAccessor serverRegistrar,
IHttpClientFactory httpClientFactory)
: base(logger, TimeSpan.FromMinutes(5), DefaultDelay)
keepAliveSettings.OnChange(x => _keepAliveSettings = x);
}
public override async Task PerformExecuteAsync(object? state)
{
if (_keepAliveSettings.DisableKeepAliveTask)
{
_hostingEnvironment = hostingEnvironment;
_mainDom = mainDom;
_keepAliveSettings = keepAliveSettings.CurrentValue;
_logger = logger;
_profilingLogger = profilingLogger;
_serverRegistrar = serverRegistrar;
_httpClientFactory = httpClientFactory;
keepAliveSettings.OnChange(x => _keepAliveSettings = x);
return;
}
public override async Task PerformExecuteAsync(object? state)
// Don't run on replicas nor unknown role servers
switch (_serverRegistrar.CurrentServerRole)
{
if (_keepAliveSettings.DisableKeepAliveTask)
case ServerRole.Subscriber:
_logger.LogDebug("Does not run on subscriber servers.");
return;
case ServerRole.Unknown:
_logger.LogDebug("Does not run on servers with unknown role.");
return;
}
// 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;
}
using (_profilingLogger.DebugDuration<KeepAlive>("Keep alive executing", "Keep alive complete"))
{
var umbracoAppUrl = _hostingEnvironment.ApplicationMainUrl?.ToString();
if (umbracoAppUrl.IsNullOrWhiteSpace())
{
_logger.LogWarning("No umbracoApplicationUrl for service (yet), skip.");
return;
}
// Don't run on replicas nor unknown role servers
switch (_serverRegistrar.CurrentServerRole)
// If the config is an absolute path, just use it
var keepAlivePingUrl = WebPath.Combine(
umbracoAppUrl!,
_hostingEnvironment.ToAbsolute(_keepAliveSettings.KeepAlivePingUrl));
try
{
case ServerRole.Subscriber:
_logger.LogDebug("Does not run on subscriber servers.");
return;
case ServerRole.Unknown:
_logger.LogDebug("Does not run on servers with unknown role.");
return;
var request = new HttpRequestMessage(HttpMethod.Get, keepAlivePingUrl);
HttpClient httpClient = _httpClientFactory.CreateClient(Constants.HttpClients.IgnoreCertificateErrors);
_ = await httpClient.SendAsync(request);
}
// Ensure we do not run if not main domain, but do NOT lock it
if (_mainDom.IsMainDom == false)
catch (Exception ex)
{
_logger.LogDebug("Does not run if not MainDom.");
return;
}
using (_profilingLogger.DebugDuration<KeepAlive>("Keep alive executing", "Keep alive complete"))
{
var umbracoAppUrl = _hostingEnvironment.ApplicationMainUrl?.ToString();
if (umbracoAppUrl.IsNullOrWhiteSpace())
{
_logger.LogWarning("No umbracoApplicationUrl for service (yet), skip.");
return;
}
// If the config is an absolute path, just use it
string keepAlivePingUrl = WebPath.Combine(umbracoAppUrl!, _hostingEnvironment.ToAbsolute(_keepAliveSettings.KeepAlivePingUrl));
try
{
var request = new HttpRequestMessage(HttpMethod.Get, keepAlivePingUrl);
HttpClient httpClient = _httpClientFactory.CreateClient(Constants.HttpClients.IgnoreCertificateErrors);
_ = await httpClient.SendAsync(request);
}
catch (Exception ex)
{
_logger.LogError(ex, "Keep alive failed (at '{keepAlivePingUrl}').", keepAlivePingUrl);
}
_logger.LogError(ex, "Keep alive failed (at '{keepAlivePingUrl}').", keepAlivePingUrl);
}
}
}

View File

@@ -1,8 +1,6 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
@@ -12,82 +10,81 @@ using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Infrastructure.HostedServices
{
/// <summary>
/// Log scrubbing hosted service.
/// </summary>
/// <remarks>
/// Will only run on non-replica servers.
/// </remarks>
public class LogScrubber : RecurringHostedServiceBase
{
private readonly IMainDom _mainDom;
private readonly IServerRoleAccessor _serverRegistrar;
private readonly IAuditService _auditService;
private LoggingSettings _settings;
private readonly IProfilingLogger _profilingLogger;
private readonly ILogger<LogScrubber> _logger;
private readonly ICoreScopeProvider _scopeProvider;
namespace Umbraco.Cms.Infrastructure.HostedServices;
/// <summary>
/// Initializes a new instance of the <see cref="LogScrubber"/> class.
/// </summary>
/// <param name="mainDom">Representation of the main application domain.</param>
/// <param name="serverRegistrar">Provider of server registrations to the distributed cache.</param>
/// <param name="auditService">Service for handling audit operations.</param>
/// <param name="settings">The configuration for logging settings.</param>
/// <param name="scopeProvider">Provides scopes for database operations.</param>
/// <param name="logger">The typed logger.</param>
/// <param name="profilingLogger">The profiling logger.</param>
public LogScrubber(
IMainDom mainDom,
IServerRoleAccessor serverRegistrar,
IAuditService auditService,
IOptionsMonitor<LoggingSettings> settings,
ICoreScopeProvider scopeProvider,
ILogger<LogScrubber> logger,
IProfilingLogger profilingLogger)
: base(logger, TimeSpan.FromHours(4), DefaultDelay)
/// <summary>
/// Log scrubbing hosted service.
/// </summary>
/// <remarks>
/// Will only run on non-replica servers.
/// </remarks>
public class LogScrubber : RecurringHostedServiceBase
{
private readonly IAuditService _auditService;
private readonly ILogger<LogScrubber> _logger;
private readonly IMainDom _mainDom;
private readonly IProfilingLogger _profilingLogger;
private readonly ICoreScopeProvider _scopeProvider;
private readonly IServerRoleAccessor _serverRegistrar;
private LoggingSettings _settings;
/// <summary>
/// Initializes a new instance of the <see cref="LogScrubber" /> class.
/// </summary>
/// <param name="mainDom">Representation of the main application domain.</param>
/// <param name="serverRegistrar">Provider of server registrations to the distributed cache.</param>
/// <param name="auditService">Service for handling audit operations.</param>
/// <param name="settings">The configuration for logging settings.</param>
/// <param name="scopeProvider">Provides scopes for database operations.</param>
/// <param name="logger">The typed logger.</param>
/// <param name="profilingLogger">The profiling logger.</param>
public LogScrubber(
IMainDom mainDom,
IServerRoleAccessor serverRegistrar,
IAuditService auditService,
IOptionsMonitor<LoggingSettings> settings,
ICoreScopeProvider scopeProvider,
ILogger<LogScrubber> logger,
IProfilingLogger profilingLogger)
: base(logger, TimeSpan.FromHours(4), DefaultDelay)
{
_mainDom = mainDom;
_serverRegistrar = serverRegistrar;
_auditService = auditService;
_settings = settings.CurrentValue;
_scopeProvider = scopeProvider;
_logger = logger;
_profilingLogger = profilingLogger;
settings.OnChange(x => _settings = x);
}
public override Task PerformExecuteAsync(object? state)
{
switch (_serverRegistrar.CurrentServerRole)
{
_mainDom = mainDom;
_serverRegistrar = serverRegistrar;
_auditService = auditService;
_settings = settings.CurrentValue;
_scopeProvider = scopeProvider;
_logger = logger;
_profilingLogger = profilingLogger;
settings.OnChange(x => _settings = x);
case ServerRole.Subscriber:
_logger.LogDebug("Does not run on subscriber servers.");
return Task.CompletedTask;
case ServerRole.Unknown:
_logger.LogDebug("Does not run on servers with unknown role.");
return Task.CompletedTask;
}
public override Task PerformExecuteAsync(object? state)
// Ensure we do not run if not main domain, but do NOT lock it
if (_mainDom.IsMainDom == false)
{
switch (_serverRegistrar.CurrentServerRole)
{
case ServerRole.Subscriber:
_logger.LogDebug("Does not run on subscriber servers.");
return Task.CompletedTask;
case ServerRole.Unknown:
_logger.LogDebug("Does not run on servers with unknown role.");
return Task.CompletedTask;
}
// 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 Task.CompletedTask;
}
// Ensure we use an explicit scope since we are running on a background thread.
using (ICoreScope scope = _scopeProvider.CreateCoreScope())
using (_profilingLogger.DebugDuration<LogScrubber>("Log scrubbing executing", "Log scrubbing complete"))
{
_auditService.CleanLogs((int)_settings.MaxLogAge.TotalMinutes);
_ = scope.Complete();
}
_logger.LogDebug("Does not run if not MainDom.");
return Task.CompletedTask;
}
// Ensure we use an explicit scope since we are running on a background thread.
using (ICoreScope scope = _scopeProvider.CreateCoreScope())
using (_profilingLogger.DebugDuration<LogScrubber>("Log scrubbing executing", "Log scrubbing complete"))
{
_auditService.CleanLogs((int)_settings.MaxLogAge.TotalMinutes);
_ = scope.Complete();
}
return Task.CompletedTask;
}
}

View File

@@ -1,62 +1,57 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Umbraco.Cms.Infrastructure.HostedServices
namespace Umbraco.Cms.Infrastructure.HostedServices;
/// <summary>
/// A queue based hosted service used to executing tasks on a background thread.
/// </summary>
/// <remarks>
/// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0
/// </remarks>
public class QueuedHostedService : BackgroundService
{
private readonly ILogger<QueuedHostedService> _logger;
/// <summary>
/// A queue based hosted service used to executing tasks on a background thread.
/// </summary>
/// <remarks>
/// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0
/// </remarks>
public class QueuedHostedService : BackgroundService
public QueuedHostedService(
IBackgroundTaskQueue taskQueue,
ILogger<QueuedHostedService> logger)
{
private readonly ILogger<QueuedHostedService> _logger;
TaskQueue = taskQueue;
_logger = logger;
}
public QueuedHostedService(IBackgroundTaskQueue taskQueue,
ILogger<QueuedHostedService> logger)
public IBackgroundTaskQueue TaskQueue { get; }
public override async Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Queued Hosted Service is stopping.");
await base.StopAsync(stoppingToken);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken) =>
await BackgroundProcessing(stoppingToken);
private async Task BackgroundProcessing(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
TaskQueue = taskQueue;
_logger = logger;
}
Func<CancellationToken, Task>? workItem = await TaskQueue.DequeueAsync(stoppingToken);
public IBackgroundTaskQueue TaskQueue { get; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await BackgroundProcessing(stoppingToken);
}
private async Task BackgroundProcessing(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
try
{
Func<CancellationToken, Task>? workItem = await TaskQueue.DequeueAsync(stoppingToken);
try
if (workItem is not null)
{
if (workItem is not null)
{
await workItem(stoppingToken);
}
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error occurred executing {WorkItem}.", nameof(workItem));
await workItem(stoppingToken);
}
}
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Queued Hosted Service is stopping.");
await base.StopAsync(stoppingToken);
catch (Exception ex)
{
_logger.LogError(
ex,
"Error occurred executing {WorkItem}.", nameof(workItem));
}
}
}
}

View File

@@ -1,122 +1,178 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration;
namespace Umbraco.Cms.Infrastructure.HostedServices
namespace Umbraco.Cms.Infrastructure.HostedServices;
/// <summary>
/// Provides a base class for recurring background tasks implemented as hosted services.
/// </summary>
/// <remarks>
/// See: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio#timed-background-tasks
/// </remarks>
public abstract class RecurringHostedServiceBase : IHostedService, IDisposable
{
/// <summary>
/// Provides a base class for recurring background tasks implemented as hosted services.
/// The default delay to use for recurring tasks for the first run after application start-up if no alternative is
/// configured.
/// </summary>
/// <remarks>
/// See: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio#timed-background-tasks
/// </remarks>
public abstract class RecurringHostedServiceBase : IHostedService, IDisposable
protected static readonly TimeSpan DefaultDelay = TimeSpan.FromMinutes(3);
private readonly TimeSpan _delay;
private readonly ILogger? _logger;
private bool _disposedValue;
private TimeSpan _period;
private Timer? _timer;
/// <summary>
/// Initializes a new instance of the <see cref="RecurringHostedServiceBase" /> class.
/// </summary>
/// <param name="logger">Logger.</param>
/// <param name="period">Timespan representing how often the task should recur.</param>
/// <param name="delay">
/// Timespan representing the initial delay after application start-up before the first run of the task
/// occurs.
/// </param>
protected RecurringHostedServiceBase(ILogger? logger, TimeSpan period, TimeSpan delay)
{
/// <summary>
/// The default delay to use for recurring tasks for the first run after application start-up if no alternative is configured.
/// </summary>
protected static readonly TimeSpan DefaultDelay = TimeSpan.FromMinutes(3);
_logger = logger;
_period = period;
_delay = delay;
}
private readonly ILogger? _logger;
private TimeSpan _period;
private readonly TimeSpan _delay;
private Timer? _timer;
private bool _disposedValue;
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Initializes a new instance of the <see cref="RecurringHostedServiceBase"/> class.
/// </summary>
/// <param name="logger">Logger.</param>
/// <param name="period">Timespan representing how often the task should recur.</param>
/// <param name="delay">Timespan representing the initial delay after application start-up before the first run of the task occurs.</param>
protected RecurringHostedServiceBase(ILogger? logger, TimeSpan period, TimeSpan delay)
/// <summary>
/// Determines the delay before the first run of a recurring task implemented as a hosted service when an optonal
/// configuration for the first run time is available.
/// </summary>
/// <param name="firstRunTime">The configured time to first run the task in crontab format.</param>
/// <param name="cronTabParser">An instance of <see cref="ICronTabParser"/></param>
/// <param name="logger">The logger.</param>
/// <param name="defaultDelay">The default delay to use when a first run time is not configured.</param>
/// <returns>The delay before first running the recurring task.</returns>
protected static TimeSpan GetDelay(
string firstRunTime,
ICronTabParser cronTabParser,
ILogger logger,
TimeSpan defaultDelay) => GetDelay(firstRunTime, cronTabParser, logger, DateTime.Now, defaultDelay);
/// <summary>
/// Determines the delay before the first run of a recurring task implemented as a hosted service when an optonal
/// configuration for the first run time is available.
/// </summary>
/// <param name="firstRunTime">The configured time to first run the task in crontab format.</param>
/// <param name="cronTabParser">An instance of <see cref="ICronTabParser"/></param>
/// <param name="logger">The logger.</param>
/// <param name="now">The current datetime.</param>
/// <param name="defaultDelay">The default delay to use when a first run time is not configured.</param>
/// <returns>The delay before first running the recurring task.</returns>
/// <remarks>Internal to expose for unit tests.</remarks>
internal static TimeSpan GetDelay(
string firstRunTime,
ICronTabParser cronTabParser,
ILogger logger,
DateTime now,
TimeSpan defaultDelay)
{
// If first run time not set, start with just small delay after application start.
if (string.IsNullOrEmpty(firstRunTime))
{
_logger = logger;
_period = period;
_delay = delay;
return defaultDelay;
}
/// <summary>
/// Change the period between operations.
/// </summary>
/// <param name="newPeriod">The new period between tasks</param>
protected void ChangePeriod(TimeSpan newPeriod) => _period = newPeriod;
/// <inheritdoc/>
public Task StartAsync(CancellationToken cancellationToken)
// If first run time not a valid cron tab, log, and revert to small delay after application start.
if (!cronTabParser.IsValidCronTab(firstRunTime))
{
using (!ExecutionContext.IsFlowSuppressed() ? (IDisposable)ExecutionContext.SuppressFlow() : null)
{
_timer = new Timer(ExecuteAsync, null, (int)_delay.TotalMilliseconds, (int)_period.TotalMilliseconds);
}
return Task.CompletedTask;
logger.LogWarning("Could not parse {FirstRunTime} as a crontab expression. Defaulting to default delay for hosted service start.", firstRunTime);
return defaultDelay;
}
/// <summary>
/// Executes the task.
/// </summary>
/// <param name="state">The task state.</param>
public async void ExecuteAsync(object? state)
{
try
{
// First, stop the timer, we do not want tasks to execute in parallel
_timer?.Change(Timeout.Infinite, 0);
// Otherwise start at scheduled time according to cron expression, unless within the default delay period.
DateTime firstRunOccurance = cronTabParser.GetNextOccurrence(firstRunTime, now);
TimeSpan delay = firstRunOccurance - now;
return delay < defaultDelay
? defaultDelay
: delay;
}
// Delegate work to method returning a task, that can be called and asserted in a unit test.
// Without this there can be behaviour where tests pass, but an error within them causes the test
// running process to crash.
// Hat-tip: https://stackoverflow.com/a/14207615/489433
await PerformExecuteAsync(state);
}
catch (Exception ex)
{
ILogger logger = _logger ?? StaticApplicationLogging.CreateLogger(GetType());
logger.LogError(ex, "Unhandled exception in recurring hosted service.");
}
finally
{
// Resume now that the task is complete - Note we use period in both because we don't want to execute again after the delay.
// So first execution is after _delay, and the we wait _period between each
_timer?.Change((int)_period.TotalMilliseconds, (int)_period.TotalMilliseconds);
}
/// <inheritdoc />
public Task StartAsync(CancellationToken cancellationToken)
{
using (!ExecutionContext.IsFlowSuppressed() ? (IDisposable)ExecutionContext.SuppressFlow() : null)
{
_timer = new Timer(ExecuteAsync, null, (int)_delay.TotalMilliseconds, (int)_period.TotalMilliseconds);
}
public abstract Task PerformExecuteAsync(object? state);
return Task.CompletedTask;
}
/// <inheritdoc/>
public Task StopAsync(CancellationToken cancellationToken)
/// <inheritdoc />
public Task StopAsync(CancellationToken cancellationToken)
{
_period = Timeout.InfiniteTimeSpan;
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
/// <summary>
/// Executes the task.
/// </summary>
/// <param name="state">The task state.</param>
public async void ExecuteAsync(object? state)
{
try
{
_period = Timeout.InfiniteTimeSpan;
// First, stop the timer, we do not want tasks to execute in parallel
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
protected virtual void Dispose(bool disposing)
// Delegate work to method returning a task, that can be called and asserted in a unit test.
// Without this there can be behaviour where tests pass, but an error within them causes the test
// running process to crash.
// Hat-tip: https://stackoverflow.com/a/14207615/489433
await PerformExecuteAsync(state);
}
catch (Exception ex)
{
if (!_disposedValue)
ILogger logger = _logger ?? StaticApplicationLogging.CreateLogger(GetType());
logger.LogError(ex, "Unhandled exception in recurring hosted service.");
}
finally
{
// Resume now that the task is complete - Note we use period in both because we don't want to execute again after the delay.
// So first execution is after _delay, and the we wait _period between each
_timer?.Change((int)_period.TotalMilliseconds, (int)_period.TotalMilliseconds);
}
}
public abstract Task PerformExecuteAsync(object? state);
/// <summary>
/// Change the period between operations.
/// </summary>
/// <param name="newPeriod">The new period between tasks</param>
protected void ChangePeriod(TimeSpan newPeriod) => _period = newPeriod;
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
if (disposing)
{
_timer?.Dispose();
}
_disposedValue = true;
_timer?.Dispose();
}
}
/// <inheritdoc/>
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
_disposedValue = true;
}
}
}

View File

@@ -1,83 +1,97 @@
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Telemetry;
using Umbraco.Cms.Core.Telemetry.Models;
using Umbraco.Cms.Web.Common.DependencyInjection;
namespace Umbraco.Cms.Infrastructure.HostedServices
{
public class ReportSiteTask : RecurringHostedServiceBase
{
private readonly ILogger<ReportSiteTask> _logger;
private readonly ITelemetryService _telemetryService;
private static HttpClient s_httpClient = new();
namespace Umbraco.Cms.Infrastructure.HostedServices;
public ReportSiteTask(
ILogger<ReportSiteTask> logger,
ITelemetryService telemetryService)
: base(logger, TimeSpan.FromDays(1), TimeSpan.FromMinutes(1))
{
_logger = logger;
_telemetryService = telemetryService;
s_httpClient = new HttpClient();
}
public class ReportSiteTask : RecurringHostedServiceBase
{
private static HttpClient _httpClient = new();
private readonly ILogger<ReportSiteTask> _logger;
private readonly ITelemetryService _telemetryService;
private readonly IRuntimeState _runtimeState;
public ReportSiteTask(
ILogger<ReportSiteTask> logger,
ITelemetryService telemetryService,
IRuntimeState runtimeState)
: base(logger, TimeSpan.FromDays(1), TimeSpan.FromMinutes(5))
{
_logger = logger;
_telemetryService = telemetryService;
_runtimeState = runtimeState;
_httpClient = new HttpClient();
}
[Obsolete("Use the constructor that takes IRuntimeState, scheduled for removal in V12")]
public ReportSiteTask(
ILogger<ReportSiteTask> logger,
ITelemetryService telemetryService)
: this(logger, telemetryService, StaticServiceProvider.Instance.GetRequiredService<IRuntimeState>())
{
}
/// <summary>
/// Runs the background task to send the anonymous ID
/// to telemetry service
/// </summary>
public override async Task PerformExecuteAsync(object? state)
public override async Task PerformExecuteAsync(object? state){
if (_runtimeState.Level is not RuntimeLevel.Run)
{
if (_telemetryService.TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData) is false)
{
_logger.LogWarning("No telemetry marker found");
// We probably haven't installed yet, so we can't get telemetry.
return;
}
return;
}
if (_telemetryService.TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData) is false)
{
_logger.LogWarning("No telemetry marker found");
try
return;
}
try
{
if (_httpClient.BaseAddress is null)
{
if (s_httpClient.BaseAddress is null)
{
// Send data to LIVE telemetry
s_httpClient.BaseAddress = new Uri("https://telemetry.umbraco.com/");
// Send data to LIVE telemetry
_httpClient.BaseAddress = new Uri("https://telemetry.umbraco.com/");
#if DEBUG
// Send data to DEBUG telemetry service
s_httpClient.BaseAddress = new Uri("https://telemetry.rainbowsrock.net/");
// Send data to DEBUG telemetry service
_httpClient.BaseAddress = new Uri("https://telemetry.rainbowsrock.net/");
#endif
}
s_httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");
using (var request = new HttpRequestMessage(HttpMethod.Post, "installs/"))
{
request.Content = new StringContent(JsonConvert.SerializeObject(telemetryReportData), Encoding.UTF8, "application/json"); //CONTENT-TYPE header
// Make a HTTP Post to telemetry service
// https://telemetry.umbraco.com/installs/
// Fire & Forget, do not need to know if its a 200, 500 etc
using (HttpResponseMessage response = await s_httpClient.SendAsync(request))
{
}
}
}
catch
_httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");
using (var request = new HttpRequestMessage(HttpMethod.Post, "installs/"))
{
// Silently swallow
// The user does not need the logs being polluted if our service has fallen over or is down etc
// Hence only logging this at a more verbose level (which users should not be using in production)
_logger.LogDebug("There was a problem sending a request to the Umbraco telemetry service");
request.Content = new StringContent(JsonConvert.SerializeObject(telemetryReportData), Encoding.UTF8,
"application/json");
// Make a HTTP Post to telemetry service
// https://telemetry.umbraco.com/installs/
// Fire & Forget, do not need to know if its a 200, 500 etc
using (await _httpClient.SendAsync(request))
{
}
}
}
catch
{
// Silently swallow
// The user does not need the logs being polluted if our service has fallen over or is down etc
// Hence only logging this at a more verbose level (which users should not be using in production)
_logger.LogDebug("There was a problem sending a request to the Umbraco telemetry service");
}
}
}

View File

@@ -1,11 +1,6 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Runtime;
@@ -13,129 +8,127 @@ using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Web.Common.DependencyInjection;
namespace Umbraco.Cms.Infrastructure.HostedServices
namespace Umbraco.Cms.Infrastructure.HostedServices;
/// <summary>
/// Hosted service implementation for scheduled publishing feature.
/// </summary>
/// <remarks>
/// Runs only on non-replica servers.
/// </remarks>
public class ScheduledPublishing : RecurringHostedServiceBase
{
private readonly IContentService _contentService;
private readonly ILogger<ScheduledPublishing> _logger;
private readonly IMainDom _mainDom;
private readonly IRuntimeState _runtimeState;
private readonly ICoreScopeProvider _scopeProvider;
private readonly IServerMessenger _serverMessenger;
private readonly IServerRoleAccessor _serverRegistrar;
private readonly IUmbracoContextFactory _umbracoContextFactory;
/// <summary>
/// Hosted service implementation for scheduled publishing feature.
/// Initializes a new instance of the <see cref="ScheduledPublishing" /> class.
/// </summary>
/// <remarks>
/// Runs only on non-replica servers.</remarks>
public class ScheduledPublishing : RecurringHostedServiceBase
public ScheduledPublishing(
IRuntimeState runtimeState,
IMainDom mainDom,
IServerRoleAccessor serverRegistrar,
IContentService contentService,
IUmbracoContextFactory umbracoContextFactory,
ILogger<ScheduledPublishing> logger,
IServerMessenger serverMessenger,
ICoreScopeProvider scopeProvider)
: base(logger, TimeSpan.FromMinutes(1), DefaultDelay)
{
private readonly IContentService _contentService;
private readonly ILogger<ScheduledPublishing> _logger;
private readonly IMainDom _mainDom;
private readonly IRuntimeState _runtimeState;
private readonly IServerMessenger _serverMessenger;
private readonly ICoreScopeProvider _scopeProvider;
private readonly IServerRoleAccessor _serverRegistrar;
private readonly IUmbracoContextFactory _umbracoContextFactory;
_runtimeState = runtimeState;
_mainDom = mainDom;
_serverRegistrar = serverRegistrar;
_contentService = contentService;
_umbracoContextFactory = umbracoContextFactory;
_logger = logger;
_serverMessenger = serverMessenger;
_scopeProvider = scopeProvider;
}
/// <summary>
/// Initializes a new instance of the <see cref="ScheduledPublishing"/> class.
/// </summary>
public ScheduledPublishing(
IRuntimeState runtimeState,
IMainDom mainDom,
IServerRoleAccessor serverRegistrar,
IContentService contentService,
IUmbracoContextFactory umbracoContextFactory,
ILogger<ScheduledPublishing> logger,
IServerMessenger serverMessenger,
ICoreScopeProvider scopeProvider)
: base(logger, TimeSpan.FromMinutes(1), DefaultDelay)
public override Task PerformExecuteAsync(object? state)
{
if (Suspendable.ScheduledPublishing.CanRun == false)
{
_runtimeState = runtimeState;
_mainDom = mainDom;
_serverRegistrar = serverRegistrar;
_contentService = contentService;
_umbracoContextFactory = umbracoContextFactory;
_logger = logger;
_serverMessenger = serverMessenger;
_scopeProvider = scopeProvider;
}
public override Task PerformExecuteAsync(object? state)
{
if (Suspendable.ScheduledPublishing.CanRun == false)
{
return Task.CompletedTask;
}
switch (_serverRegistrar.CurrentServerRole)
{
case ServerRole.Subscriber:
_logger.LogDebug("Does not run on subscriber servers.");
return Task.CompletedTask;
case ServerRole.Unknown:
_logger.LogDebug("Does not run on servers with unknown role.");
return Task.CompletedTask;
}
// 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 Task.CompletedTask;
}
// Do NOT run publishing if not properly running
if (_runtimeState.Level != RuntimeLevel.Run)
{
_logger.LogDebug("Does not run if run level is not Run.");
return Task.CompletedTask;
}
try
{
// Ensure we run with an UmbracoContext, because this will run in a background task,
// and developers may be using the UmbracoContext in the event handlers.
// TODO: or maybe not, CacheRefresherComponent already ensures a context when handling events
// - UmbracoContext 'current' needs to be refactored and cleaned up
// - batched messenger should not depend on a current HttpContext
// but then what should be its "scope"? could we attach it to scopes?
// - and we should definitively *not* have to flush it here (should be auto)
using UmbracoContextReference contextReference = _umbracoContextFactory.EnsureUmbracoContext();
using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
/* We used to assume that there will never be two instances running concurrently where (IsMainDom && ServerRole == SchedulingPublisher)
* However this is possible during an azure deployment slot swap for the SchedulingPublisher instance when trying to achieve zero downtime deployments.
* If we take a distributed write lock, we are certain that the multiple instances of the job will not run in parallel.
* It's possible that during the swapping process we may run this job more frequently than intended but this is not of great concern and it's
* only until the old SchedulingPublisher shuts down. */
scope.EagerWriteLock(Constants.Locks.ScheduledPublishing);
try
{
// Run
IEnumerable<PublishResult> result = _contentService.PerformScheduledPublish(DateTime.Now);
foreach (IGrouping<PublishResultType, PublishResult> grouped in result.GroupBy(x => x.Result))
{
_logger.LogInformation(
"Scheduled publishing result: '{StatusCount}' items with status {Status}",
grouped.Count(),
grouped.Key);
}
}
finally
{
// If running on a temp context, we have to flush the messenger
if (contextReference.IsRoot)
{
_serverMessenger.SendMessages();
}
}
}
catch (Exception ex)
{
// important to catch *everything* to ensure the task repeats
_logger.LogError(ex, "Failed.");
}
return Task.CompletedTask;
}
switch (_serverRegistrar.CurrentServerRole)
{
case ServerRole.Subscriber:
_logger.LogDebug("Does not run on subscriber servers.");
return Task.CompletedTask;
case ServerRole.Unknown:
_logger.LogDebug("Does not run on servers with unknown role.");
return Task.CompletedTask;
}
// 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 Task.CompletedTask;
}
// Do NOT run publishing if not properly running
if (_runtimeState.Level != RuntimeLevel.Run)
{
_logger.LogDebug("Does not run if run level is not Run.");
return Task.CompletedTask;
}
try
{
// Ensure we run with an UmbracoContext, because this will run in a background task,
// and developers may be using the UmbracoContext in the event handlers.
// TODO: or maybe not, CacheRefresherComponent already ensures a context when handling events
// - UmbracoContext 'current' needs to be refactored and cleaned up
// - batched messenger should not depend on a current HttpContext
// but then what should be its "scope"? could we attach it to scopes?
// - and we should definitively *not* have to flush it here (should be auto)
using UmbracoContextReference contextReference = _umbracoContextFactory.EnsureUmbracoContext();
using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
/* We used to assume that there will never be two instances running concurrently where (IsMainDom && ServerRole == SchedulingPublisher)
* However this is possible during an azure deployment slot swap for the SchedulingPublisher instance when trying to achieve zero downtime deployments.
* If we take a distributed write lock, we are certain that the multiple instances of the job will not run in parallel.
* It's possible that during the swapping process we may run this job more frequently than intended but this is not of great concern and it's
* only until the old SchedulingPublisher shuts down. */
scope.EagerWriteLock(Constants.Locks.ScheduledPublishing);
try
{
// Run
IEnumerable<PublishResult> result = _contentService.PerformScheduledPublish(DateTime.Now);
foreach (IGrouping<PublishResultType, PublishResult> grouped in result.GroupBy(x => x.Result))
{
_logger.LogInformation(
"Scheduled publishing result: '{StatusCount}' items with status {Status}",
grouped.Count(),
grouped.Key);
}
}
finally
{
// If running on a temp context, we have to flush the messenger
if (contextReference.IsRoot)
{
_serverMessenger.SendMessages();
}
}
}
catch (Exception ex)
{
// important to catch *everything* to ensure the task repeats
_logger.LogError(ex, "Failed.");
}
return Task.CompletedTask;
}
}

View File

@@ -1,8 +1,6 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
@@ -10,65 +8,64 @@ using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration
namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration;
/// <summary>
/// Implements periodic database instruction processing as a hosted service.
/// </summary>
public class InstructionProcessTask : RecurringHostedServiceBase
{
private readonly ILogger<InstructionProcessTask> _logger;
private readonly IServerMessenger _messenger;
private readonly IRuntimeState _runtimeState;
private bool _disposedValue;
/// <summary>
/// Implements periodic database instruction processing as a hosted service.
/// Initializes a new instance of the <see cref="InstructionProcessTask" /> class.
/// </summary>
public class InstructionProcessTask : RecurringHostedServiceBase
/// <param name="runtimeState">Representation of the state of the Umbraco runtime.</param>
/// <param name="messenger">Service broadcasting cache notifications to registered servers.</param>
/// <param name="logger">The typed logger.</param>
/// <param name="globalSettings">The configuration for global settings.</param>
public InstructionProcessTask(IRuntimeState runtimeState, IServerMessenger messenger, ILogger<InstructionProcessTask> logger, IOptions<GlobalSettings> globalSettings)
: base(logger, globalSettings.Value.DatabaseServerMessenger.TimeBetweenSyncOperations, TimeSpan.FromMinutes(1))
{
private readonly IRuntimeState _runtimeState;
private readonly IServerMessenger _messenger;
private readonly ILogger<InstructionProcessTask> _logger;
private bool _disposedValue;
_runtimeState = runtimeState;
_messenger = messenger;
_logger = logger;
}
/// <summary>
/// Initializes a new instance of the <see cref="InstructionProcessTask"/> class.
/// </summary>
/// <param name="runtimeState">Representation of the state of the Umbraco runtime.</param>
/// <param name="messenger">Service broadcasting cache notifications to registered servers.</param>
/// <param name="logger">The typed logger.</param>
/// <param name="globalSettings">The configuration for global settings.</param>
public InstructionProcessTask(IRuntimeState runtimeState, IServerMessenger messenger, ILogger<InstructionProcessTask> logger, IOptions<GlobalSettings> globalSettings)
: base(logger, globalSettings.Value.DatabaseServerMessenger.TimeBetweenSyncOperations, TimeSpan.FromMinutes(1))
public override Task PerformExecuteAsync(object? state)
{
if (_runtimeState.Level != RuntimeLevel.Run)
{
_runtimeState = runtimeState;
_messenger = messenger;
_logger = logger;
}
public override Task PerformExecuteAsync(object? state)
{
if (_runtimeState.Level != RuntimeLevel.Run)
{
return Task.CompletedTask;
}
try
{
_messenger.Sync();
}
catch (Exception e)
{
_logger.LogError(e, "Failed (will repeat).");
}
return Task.CompletedTask;
}
protected override void Dispose(bool disposing)
try
{
if (!_disposedValue)
{
if (disposing && _messenger is IDisposable disposable)
{
disposable.Dispose();
}
_messenger.Sync();
}
catch (Exception e)
{
_logger.LogError(e, "Failed (will repeat).");
}
_disposedValue = true;
return Task.CompletedTask;
}
protected override void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing && _messenger is IDisposable disposable)
{
disposable.Dispose();
}
base.Dispose(disposing);
_disposedValue = true;
}
base.Dispose(disposing);
}
}

View File

@@ -1,10 +1,6 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
@@ -12,85 +8,87 @@ using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration
namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration;
/// <summary>
/// Implements periodic server "touching" (to mark as active/deactive) as a hosted service.
/// </summary>
public class TouchServerTask : RecurringHostedServiceBase
{
private readonly IHostingEnvironment _hostingEnvironment;
private readonly ILogger<TouchServerTask> _logger;
private readonly IRuntimeState _runtimeState;
private readonly IServerRegistrationService _serverRegistrationService;
private readonly IServerRoleAccessor _serverRoleAccessor;
private GlobalSettings _globalSettings;
/// <summary>
/// Implements periodic server "touching" (to mark as active/deactive) as a hosted service.
/// Initializes a new instance of the <see cref="TouchServerTask" /> class.
/// </summary>
public class TouchServerTask : RecurringHostedServiceBase
/// <param name="runtimeState">Representation of the state of the Umbraco runtime.</param>
/// <param name="serverRegistrationService">Services for server registrations.</param>
/// <param name="logger">The typed logger.</param>
/// <param name="globalSettings">The configuration for global settings.</param>
/// <param name="hostingEnvironment">The hostingEnviroment.</param>
/// <param name="serverRoleAccessor">The accessor for the server role</param>
public TouchServerTask(
IRuntimeState runtimeState,
IServerRegistrationService serverRegistrationService,
IHostingEnvironment hostingEnvironment,
ILogger<TouchServerTask> logger,
IOptionsMonitor<GlobalSettings> globalSettings,
IServerRoleAccessor serverRoleAccessor)
: base(logger, globalSettings.CurrentValue.DatabaseServerRegistrar.WaitTimeBetweenCalls, TimeSpan.FromSeconds(15))
{
private readonly IRuntimeState _runtimeState;
private readonly IServerRegistrationService _serverRegistrationService;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly ILogger<TouchServerTask> _logger;
private readonly IServerRoleAccessor _serverRoleAccessor;
private GlobalSettings _globalSettings;
/// <summary>
/// Initializes a new instance of the <see cref="TouchServerTask"/> class.
/// </summary>
/// <param name="runtimeState">Representation of the state of the Umbraco runtime.</param>
/// <param name="serverRegistrationService">Services for server registrations.</param>
/// <param name="requestAccessor">Accessor for the current request.</param>
/// <param name="logger">The typed logger.</param>
/// <param name="globalSettings">The configuration for global settings.</param>
public TouchServerTask(
IRuntimeState runtimeState,
IServerRegistrationService serverRegistrationService,
IHostingEnvironment hostingEnvironment,
ILogger<TouchServerTask> logger,
IOptionsMonitor<GlobalSettings> globalSettings,
IServerRoleAccessor serverRoleAccessor)
: base(logger, globalSettings.CurrentValue.DatabaseServerRegistrar.WaitTimeBetweenCalls, TimeSpan.FromSeconds(15))
_runtimeState = runtimeState;
_serverRegistrationService = serverRegistrationService ??
throw new ArgumentNullException(nameof(serverRegistrationService));
_hostingEnvironment = hostingEnvironment;
_logger = logger;
_globalSettings = globalSettings.CurrentValue;
globalSettings.OnChange(x =>
{
_runtimeState = runtimeState;
_serverRegistrationService = serverRegistrationService ?? throw new ArgumentNullException(nameof(serverRegistrationService));
_hostingEnvironment = hostingEnvironment;
_logger = logger;
_globalSettings = globalSettings.CurrentValue;
globalSettings.OnChange(x =>
{
_globalSettings = x;
ChangePeriod(x.DatabaseServerRegistrar.WaitTimeBetweenCalls);
});
_serverRoleAccessor = serverRoleAccessor;
}
_globalSettings = x;
ChangePeriod(x.DatabaseServerRegistrar.WaitTimeBetweenCalls);
});
_serverRoleAccessor = serverRoleAccessor;
}
public override Task PerformExecuteAsync(object? state)
public override Task PerformExecuteAsync(object? state)
{
if (_runtimeState.Level != RuntimeLevel.Run)
{
if (_runtimeState.Level != RuntimeLevel.Run)
{
return Task.CompletedTask;
}
// If the IServerRoleAccessor has been changed away from ElectedServerRoleAccessor this task no longer makes sense,
// since all it's used for is to allow the ElectedServerRoleAccessor
// to figure out what role a given server has, so we just stop this task.
if (_serverRoleAccessor is not ElectedServerRoleAccessor)
{
return StopAsync(CancellationToken.None);
}
var serverAddress = _hostingEnvironment.ApplicationMainUrl?.ToString();
if (serverAddress.IsNullOrWhiteSpace())
{
_logger.LogWarning("No umbracoApplicationUrl for service (yet), skip.");
return Task.CompletedTask;
}
try
{
_serverRegistrationService.TouchServer(serverAddress!, _globalSettings.DatabaseServerRegistrar.StaleServerTimeout);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to update server record in database.");
}
return Task.CompletedTask;
}
// If the IServerRoleAccessor has been changed away from ElectedServerRoleAccessor this task no longer makes sense,
// since all it's used for is to allow the ElectedServerRoleAccessor
// to figure out what role a given server has, so we just stop this task.
if (_serverRoleAccessor is not ElectedServerRoleAccessor)
{
return StopAsync(CancellationToken.None);
}
var serverAddress = _hostingEnvironment.ApplicationMainUrl?.ToString();
if (serverAddress.IsNullOrWhiteSpace())
{
_logger.LogWarning("No umbracoApplicationUrl for service (yet), skip.");
return Task.CompletedTask;
}
try
{
_serverRegistrationService.TouchServer(
serverAddress!,
_globalSettings.DatabaseServerRegistrar.StaleServerTimeout);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to update server record in database.");
}
return Task.CompletedTask;
}
}

View File

@@ -1,102 +1,99 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Runtime;
namespace Umbraco.Cms.Infrastructure.HostedServices
namespace Umbraco.Cms.Infrastructure.HostedServices;
/// <summary>
/// Used to cleanup temporary file locations.
/// </summary>
/// <remarks>
/// Will run on all servers - even though file upload should only be handled on the scheduling publisher, this will
/// ensure that in the case it happens on subscribers that they are cleaned up too.
/// </remarks>
public class TempFileCleanup : RecurringHostedServiceBase
{
private readonly TimeSpan _age = TimeSpan.FromDays(1);
private readonly IIOHelper _ioHelper;
private readonly ILogger<TempFileCleanup> _logger;
private readonly IMainDom _mainDom;
private readonly DirectoryInfo[] _tempFolders;
/// <summary>
/// Used to cleanup temporary file locations.
/// Initializes a new instance of the <see cref="TempFileCleanup" /> class.
/// </summary>
/// <remarks>
/// Will run on all servers - even though file upload should only be handled on the scheduling publisher, this will
/// ensure that in the case it happens on subscribers that they are cleaned up too.
/// </remarks>
public class TempFileCleanup : RecurringHostedServiceBase
/// <param name="ioHelper">Helper service for IO operations.</param>
/// <param name="mainDom">Representation of the main application domain.</param>
/// <param name="logger">The typed logger.</param>
public TempFileCleanup(IIOHelper ioHelper, IMainDom mainDom, ILogger<TempFileCleanup> logger)
: base(logger, TimeSpan.FromMinutes(60), DefaultDelay)
{
private readonly IIOHelper _ioHelper;
private readonly IMainDom _mainDom;
private readonly ILogger<TempFileCleanup> _logger;
_ioHelper = ioHelper;
_mainDom = mainDom;
_logger = logger;
private readonly DirectoryInfo[] _tempFolders;
private readonly TimeSpan _age = TimeSpan.FromDays(1);
_tempFolders = _ioHelper.GetTempFolders();
}
/// <summary>
/// Initializes a new instance of the <see cref="TempFileCleanup"/> class.
/// </summary>
/// <param name="ioHelper">Helper service for IO operations.</param>
/// <param name="mainDom">Representation of the main application domain.</param>
/// <param name="logger">The typed logger.</param>
public TempFileCleanup(IIOHelper ioHelper, IMainDom mainDom, ILogger<TempFileCleanup> logger)
: base(logger, TimeSpan.FromMinutes(60), DefaultDelay)
public override Task PerformExecuteAsync(object? state)
{
// Ensure we do not run if not main domain
if (_mainDom.IsMainDom == false)
{
_ioHelper = ioHelper;
_mainDom = mainDom;
_logger = logger;
_tempFolders = _ioHelper.GetTempFolders();
}
public override Task PerformExecuteAsync(object? state)
{
// Ensure we do not run if not main domain
if (_mainDom.IsMainDom == false)
{
_logger.LogDebug("Does not run if not MainDom.");
return Task.CompletedTask;
}
foreach (DirectoryInfo folder in _tempFolders)
{
CleanupFolder(folder);
}
_logger.LogDebug("Does not run if not MainDom.");
return Task.CompletedTask;
}
private void CleanupFolder(DirectoryInfo folder)
foreach (DirectoryInfo folder in _tempFolders)
{
CleanFolderResult result = _ioHelper.CleanFolder(folder, _age);
switch (result.Status)
{
case CleanFolderResultStatus.FailedAsDoesNotExist:
_logger.LogDebug("The cleanup folder doesn't exist {Folder}", folder.FullName);
break;
case CleanFolderResultStatus.FailedWithException:
foreach (CleanFolderResult.Error error in result.Errors!)
{
_logger.LogError(error.Exception, "Could not delete temp file {FileName}", error.ErroringFile.FullName);
}
CleanupFolder(folder);
}
break;
}
return Task.CompletedTask;
}
folder.Refresh(); // In case it's changed during runtime
if (!folder.Exists)
{
private void CleanupFolder(DirectoryInfo folder)
{
CleanFolderResult result = _ioHelper.CleanFolder(folder, _age);
switch (result.Status)
{
case CleanFolderResultStatus.FailedAsDoesNotExist:
_logger.LogDebug("The cleanup folder doesn't exist {Folder}", folder.FullName);
return;
}
FileInfo[] files = folder.GetFiles("*.*", SearchOption.AllDirectories);
foreach (FileInfo file in files)
{
if (DateTime.UtcNow - file.LastWriteTimeUtc > _age)
break;
case CleanFolderResultStatus.FailedWithException:
foreach (CleanFolderResult.Error error in result.Errors!)
{
try
{
file.IsReadOnly = false;
file.Delete();
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not delete temp file {FileName}", file.FullName);
}
_logger.LogError(error.Exception, "Could not delete temp file {FileName}",
error.ErroringFile.FullName);
}
break;
}
folder.Refresh(); // In case it's changed during runtime
if (!folder.Exists)
{
_logger.LogDebug("The cleanup folder doesn't exist {Folder}", folder.FullName);
return;
}
FileInfo[] files = folder.GetFiles("*.*", SearchOption.AllDirectories);
foreach (FileInfo file in files)
{
if (DateTime.UtcNow - file.LastWriteTimeUtc > _age)
{
try
{
file.IsReadOnly = false;
file.Delete();
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not delete temp file {FileName}", file.FullName);
}
}
}