merge v10 to v11
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user