From 04ac1542aa4a81a6f335fc988149b91e932903cc Mon Sep 17 00:00:00 2001 From: Andrew McKaskill <32095907+andrewmckaskill@users.noreply.github.com> Date: Thu, 2 Nov 2023 12:15:14 +0000 Subject: [PATCH] Refactor hostedServices into background jobs (#14291) * Refactor jobs from HostedServices into BackgroundJobs * Clean up generics and DI setup * Add RecurringBackgroundJob Unit Tests * Add ServiceCollection helper * Add Obsolete attributes * Add Notification Classes * Add UnitTests for RecurringBackgroundJob HostedService * Add NotificationEvents * Add state to notifications * Update UnitTests * Add Obsolete Attributes to old hosted service classes * Updated xmldoc in IRecurringBackgroundJob.cs * Update Obsolete attribute messages to indicate classes will be removed in Umbraco 14 (cherry picked from commit c30ffa9ac3ae9c12508242855c63208567c55eb9) --- .../BackgroundJobs/DelayCalculator.cs | 67 ++++++ .../BackgroundJobs/IRecurringBackgroundJob.cs | 28 +++ .../Jobs/ContentVersionCleanupJob.cs | 66 ++++++ .../Jobs/HealthCheckNotifierJob.cs | 116 ++++++++++ .../BackgroundJobs/Jobs/KeepAliveJob.cs | 90 ++++++++ .../BackgroundJobs/Jobs/LogScrubberJob.cs | 73 ++++++ .../BackgroundJobs/Jobs/ReportSiteJob.cs | 92 ++++++++ .../Jobs/ScheduledPublishingJob.cs | 105 +++++++++ .../InstructionProcessJob.cs | 59 +++++ .../Jobs/ServerRegistration/TouchServerJob.cs | 102 +++++++++ .../BackgroundJobs/Jobs/TempFileCleanupJob.cs | 99 +++++++++ .../RecurringBackgroundJobHostedService.cs | 127 +++++++++++ ...curringBackgroundJobHostedServiceRunner.cs | 81 +++++++ .../Extensions/ServiceCollectionExtensions.cs | 31 +++ .../HostedServices/ContentVersionCleanup.cs | 1 + .../HostedServices/HealthCheckNotifier.cs | 1 + .../HostedServices/KeepAlive.cs | 1 + .../HostedServices/LogScrubber.cs | 1 + .../RecurringHostedServiceBase.cs | 4 +- .../HostedServices/ReportSiteTask.cs | 1 + .../HostedServices/ScheduledPublishing.cs | 1 + .../InstructionProcessTask.cs | 1 + .../ServerRegistration/TouchServerTask.cs | 1 + .../HostedServices/TempFileCleanup.cs | 1 + ...urringBackgroundJobExecutedNotification.cs | 12 + ...rringBackgroundJobExecutingNotification.cs | 12 + ...ecurringBackgroundJobFailedNotification.cs | 12 + ...curringBackgroundJobIgnoredNotification.cs | 12 + .../RecurringBackgroundJobNotification.cs | 12 + ...curringBackgroundJobStartedNotification.cs | 12 + ...urringBackgroundJobStartingNotification.cs | 12 + ...curringBackgroundJobStoppedNotification.cs | 12 + ...urringBackgroundJobStoppingNotification.cs | 12 + .../UmbracoBuilderExtensions.cs | 2 +- .../UmbracoBuilderExtensions.cs | 35 +++ .../Jobs/HealthCheckNotifierJobTests.cs | 139 ++++++++++++ .../BackgroundJobs/Jobs/KeepAliveJobTests.cs | 94 ++++++++ .../Jobs/LogScrubberJobTests.cs | 72 ++++++ .../Jobs/ScheduledPublishingJobTests.cs | 92 ++++++++ .../InstructionProcessJobTests.cs | 51 +++++ .../ServerRegistration/TouchServerJobTests.cs | 95 ++++++++ .../Jobs/TempFileCleanupJobTests.cs | 50 +++++ ...ecurringBackgroundJobHostedServiceTests.cs | 208 ++++++++++++++++++ .../HealthCheckNotifierTests.cs | 1 + .../HostedServices/KeepAliveTests.cs | 1 + .../HostedServices/LogScrubberTests.cs | 1 + .../ScheduledPublishingTests.cs | 1 + .../InstructionProcessTaskTests.cs | 1 + .../TouchServerTaskTests.cs | 1 + .../HostedServices/TempFileCleanupTests.cs | 1 + 50 files changed, 2099 insertions(+), 3 deletions(-) create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/DelayCalculator.cs create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/IRecurringBackgroundJob.cs create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ContentVersionCleanupJob.cs create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/Jobs/HealthCheckNotifierJob.cs create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/Jobs/KeepAliveJob.cs create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/Jobs/LogScrubberJob.cs create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ReportSiteJob.cs create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ScheduledPublishingJob.cs create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/InstructionProcessJob.cs create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/TouchServerJob.cs create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/Jobs/TempFileCleanupJob.cs create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobHostedService.cs create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobHostedServiceRunner.cs create mode 100644 src/Umbraco.Infrastructure/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobExecutedNotification.cs create mode 100644 src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobExecutingNotification.cs create mode 100644 src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobFailedNotification.cs create mode 100644 src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobIgnoredNotification.cs create mode 100644 src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobNotification.cs create mode 100644 src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStartedNotification.cs create mode 100644 src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStartingNotification.cs create mode 100644 src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStoppedNotification.cs create mode 100644 src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStoppingNotification.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/HealthCheckNotifierJobTests.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/KeepAliveJobTests.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/LogScrubberJobTests.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/ScheduledPublishingJobTests.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/InstructionProcessJobTests.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/TouchServerJobTests.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/TempFileCleanupJobTests.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobHostedServiceTests.cs diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/DelayCalculator.cs b/src/Umbraco.Infrastructure/BackgroundJobs/DelayCalculator.cs new file mode 100644 index 0000000000..42d016c066 --- /dev/null +++ b/src/Umbraco.Infrastructure/BackgroundJobs/DelayCalculator.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Configuration; + +namespace Umbraco.Cms.Infrastructure.BackgroundJobs +{ + public class DelayCalculator + { + /// + /// 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. + /// + /// The configured time to first run the task in crontab format. + /// An instance of + /// The logger. + /// The default delay to use when a first run time is not configured. + /// The delay before first running the recurring task. + public static TimeSpan GetDelay( + string firstRunTime, + ICronTabParser cronTabParser, + ILogger logger, + TimeSpan defaultDelay) => GetDelay(firstRunTime, cronTabParser, logger, DateTime.Now, defaultDelay); + + /// + /// 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. + /// + /// The configured time to first run the task in crontab format. + /// An instance of + /// The logger. + /// The current datetime. + /// The default delay to use when a first run time is not configured. + /// The delay before first running the recurring task. + /// Internal to expose for unit tests. + 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)) + { + return defaultDelay; + } + + // If first run time not a valid cron tab, log, and revert to small delay after application start. + if (!cronTabParser.IsValidCronTab(firstRunTime)) + { + logger.LogWarning("Could not parse {FirstRunTime} as a crontab expression. Defaulting to default delay for hosted service start.", firstRunTime); + return defaultDelay; + } + + // 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; + } + } +} diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/IRecurringBackgroundJob.cs b/src/Umbraco.Infrastructure/BackgroundJobs/IRecurringBackgroundJob.cs new file mode 100644 index 0000000000..c6be3dcec5 --- /dev/null +++ b/src/Umbraco.Infrastructure/BackgroundJobs/IRecurringBackgroundJob.cs @@ -0,0 +1,28 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Sync; + +namespace Umbraco.Cms.Infrastructure.BackgroundJobs; +/// +/// A recurring background job +/// +public interface IRecurringBackgroundJob +{ + static readonly TimeSpan DefaultDelay = System.TimeSpan.FromMinutes(3); + static readonly ServerRole[] DefaultServerRoles = new[] { ServerRole.Single, ServerRole.SchedulingPublisher }; + + /// Timespan representing how often the task should recur. + TimeSpan Period { get; } + + /// + /// Timespan representing the initial delay after application start-up before the first run of the task + /// occurs. + /// + TimeSpan Delay { get => DefaultDelay; } + + ServerRole[] ServerRoles { get => DefaultServerRoles; } + + event EventHandler PeriodChanged; + + Task RunJobAsync(); +} + diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ContentVersionCleanupJob.cs b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ContentVersionCleanupJob.cs new file mode 100644 index 0000000000..cb89d600aa --- /dev/null +++ b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ContentVersionCleanupJob.cs @@ -0,0 +1,66 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Runtime; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Infrastructure.BackgroundJobs; + +namespace Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs; + +/// +/// Recurring hosted service that executes the content history cleanup. +/// +public class ContentVersionCleanupJob : IRecurringBackgroundJob +{ + + public TimeSpan Period { get => TimeSpan.FromHours(1); } + + // No-op event as the period never changes on this job + public event EventHandler PeriodChanged { add { } remove { } } + + private readonly ILogger _logger; + private readonly IContentVersionService _service; + private readonly IOptionsMonitor _settingsMonitor; + + + /// + /// Initializes a new instance of the class. + /// + public ContentVersionCleanupJob( + ILogger logger, + IOptionsMonitor settingsMonitor, + IContentVersionService service) + { + _logger = logger; + _settingsMonitor = settingsMonitor; + _service = service; + } + + /// + public Task RunJobAsync() + { + // 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.CompletedTask; + } + + + 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.CompletedTask; + } +} diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/HealthCheckNotifierJob.cs b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/HealthCheckNotifierJob.cs new file mode 100644 index 0000000000..ba78af33b4 --- /dev/null +++ b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/HealthCheckNotifierJob.cs @@ -0,0 +1,116 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.HealthChecks; +using Umbraco.Cms.Core.HealthChecks.NotificationMethods; +using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Runtime; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs; + +/// +/// Hosted service implementation for recurring health check notifications. +/// +public class HealthCheckNotifierJob : IRecurringBackgroundJob +{ + + + public TimeSpan Period { get; private set; } + public TimeSpan Delay { get; private set; } + + private event EventHandler? _periodChanged; + public event EventHandler PeriodChanged + { + add { _periodChanged += value; } + remove { _periodChanged -= value; } + } + + private readonly HealthCheckCollection _healthChecks; + private readonly ILogger _logger; + private readonly HealthCheckNotificationMethodCollection _notifications; + private readonly IProfilingLogger _profilingLogger; + private readonly ICoreScopeProvider _scopeProvider; + private HealthChecksSettings _healthChecksSettings; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration for health check settings. + /// The collection of healthchecks. + /// The collection of healthcheck notification methods. + /// Provides scopes for database operations. + /// The typed logger. + /// The profiling logger. + /// Parser of crontab expressions. + public HealthCheckNotifierJob( + IOptionsMonitor healthChecksSettings, + HealthCheckCollection healthChecks, + HealthCheckNotificationMethodCollection notifications, + ICoreScopeProvider scopeProvider, + ILogger logger, + IProfilingLogger profilingLogger, + ICronTabParser cronTabParser) + { + _healthChecksSettings = healthChecksSettings.CurrentValue; + _healthChecks = healthChecks; + _notifications = notifications; + _scopeProvider = scopeProvider; + _logger = logger; + _profilingLogger = profilingLogger; + + Period = healthChecksSettings.CurrentValue.Notification.Period; + Delay = DelayCalculator.GetDelay(healthChecksSettings.CurrentValue.Notification.FirstRunTime, cronTabParser, logger, TimeSpan.FromMinutes(3)); + + + healthChecksSettings.OnChange(x => + { + _healthChecksSettings = x; + Period = x.Notification.Period; + _periodChanged?.Invoke(this, EventArgs.Empty); + }); + } + + public async Task RunJobAsync() + { + if (_healthChecksSettings.Notification.Enabled == false) + { + 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("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 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)) + { + await notificationMethod.SendAsync(results); + } + } + } +} diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/KeepAliveJob.cs b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/KeepAliveJob.cs new file mode 100644 index 0000000000..a9849ddbb7 --- /dev/null +++ b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/KeepAliveJob.cs @@ -0,0 +1,90 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Runtime; +using Umbraco.Cms.Core.Sync; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs; + +/// +/// Hosted service implementation for keep alive feature. +/// +public class KeepAliveJob : IRecurringBackgroundJob +{ + public TimeSpan Period { get => TimeSpan.FromMinutes(5); } + + // No-op event as the period never changes on this job + public event EventHandler PeriodChanged { add { } remove { } } + + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; + private readonly IProfilingLogger _profilingLogger; + private KeepAliveSettings _keepAliveSettings; + + /// + /// Initializes a new instance of the class. + /// + /// The current hosting environment + /// The configuration for keep alive settings. + /// The typed logger. + /// The profiling logger. + /// Factory for instances. + public KeepAliveJob( + IHostingEnvironment hostingEnvironment, + IOptionsMonitor keepAliveSettings, + ILogger logger, + IProfilingLogger profilingLogger, + IHttpClientFactory httpClientFactory) + { + _hostingEnvironment = hostingEnvironment; + _keepAliveSettings = keepAliveSettings.CurrentValue; + _logger = logger; + _profilingLogger = profilingLogger; + _httpClientFactory = httpClientFactory; + + keepAliveSettings.OnChange(x => _keepAliveSettings = x); + } + + public async Task RunJobAsync() + { + if (_keepAliveSettings.DisableKeepAliveTask) + { + return; + } + + using (_profilingLogger.DebugDuration("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 + var 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); + } + } + } +} diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/LogScrubberJob.cs b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/LogScrubberJob.cs new file mode 100644 index 0000000000..1c745661cb --- /dev/null +++ b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/LogScrubberJob.cs @@ -0,0 +1,73 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Runtime; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; + +namespace Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs; + +/// +/// Log scrubbing hosted service. +/// +/// +/// Will only run on non-replica servers. +/// +public class LogScrubberJob : IRecurringBackgroundJob +{ + public TimeSpan Period { get => TimeSpan.FromHours(4); } + + // No-op event as the period never changes on this job + public event EventHandler PeriodChanged { add { } remove { } } + + + private readonly IAuditService _auditService; + private readonly ILogger _logger; + private readonly IProfilingLogger _profilingLogger; + private readonly ICoreScopeProvider _scopeProvider; + private LoggingSettings _settings; + + /// + /// Initializes a new instance of the class. + /// + /// Service for handling audit operations. + /// The configuration for logging settings. + /// Provides scopes for database operations. + /// The typed logger. + /// The profiling logger. + public LogScrubberJob( + IAuditService auditService, + IOptionsMonitor settings, + ICoreScopeProvider scopeProvider, + ILogger logger, + IProfilingLogger profilingLogger) + { + + _auditService = auditService; + _settings = settings.CurrentValue; + _scopeProvider = scopeProvider; + _logger = logger; + _profilingLogger = profilingLogger; + settings.OnChange(x => _settings = x); + } + + public Task RunJobAsync() + { + + // Ensure we use an explicit scope since we are running on a background thread. + using (ICoreScope scope = _scopeProvider.CreateCoreScope()) + using (_profilingLogger.DebugDuration("Log scrubbing executing", "Log scrubbing complete")) + { + _auditService.CleanLogs((int)_settings.MaxLogAge.TotalMinutes); + _ = scope.Complete(); + } + + return Task.CompletedTask; + } +} diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ReportSiteJob.cs b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ReportSiteJob.cs new file mode 100644 index 0000000000..5d39b57add --- /dev/null +++ b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ReportSiteJob.cs @@ -0,0 +1,92 @@ +using System.Text; +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.DependencyInjection; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Telemetry; +using Umbraco.Cms.Core.Telemetry.Models; + +namespace Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs; + +public class ReportSiteJob : IRecurringBackgroundJob +{ + + public TimeSpan Period { get => TimeSpan.FromDays(1); } + public TimeSpan Delay { get => TimeSpan.FromMinutes(5); } + public ServerRole[] ServerRoles { get => Enum.GetValues(); } + + // No-op event as the period never changes on this job + public event EventHandler PeriodChanged { add { } remove { } } + + + private static HttpClient _httpClient = new(); + private readonly ILogger _logger; + private readonly ITelemetryService _telemetryService; + + + public ReportSiteJob( + ILogger logger, + ITelemetryService telemetryService) + { + _logger = logger; + _telemetryService = telemetryService; + _httpClient = new HttpClient(); + } + + /// + /// Runs the background task to send the anonymous ID + /// to telemetry service + /// + public async Task RunJobAsync() + { + + if (_telemetryService.TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData) is false) + { + _logger.LogWarning("No telemetry marker found"); + + return; + } + + try + { + if (_httpClient.BaseAddress is null) + { + // Send data to LIVE telemetry + _httpClient.BaseAddress = new Uri("https://telemetry.umbraco.com/"); + +#if DEBUG + // Send data to DEBUG telemetry service + _httpClient.BaseAddress = new Uri("https://telemetry.rainbowsrock.net/"); +#endif + } + + _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"); + + // 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"); + } + } +} diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ScheduledPublishingJob.cs b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ScheduledPublishingJob.cs new file mode 100644 index 0000000000..f815366a21 --- /dev/null +++ b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ScheduledPublishingJob.cs @@ -0,0 +1,105 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Runtime; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Web; + +namespace Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs; + +/// +/// Hosted service implementation for scheduled publishing feature. +/// +/// +/// Runs only on non-replica servers. +/// +public class ScheduledPublishingJob : IRecurringBackgroundJob +{ + public TimeSpan Period { get => TimeSpan.FromMinutes(1); } + // No-op event as the period never changes on this job + public event EventHandler PeriodChanged { add { } remove { } } + + + private readonly IContentService _contentService; + private readonly ILogger _logger; + private readonly ICoreScopeProvider _scopeProvider; + private readonly IServerMessenger _serverMessenger; + private readonly IUmbracoContextFactory _umbracoContextFactory; + + /// + /// Initializes a new instance of the class. + /// + public ScheduledPublishingJob( + IContentService contentService, + IUmbracoContextFactory umbracoContextFactory, + ILogger logger, + IServerMessenger serverMessenger, + ICoreScopeProvider scopeProvider) + { + _contentService = contentService; + _umbracoContextFactory = umbracoContextFactory; + _logger = logger; + _serverMessenger = serverMessenger; + _scopeProvider = scopeProvider; + } + + public Task RunJobAsync() + { + if (Suspendable.ScheduledPublishing.CanRun == false) + { + 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 result = _contentService.PerformScheduledPublish(DateTime.Now); + foreach (IGrouping 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; + } +} diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/InstructionProcessJob.cs b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/InstructionProcessJob.cs new file mode 100644 index 0000000000..cc6e35bf83 --- /dev/null +++ b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/InstructionProcessJob.cs @@ -0,0 +1,59 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; + +namespace Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs.ServerRegistration; + +/// +/// Implements periodic database instruction processing as a hosted service. +/// +public class InstructionProcessJob : IRecurringBackgroundJob +{ + + public TimeSpan Period { get; } + public TimeSpan Delay { get => TimeSpan.FromMinutes(1); } + public ServerRole[] ServerRoles { get => Enum.GetValues(); } + + // No-op event as the period never changes on this job + public event EventHandler PeriodChanged { add { } remove { } } + + private readonly ILogger _logger; + private readonly IServerMessenger _messenger; + + /// + /// Initializes a new instance of the class. + /// + /// Service broadcasting cache notifications to registered servers. + /// The typed logger. + /// The configuration for global settings. + public InstructionProcessJob( + IServerMessenger messenger, + ILogger logger, + IOptions globalSettings) + { + _messenger = messenger; + _logger = logger; + + Period = globalSettings.Value.DatabaseServerMessenger.TimeBetweenSyncOperations; + } + + public Task RunJobAsync() + { + try + { + _messenger.Sync(); + } + catch (Exception e) + { + _logger.LogError(e, "Failed (will repeat)."); + } + + return Task.CompletedTask; + } +} diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/TouchServerJob.cs b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/TouchServerJob.cs new file mode 100644 index 0000000000..8258da1a35 --- /dev/null +++ b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/TouchServerJob.cs @@ -0,0 +1,102 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs.ServerRegistration; + +/// +/// Implements periodic server "touching" (to mark as active/deactive) as a hosted service. +/// +public class TouchServerJob : IRecurringBackgroundJob +{ + public TimeSpan Period { get; private set; } + public TimeSpan Delay { get => TimeSpan.FromSeconds(15); } + + // Runs on all servers + public ServerRole[] ServerRoles { get => Enum.GetValues(); } + + private event EventHandler? _periodChanged; + public event EventHandler PeriodChanged { + add { _periodChanged += value; } + remove { _periodChanged -= value; } + } + + + private readonly IHostingEnvironment _hostingEnvironment; + private readonly ILogger _logger; + private readonly IServerRegistrationService _serverRegistrationService; + private readonly IServerRoleAccessor _serverRoleAccessor; + private GlobalSettings _globalSettings; + + /// + /// Initializes a new instance of the class. + /// + /// Services for server registrations. + /// The typed logger. + /// The configuration for global settings. + /// The hostingEnviroment. + /// The accessor for the server role + public TouchServerJob( + IServerRegistrationService serverRegistrationService, + IHostingEnvironment hostingEnvironment, + ILogger logger, + IOptionsMonitor globalSettings, + IServerRoleAccessor serverRoleAccessor) + { + _serverRegistrationService = serverRegistrationService ?? + throw new ArgumentNullException(nameof(serverRegistrationService)); + _hostingEnvironment = hostingEnvironment; + _logger = logger; + _globalSettings = globalSettings.CurrentValue; + _serverRoleAccessor = serverRoleAccessor; + + Period = _globalSettings.DatabaseServerRegistrar.WaitTimeBetweenCalls; + globalSettings.OnChange(x => + { + _globalSettings = x; + Period = x.DatabaseServerRegistrar.WaitTimeBetweenCalls; + + _periodChanged?.Invoke(this, EventArgs.Empty); + }); + } + + public Task RunJobAsync() + { + + // 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 Task.CompletedTask; + } + + 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; + } +} diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/TempFileCleanupJob.cs b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/TempFileCleanupJob.cs new file mode 100644 index 0000000000..ac88a8edce --- /dev/null +++ b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/TempFileCleanupJob.cs @@ -0,0 +1,99 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Runtime; +using Umbraco.Cms.Core.Sync; + +namespace Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs; + +/// +/// Used to cleanup temporary file locations. +/// +/// +/// 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. +/// +public class TempFileCleanupJob : IRecurringBackgroundJob +{ + public TimeSpan Period { get => TimeSpan.FromMinutes(60); } + + // Runs on all servers + public ServerRole[] ServerRoles { get => Enum.GetValues(); } + + // No-op event as the period never changes on this job + public event EventHandler PeriodChanged { add { } remove { } } + + private readonly TimeSpan _age = TimeSpan.FromDays(1); + private readonly IIOHelper _ioHelper; + private readonly ILogger _logger; + private readonly DirectoryInfo[] _tempFolders; + + /// + /// Initializes a new instance of the class. + /// + /// Helper service for IO operations. + /// The typed logger. + public TempFileCleanupJob(IIOHelper ioHelper, ILogger logger) + { + _ioHelper = ioHelper; + _logger = logger; + + _tempFolders = _ioHelper.GetTempFolders(); + } + + public Task RunJobAsync() + { + foreach (DirectoryInfo folder in _tempFolders) + { + CleanupFolder(folder); + } + + return Task.CompletedTask; + } + + 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); + break; + case CleanFolderResultStatus.FailedWithException: + foreach (CleanFolderResult.Error error in result.Errors!) + { + _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); + } + } + } + } + +} diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobHostedService.cs b/src/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobHostedService.cs new file mode 100644 index 0000000000..80afdb903c --- /dev/null +++ b/src/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobHostedService.cs @@ -0,0 +1,127 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Serilog.Core; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Runtime; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Infrastructure.HostedServices; + +namespace Umbraco.Cms.Infrastructure.BackgroundJobs; + +public static class RecurringBackgroundJobHostedService +{ + public static Func CreateHostedServiceFactory(IServiceProvider serviceProvider) => + (IRecurringBackgroundJob job) => + { + Type hostedServiceType = typeof(RecurringBackgroundJobHostedService<>).MakeGenericType(job.GetType()); + return (IHostedService)ActivatorUtilities.CreateInstance(serviceProvider, hostedServiceType, job); + }; +} + +/// +/// Runs a recurring background job inside a hosted service. +/// Generic version for DependencyInjection +/// +/// Type of the Job +public class RecurringBackgroundJobHostedService : RecurringHostedServiceBase where TJob : IRecurringBackgroundJob +{ + + private readonly ILogger> _logger; + private readonly IMainDom _mainDom; + private readonly IRuntimeState _runtimeState; + private readonly IServerRoleAccessor _serverRoleAccessor; + private readonly IEventAggregator _eventAggregator; + private readonly IRecurringBackgroundJob _job; + + public RecurringBackgroundJobHostedService( + IRuntimeState runtimeState, + ILogger> logger, + IMainDom mainDom, + IServerRoleAccessor serverRoleAccessor, + IEventAggregator eventAggregator, + TJob job) + : base(logger, job.Period, job.Delay) + { + _runtimeState = runtimeState; + _logger = logger; + _mainDom = mainDom; + _serverRoleAccessor = serverRoleAccessor; + _eventAggregator = eventAggregator; + _job = job; + + _job.PeriodChanged += (sender, e) => ChangePeriod(_job.Period); + } + + /// + public override async Task PerformExecuteAsync(object? state) + { + var executingNotification = new Notifications.RecurringBackgroundJobExecutingNotification(_job, new EventMessages()); + await _eventAggregator.PublishAsync(executingNotification); + + try + { + + if (_runtimeState.Level != RuntimeLevel.Run) + { + _logger.LogDebug("Job not running as runlevel not yet ready"); + await _eventAggregator.PublishAsync(new Notifications.RecurringBackgroundJobIgnoredNotification(_job, new EventMessages()).WithStateFrom(executingNotification)); + return; + } + + // Don't run on replicas nor unknown role servers + if (!_job.ServerRoles.Contains(_serverRoleAccessor.CurrentServerRole)) + { + _logger.LogDebug("Job not running on this server role"); + await _eventAggregator.PublishAsync(new Notifications.RecurringBackgroundJobIgnoredNotification(_job, new EventMessages()).WithStateFrom(executingNotification)); + return; + } + + // Ensure we do not run if not main domain, but do NOT lock it + if (!_mainDom.IsMainDom) + { + _logger.LogDebug("Job not running as not MainDom"); + await _eventAggregator.PublishAsync(new Notifications.RecurringBackgroundJobIgnoredNotification(_job, new EventMessages()).WithStateFrom(executingNotification)); + return; + } + + + await _job.RunJobAsync(); + await _eventAggregator.PublishAsync(new Notifications.RecurringBackgroundJobExecutedNotification(_job, new EventMessages()).WithStateFrom(executingNotification)); + + + } + catch (Exception ex) + { + await _eventAggregator.PublishAsync(new Notifications.RecurringBackgroundJobFailedNotification(_job, new EventMessages()).WithStateFrom(executingNotification)); + _logger.LogError(ex, "Unhandled exception in recurring background job."); + } + + } + + public override async Task StartAsync(CancellationToken cancellationToken) + { + var startingNotification = new Notifications.RecurringBackgroundJobStartingNotification(_job, new EventMessages()); + await _eventAggregator.PublishAsync(startingNotification); + + await base.StartAsync(cancellationToken); + + await _eventAggregator.PublishAsync(new Notifications.RecurringBackgroundJobStartedNotification(_job, new EventMessages()).WithStateFrom(startingNotification)); + + } + + public override async Task StopAsync(CancellationToken cancellationToken) + { + var stoppingNotification = new Notifications.RecurringBackgroundJobStoppingNotification(_job, new EventMessages()); + await _eventAggregator.PublishAsync(stoppingNotification); + + await base.StopAsync(cancellationToken); + + await _eventAggregator.PublishAsync(new Notifications.RecurringBackgroundJobStoppedNotification(_job, new EventMessages()).WithStateFrom(stoppingNotification)); + } +} diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobHostedServiceRunner.cs b/src/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobHostedServiceRunner.cs new file mode 100644 index 0000000000..0e56bfa2b1 --- /dev/null +++ b/src/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobHostedServiceRunner.cs @@ -0,0 +1,81 @@ +using System.Linq; +using System.Threading; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Runtime; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Infrastructure.ModelsBuilder; + +namespace Umbraco.Cms.Infrastructure.BackgroundJobs; + +/// +/// A hosted service that discovers and starts hosted services for any recurring background jobs in the DI container. +/// +public class RecurringBackgroundJobHostedServiceRunner : IHostedService +{ + private readonly ILogger _logger; + private readonly List _jobs; + private readonly Func _jobFactory; + private IList _hostedServices = new List(); + + + public RecurringBackgroundJobHostedServiceRunner( + ILogger logger, + IEnumerable jobs, + Func jobFactory) + { + _jobs = jobs.ToList(); + _logger = logger; + _jobFactory = jobFactory; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Creating recurring background jobs hosted services"); + + // create hosted services for each background job + _hostedServices = _jobs.Select(_jobFactory).ToList(); + + _logger.LogInformation("Starting recurring background jobs hosted services"); + + foreach (IHostedService hostedService in _hostedServices) + { + try + { + _logger.LogInformation($"Starting background hosted service for {hostedService.GetType().Name}"); + await hostedService.StartAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) + { + _logger.LogError(exception, $"Failed to start background hosted service for {hostedService.GetType().Name}"); + } + } + + _logger.LogInformation("Completed starting recurring background jobs hosted services"); + + + } + + public async Task StopAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Stopping recurring background jobs hosted services"); + + foreach (IHostedService hostedService in _hostedServices) + { + try + { + _logger.LogInformation($"Stopping background hosted service for {hostedService.GetType().Name}"); + await hostedService.StopAsync(stoppingToken).ConfigureAwait(false); + } + catch (Exception exception) + { + _logger.LogError(exception, $"Failed to stop background hosted service for {hostedService.GetType().Name}"); + } + } + + _logger.LogInformation("Completed stopping recurring background jobs hosted services"); + + } +} diff --git a/src/Umbraco.Infrastructure/Extensions/ServiceCollectionExtensions.cs b/src/Umbraco.Infrastructure/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000..efaac24ceb --- /dev/null +++ b/src/Umbraco.Infrastructure/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Infrastructure.BackgroundJobs; + +namespace Umbraco.Extensions; + +public static class ServiceCollectionExtensions +{ + /// + /// Adds a recurring background job with an implementation type of + /// to the specified . + /// + public static void AddRecurringBackgroundJob( + this IServiceCollection services) + where TJob : class, IRecurringBackgroundJob => + services.AddSingleton(); + + /// + /// Adds a recurring background job with an implementation type of + /// using the factory + /// to the specified . + /// + public static void AddRecurringBackgroundJob( + this IServiceCollection services, + Func implementationFactory) + where TJob : class, IRecurringBackgroundJob => + services.AddSingleton(implementationFactory); + +} + diff --git a/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs b/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs index 37eeb668f9..1ecfcbf926 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs @@ -11,6 +11,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices; /// /// Recurring hosted service that executes the content history cleanup. /// +[Obsolete("Use Umbraco.Cms.Infrastructure.BackgroundJobs.ContentVersionCleanupJob instead. This class will be removed in Umbraco 14.")] public class ContentVersionCleanup : RecurringHostedServiceBase { private readonly ILogger _logger; diff --git a/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs b/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs index e1a10d9f71..5ee76d1a18 100644 --- a/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs +++ b/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs @@ -20,6 +20,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices; /// /// Hosted service implementation for recurring health check notifications. /// +[Obsolete("Use Umbraco.Cms.Infrastructure.BackgroundJobs.HealthCheckNotifierJob instead. This class will be removed in Umbraco 14.")] public class HealthCheckNotifier : RecurringHostedServiceBase { private readonly HealthCheckCollection _healthChecks; diff --git a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs index 5db59ff225..978ffa2dd1 100644 --- a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs +++ b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs @@ -17,6 +17,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices; /// /// Hosted service implementation for keep alive feature. /// +[Obsolete("Use Umbraco.Cms.Infrastructure.BackgroundJobs.KeepAliveJob instead. This class will be removed in Umbraco 14.")] public class KeepAlive : RecurringHostedServiceBase { private readonly IHostingEnvironment _hostingEnvironment; diff --git a/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs b/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs index 9ae0dfe656..4c3df658c6 100644 --- a/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs +++ b/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs @@ -18,6 +18,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices; /// /// Will only run on non-replica servers. /// +[Obsolete("Use Umbraco.Cms.Infrastructure.BackgroundJobs.LogScrubberJob instead. This class will be removed in Umbraco 14.")] public class LogScrubber : RecurringHostedServiceBase { private readonly IAuditService _auditService; diff --git a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs index c100da0ab2..a35f7aa956 100644 --- a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs +++ b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs @@ -107,7 +107,7 @@ public abstract class RecurringHostedServiceBase : IHostedService, IDisposable } /// - public Task StartAsync(CancellationToken cancellationToken) + public virtual Task StartAsync(CancellationToken cancellationToken) { using (!ExecutionContext.IsFlowSuppressed() ? (IDisposable)ExecutionContext.SuppressFlow() : null) { @@ -118,7 +118,7 @@ public abstract class RecurringHostedServiceBase : IHostedService, IDisposable } /// - public Task StopAsync(CancellationToken cancellationToken) + public virtual Task StopAsync(CancellationToken cancellationToken) { _period = Timeout.InfiniteTimeSpan; _timer?.Change(Timeout.Infinite, 0); diff --git a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs index 7184aaf16e..77800ae7d6 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs @@ -13,6 +13,7 @@ using Umbraco.Cms.Core.Telemetry.Models; namespace Umbraco.Cms.Infrastructure.HostedServices; +[Obsolete("Use Umbraco.Cms.Infrastructure.BackgroundJobs.ReportSiteJob instead. This class will be removed in Umbraco 14.")] public class ReportSiteTask : RecurringHostedServiceBase { private static HttpClient _httpClient = new(); diff --git a/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs b/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs index da1fbaf157..efbd8017df 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs @@ -17,6 +17,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices; /// /// Runs only on non-replica servers. /// +[Obsolete("Use Umbraco.Cms.Infrastructure.BackgroundJobs.ScheduledPublishingJob instead. This class will be removed in Umbraco 14.")] public class ScheduledPublishing : RecurringHostedServiceBase { private readonly IContentService _contentService; diff --git a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs index e4e5700496..fbbdab8878 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs @@ -13,6 +13,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration; /// /// Implements periodic database instruction processing as a hosted service. /// +[Obsolete("Use Umbraco.Cms.Infrastructure.BackgroundJobs.ServerRegistration.InstructionProcessJob instead. This class will be removed in Umbraco 14.")] public class InstructionProcessTask : RecurringHostedServiceBase { private readonly ILogger _logger; diff --git a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs index 730282c6b0..a844c33ad6 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs @@ -15,6 +15,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration; /// /// Implements periodic server "touching" (to mark as active/deactive) as a hosted service. /// +[Obsolete("Use Umbraco.Cms.Infrastructure.BackgroundJobs.ServerRegistration.TouchServerJob instead. This class will be removed in Umbraco 14.")] public class TouchServerTask : RecurringHostedServiceBase { private readonly IHostingEnvironment _hostingEnvironment; diff --git a/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs b/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs index cf46e38750..81de651e79 100644 --- a/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs +++ b/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs @@ -14,6 +14,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices; /// 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. /// +[Obsolete("Use Umbraco.Cms.Infrastructure.BackgroundJobs.TempFileCleanupJob instead. This class will be removed in Umbraco 14.")] public class TempFileCleanup : RecurringHostedServiceBase { private readonly TimeSpan _age = TimeSpan.FromDays(1); diff --git a/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobExecutedNotification.cs b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobExecutedNotification.cs new file mode 100644 index 0000000000..8d2fbf96aa --- /dev/null +++ b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobExecutedNotification.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Infrastructure.BackgroundJobs; + +namespace Umbraco.Cms.Infrastructure.Notifications +{ + public sealed class RecurringBackgroundJobExecutedNotification : RecurringBackgroundJobNotification + { + public RecurringBackgroundJobExecutedNotification(IRecurringBackgroundJob target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobExecutingNotification.cs b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobExecutingNotification.cs new file mode 100644 index 0000000000..71f5cf3edc --- /dev/null +++ b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobExecutingNotification.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Infrastructure.BackgroundJobs; + +namespace Umbraco.Cms.Infrastructure.Notifications +{ + public sealed class RecurringBackgroundJobExecutingNotification : RecurringBackgroundJobNotification + { + public RecurringBackgroundJobExecutingNotification(IRecurringBackgroundJob target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobFailedNotification.cs b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobFailedNotification.cs new file mode 100644 index 0000000000..594f01fc7b --- /dev/null +++ b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobFailedNotification.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Infrastructure.BackgroundJobs; + +namespace Umbraco.Cms.Infrastructure.Notifications +{ + public sealed class RecurringBackgroundJobFailedNotification : RecurringBackgroundJobNotification + { + public RecurringBackgroundJobFailedNotification(IRecurringBackgroundJob target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobIgnoredNotification.cs b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobIgnoredNotification.cs new file mode 100644 index 0000000000..8c3a0079d7 --- /dev/null +++ b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobIgnoredNotification.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Infrastructure.BackgroundJobs; + +namespace Umbraco.Cms.Infrastructure.Notifications +{ + public sealed class RecurringBackgroundJobIgnoredNotification : RecurringBackgroundJobNotification + { + public RecurringBackgroundJobIgnoredNotification(IRecurringBackgroundJob target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobNotification.cs b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobNotification.cs new file mode 100644 index 0000000000..f9185cb412 --- /dev/null +++ b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobNotification.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Infrastructure.BackgroundJobs; + +namespace Umbraco.Cms.Infrastructure.Notifications +{ + public class RecurringBackgroundJobNotification : ObjectNotification + { + public IRecurringBackgroundJob Job { get; } + public RecurringBackgroundJobNotification(IRecurringBackgroundJob target, EventMessages messages) : base(target, messages) => Job = target; + } +} diff --git a/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStartedNotification.cs b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStartedNotification.cs new file mode 100644 index 0000000000..dca1e69d40 --- /dev/null +++ b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStartedNotification.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Infrastructure.BackgroundJobs; + +namespace Umbraco.Cms.Infrastructure.Notifications +{ + public sealed class RecurringBackgroundJobStartedNotification : RecurringBackgroundJobNotification + { + public RecurringBackgroundJobStartedNotification(IRecurringBackgroundJob target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStartingNotification.cs b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStartingNotification.cs new file mode 100644 index 0000000000..3ee8d2a710 --- /dev/null +++ b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStartingNotification.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Infrastructure.BackgroundJobs; + +namespace Umbraco.Cms.Infrastructure.Notifications +{ + public sealed class RecurringBackgroundJobStartingNotification : RecurringBackgroundJobNotification + { + public RecurringBackgroundJobStartingNotification(IRecurringBackgroundJob target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStoppedNotification.cs b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStoppedNotification.cs new file mode 100644 index 0000000000..a1df71a4ee --- /dev/null +++ b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStoppedNotification.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Infrastructure.BackgroundJobs; + +namespace Umbraco.Cms.Infrastructure.Notifications +{ + public sealed class RecurringBackgroundJobStoppedNotification : RecurringBackgroundJobNotification + { + public RecurringBackgroundJobStoppedNotification(IRecurringBackgroundJob target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStoppingNotification.cs b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStoppingNotification.cs new file mode 100644 index 0000000000..985a20e286 --- /dev/null +++ b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStoppingNotification.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Infrastructure.BackgroundJobs; + +namespace Umbraco.Cms.Infrastructure.Notifications +{ + public sealed class RecurringBackgroundJobStoppingNotification : RecurringBackgroundJobNotification + { + public RecurringBackgroundJobStoppingNotification(IRecurringBackgroundJob target, EventMessages messages) : base(target, messages) + { + } + } +} diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index dfa22b06e8..7c23ce19da 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -46,7 +46,7 @@ public static partial class UmbracoBuilderExtensions .AddMvcAndRazor(configureMvc) .AddWebServer() .AddPreviewSupport() - .AddHostedServices() + .AddRecurringBackgroundJobs() .AddNuCache() .AddDistributedCache() .TryAddModelsBuilderDashboard() diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 2dd828f9c2..71704f4edc 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Serilog.Extensions.Logging; @@ -38,6 +39,9 @@ using Umbraco.Cms.Core.Telemetry; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Core.WebAssets; +using Umbraco.Cms.Infrastructure.BackgroundJobs; +using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs; +using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs.ServerRegistration; using Umbraco.Cms.Infrastructure.DependencyInjection; using Umbraco.Cms.Infrastructure.HostedServices; using Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration; @@ -176,6 +180,7 @@ public static partial class UmbracoBuilderExtensions /// /// Add Umbraco hosted services /// + [Obsolete("Use AddRecurringBackgroundJobs instead")] public static IUmbracoBuilder AddHostedServices(this IUmbracoBuilder builder) { builder.Services.AddHostedService(); @@ -191,6 +196,36 @@ public static partial class UmbracoBuilderExtensions new ReportSiteTask( provider.GetRequiredService>(), provider.GetRequiredService())); + + + return builder; + } + + /// + /// Add Umbraco recurring background jobs + /// + public static IUmbracoBuilder AddRecurringBackgroundJobs(this IUmbracoBuilder builder) + { + // Add background jobs + builder.Services.AddRecurringBackgroundJob(); + builder.Services.AddRecurringBackgroundJob(); + builder.Services.AddRecurringBackgroundJob(); + builder.Services.AddRecurringBackgroundJob(); + builder.Services.AddRecurringBackgroundJob(); + builder.Services.AddRecurringBackgroundJob(); + builder.Services.AddRecurringBackgroundJob(); + builder.Services.AddRecurringBackgroundJob(); + builder.Services.AddRecurringBackgroundJob(provider => + new ReportSiteJob( + provider.GetRequiredService>(), + provider.GetRequiredService())); + + + builder.Services.AddHostedService(); + builder.Services.AddSingleton(RecurringBackgroundJobHostedService.CreateHostedServiceFactory); + builder.Services.AddHostedService(); + + return builder; } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/HealthCheckNotifierJobTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/HealthCheckNotifierJobTests.cs new file mode 100644 index 0000000000..cf9883603b --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/HealthCheckNotifierJobTests.cs @@ -0,0 +1,139 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.HealthChecks; +using Umbraco.Cms.Core.HealthChecks.NotificationMethods; +using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Runtime; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Infrastructure.BackgroundJobs; +using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs; +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Cms.Tests.Common; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.BackgroundJobs.Jobs; + +[TestFixture] +public class HealthCheckNotifierJobTests +{ + private Mock _mockNotificationMethod; + + private const string Check1Id = "00000000-0000-0000-0000-000000000001"; + private const string Check2Id = "00000000-0000-0000-0000-000000000002"; + private const string Check3Id = "00000000-0000-0000-0000-000000000003"; + + [Test] + public async Task Does_Not_Execute_When_Not_Enabled() + { + var sut = CreateHealthCheckNotifier(false); + await sut.RunJobAsync(); + VerifyNotificationsNotSent(); + } + + [Test] + public async Task Does_Not_Execute_With_No_Enabled_Notification_Methods() + { + var sut = CreateHealthCheckNotifier(notificationEnabled: false); + await sut.RunJobAsync(); + VerifyNotificationsNotSent(); + } + + [Test] + public async Task Executes_With_Enabled_Notification_Methods() + { + var sut = CreateHealthCheckNotifier(); + await sut.RunJobAsync(); + VerifyNotificationsSent(); + } + + [Test] + public async Task Executes_Only_Enabled_Checks() + { + var sut = CreateHealthCheckNotifier(); + await sut.RunJobAsync(); + _mockNotificationMethod.Verify( + x => x.SendAsync( + It.Is(y => + y.ResultsAsDictionary.Count == 1 && y.ResultsAsDictionary.ContainsKey("Check1"))), + Times.Once); + } + + private HealthCheckNotifierJob CreateHealthCheckNotifier( + bool enabled = true, + bool notificationEnabled = true) + { + var settings = new HealthChecksSettings + { + Notification = new HealthChecksNotificationSettings + { + Enabled = enabled, + DisabledChecks = new List { new() { Id = Guid.Parse(Check3Id) } }, + }, + DisabledChecks = new List { new() { Id = Guid.Parse(Check2Id) } }, + }; + var checks = new HealthCheckCollection(() => new List + { + new TestHealthCheck1(), + new TestHealthCheck2(), + new TestHealthCheck3(), + }); + + _mockNotificationMethod = new Mock(); + _mockNotificationMethod.SetupGet(x => x.Enabled).Returns(notificationEnabled); + var notifications = new HealthCheckNotificationMethodCollection(() => + new List { _mockNotificationMethod.Object }); + + + var mockScopeProvider = new Mock(); + var mockLogger = new Mock>(); + var mockProfilingLogger = new Mock(); + + return new HealthCheckNotifierJob( + new TestOptionsMonitor(settings), + checks, + notifications, + mockScopeProvider.Object, + mockLogger.Object, + mockProfilingLogger.Object, + Mock.Of()); + } + + private void VerifyNotificationsNotSent() => VerifyNotificationsSentTimes(Times.Never()); + + private void VerifyNotificationsSent() => VerifyNotificationsSentTimes(Times.Once()); + + private void VerifyNotificationsSentTimes(Times times) => + _mockNotificationMethod.Verify(x => x.SendAsync(It.IsAny()), times); + + [HealthCheck(Check1Id, "Check1")] + private class TestHealthCheck1 : TestHealthCheck + { + } + + [HealthCheck(Check2Id, "Check2")] + private class TestHealthCheck2 : TestHealthCheck + { + } + + [HealthCheck(Check3Id, "Check3")] + private class TestHealthCheck3 : TestHealthCheck + { + } + + private class TestHealthCheck : HealthCheck + { + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) => new("Check message"); + + public override async Task> GetStatus() => Enumerable.Empty(); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/KeepAliveJobTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/KeepAliveJobTests.cs new file mode 100644 index 0000000000..6821cbcccc --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/KeepAliveJobTests.cs @@ -0,0 +1,94 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using Moq.Protected; +using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Runtime; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs; +using Umbraco.Cms.Infrastructure.HostedServices; +using Umbraco.Cms.Tests.Common; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.BackgroundJobs.Jobs; + +[TestFixture] +public class KeepAliveJobTests +{ + private Mock _mockHttpMessageHandler; + + private const string ApplicationUrl = "https://mysite.com"; + + [Test] + public async Task Does_Not_Execute_When_Not_Enabled() + { + var sut = CreateKeepAlive(false); + await sut.RunJobAsync(); + VerifyKeepAliveRequestNotSent(); + } + + + [Test] + public async Task Executes_And_Calls_Ping_Url() + { + var sut = CreateKeepAlive(); + await sut.RunJobAsync(); + VerifyKeepAliveRequestSent(); + } + + private KeepAliveJob CreateKeepAlive( + bool enabled = true) + { + var settings = new KeepAliveSettings { DisableKeepAliveTask = !enabled }; + + var mockHostingEnvironment = new Mock(); + mockHostingEnvironment.SetupGet(x => x.ApplicationMainUrl).Returns(new Uri(ApplicationUrl)); + mockHostingEnvironment.Setup(x => x.ToAbsolute(It.IsAny())) + .Returns((string s) => s.TrimStart('~')); + + var mockScopeProvider = new Mock(); + var mockLogger = new Mock>(); + var mockProfilingLogger = new Mock(); + + _mockHttpMessageHandler = new Mock(); + _mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)) + .Verifiable(); + _mockHttpMessageHandler.As().Setup(s => s.Dispose()); + var httpClient = new HttpClient(_mockHttpMessageHandler.Object); + + var mockHttpClientFactory = new Mock(MockBehavior.Strict); + mockHttpClientFactory.Setup(x => x.CreateClient(It.IsAny())).Returns(httpClient); + + return new KeepAliveJob( + mockHostingEnvironment.Object, + new TestOptionsMonitor(settings), + mockLogger.Object, + mockProfilingLogger.Object, + mockHttpClientFactory.Object); + } + + private void VerifyKeepAliveRequestNotSent() => VerifyKeepAliveRequestSentTimes(Times.Never()); + + private void VerifyKeepAliveRequestSent() => VerifyKeepAliveRequestSentTimes(Times.Once()); + + private void VerifyKeepAliveRequestSentTimes(Times times) => _mockHttpMessageHandler.Protected() + .Verify( + "SendAsync", + times, + ItExpr.Is(x => x.RequestUri.ToString() == $"{ApplicationUrl}/api/keepalive/ping"), + ItExpr.IsAny()); +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/LogScrubberJobTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/LogScrubberJobTests.cs new file mode 100644 index 0000000000..6dd479364c --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/LogScrubberJobTests.cs @@ -0,0 +1,72 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Data; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Runtime; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs; +using Umbraco.Cms.Infrastructure.HostedServices; +using Umbraco.Cms.Tests.Common; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.BackgroundJobs.Jobs; + +[TestFixture] +public class LogScrubberJobTests +{ + private Mock _mockAuditService; + + private const int MaxLogAgeInMinutes = 60; + + [Test] + public async Task Executes_And_Scrubs_Logs() + { + var sut = CreateLogScrubber(); + await sut.RunJobAsync(); + VerifyLogsScrubbed(); + } + + private LogScrubberJob CreateLogScrubber() + { + var settings = new LoggingSettings { MaxLogAge = TimeSpan.FromMinutes(MaxLogAgeInMinutes) }; + + var mockScope = new Mock(); + var mockScopeProvider = new Mock(); + mockScopeProvider + .Setup(x => x.CreateCoreScope( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(mockScope.Object); + var mockLogger = new Mock>(); + var mockProfilingLogger = new Mock(); + + _mockAuditService = new Mock(); + + return new LogScrubberJob( + _mockAuditService.Object, + new TestOptionsMonitor(settings), + mockScopeProvider.Object, + mockLogger.Object, + mockProfilingLogger.Object); + } + + private void VerifyLogsNotScrubbed() => VerifyLogsScrubbed(Times.Never()); + + private void VerifyLogsScrubbed() => VerifyLogsScrubbed(Times.Once()); + + private void VerifyLogsScrubbed(Times times) => + _mockAuditService.Verify(x => x.CleanLogs(It.Is(y => y == MaxLogAgeInMinutes)), times); +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/ScheduledPublishingJobTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/ScheduledPublishingJobTests.cs new file mode 100644 index 0000000000..eb1f0695c8 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/ScheduledPublishingJobTests.cs @@ -0,0 +1,92 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Data; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Runtime; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Infrastructure; +using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs; +using Umbraco.Cms.Infrastructure.HostedServices; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.BackgroundJobs.Jobs; + +[TestFixture] +public class ScheduledPublishingJobTests +{ + private Mock _mockContentService; + private Mock> _mockLogger; + + [Test] + public async Task Does_Not_Execute_When_Not_Enabled() + { + var sut = CreateScheduledPublishing(enabled: false); + await sut.RunJobAsync(); + VerifyScheduledPublishingNotPerformed(); + } + + [Test] + public async Task Executes_And_Performs_Scheduled_Publishing() + { + var sut = CreateScheduledPublishing(); + await sut.RunJobAsync(); + VerifyScheduledPublishingPerformed(); + } + + private ScheduledPublishingJob CreateScheduledPublishing( + bool enabled = true) + { + if (enabled) + { + Suspendable.ScheduledPublishing.Resume(); + } + else + { + Suspendable.ScheduledPublishing.Suspend(); + } + + _mockContentService = new Mock(); + + var mockUmbracoContextFactory = new Mock(); + mockUmbracoContextFactory.Setup(x => x.EnsureUmbracoContext()) + .Returns(new UmbracoContextReference(null, false, null)); + + _mockLogger = new Mock>(); + + var mockServerMessenger = new Mock(); + + var mockScopeProvider = new Mock(); + mockScopeProvider + .Setup(x => x.CreateCoreScope( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(Mock.Of()); + + return new ScheduledPublishingJob( + _mockContentService.Object, + mockUmbracoContextFactory.Object, + _mockLogger.Object, + mockServerMessenger.Object, + mockScopeProvider.Object); + } + + private void VerifyScheduledPublishingNotPerformed() => VerifyScheduledPublishingPerformed(Times.Never()); + + private void VerifyScheduledPublishingPerformed() => VerifyScheduledPublishingPerformed(Times.Once()); + + private void VerifyScheduledPublishingPerformed(Times times) => + _mockContentService.Verify(x => x.PerformScheduledPublish(It.IsAny()), times); +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/InstructionProcessJobTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/InstructionProcessJobTests.cs new file mode 100644 index 0000000000..f2e6e574ef --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/InstructionProcessJobTests.cs @@ -0,0 +1,51 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs.ServerRegistration; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.BackgroundJobs.Jobs.ServerRegistration; + +[TestFixture] +public class InstructionProcessJobTests +{ + private Mock _mockDatabaseServerMessenger; + + + [Test] + public async Task Executes_And_Touches_Server() + { + var sut = CreateInstructionProcessJob(); + await sut.RunJobAsync(); + VerifyMessengerSynced(); + } + + private InstructionProcessJob CreateInstructionProcessJob() + { + + var mockLogger = new Mock>(); + + _mockDatabaseServerMessenger = new Mock(); + + var settings = new GlobalSettings(); + + return new InstructionProcessJob( + _mockDatabaseServerMessenger.Object, + mockLogger.Object, + Options.Create(settings)); + } + + private void VerifyMessengerNotSynced() => VerifyMessengerSyncedTimes(Times.Never()); + + private void VerifyMessengerSynced() => VerifyMessengerSyncedTimes(Times.Once()); + + private void VerifyMessengerSyncedTimes(Times times) => _mockDatabaseServerMessenger.Verify(x => x.Sync(), times); +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/TouchServerJobTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/TouchServerJobTests.cs new file mode 100644 index 0000000000..0da3b15e1b --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/TouchServerJobTests.cs @@ -0,0 +1,95 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs.ServerRegistration; +using Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration; +using Umbraco.Cms.Tests.Common; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.BackgroundJobs.Jobs.ServerRegistration; + +[TestFixture] +public class TouchServerJobTests +{ + private Mock _mockServerRegistrationService; + + private const string ApplicationUrl = "https://mysite.com/"; + private readonly TimeSpan _staleServerTimeout = TimeSpan.FromMinutes(2); + + + [Test] + public async Task Does_Not_Execute_When_Application_Url_Is_Not_Available() + { + var sut = CreateTouchServerTask(applicationUrl: string.Empty); + await sut.RunJobAsync(); + VerifyServerNotTouched(); + } + + [Test] + public async Task Executes_And_Touches_Server() + { + var sut = CreateTouchServerTask(); + await sut.RunJobAsync(); + VerifyServerTouched(); + } + + [Test] + public async Task Does_Not_Execute_When_Role_Accessor_Is_Not_Elected() + { + var sut = CreateTouchServerTask(useElection: false); + await sut.RunJobAsync(); + VerifyServerNotTouched(); + } + + private TouchServerJob CreateTouchServerTask( + RuntimeLevel runtimeLevel = RuntimeLevel.Run, + string applicationUrl = ApplicationUrl, + bool useElection = true) + { + var mockRequestAccessor = new Mock(); + mockRequestAccessor.SetupGet(x => x.ApplicationMainUrl) + .Returns(!string.IsNullOrEmpty(applicationUrl) ? new Uri(ApplicationUrl) : null); + + var mockRunTimeState = new Mock(); + mockRunTimeState.SetupGet(x => x.Level).Returns(runtimeLevel); + + var mockLogger = new Mock>(); + + _mockServerRegistrationService = new Mock(); + + var settings = new GlobalSettings + { + DatabaseServerRegistrar = new DatabaseServerRegistrarSettings { StaleServerTimeout = _staleServerTimeout }, + }; + + IServerRoleAccessor roleAccessor = useElection + ? new ElectedServerRoleAccessor(_mockServerRegistrationService.Object) + : new SingleServerRoleAccessor(); + + return new TouchServerJob( + _mockServerRegistrationService.Object, + mockRequestAccessor.Object, + mockLogger.Object, + new TestOptionsMonitor(settings), + roleAccessor); + } + + private void VerifyServerNotTouched() => VerifyServerTouchedTimes(Times.Never()); + + private void VerifyServerTouched() => VerifyServerTouchedTimes(Times.Once()); + + private void VerifyServerTouchedTimes(Times times) => _mockServerRegistrationService + .Verify( + x => x.TouchServer( + It.Is(y => y == ApplicationUrl), + It.Is(y => y == _staleServerTimeout)), + times); +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/TempFileCleanupJobTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/TempFileCleanupJobTests.cs new file mode 100644 index 0000000000..c37094e6ac --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/TempFileCleanupJobTests.cs @@ -0,0 +1,50 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Runtime; +using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.BackgroundJobs.Jobs +{ + [TestFixture] + public class TempFileCleanupJobTests + { + private Mock _mockIOHelper; + private readonly string _testPath = Path.Combine(TestContext.CurrentContext.TestDirectory.Split("bin")[0], "App_Data", "TEMP"); + + + [Test] + public async Task Executes_And_Cleans_Files() + { + TempFileCleanupJob sut = CreateTempFileCleanupJob(); + await sut.RunJobAsync(); + VerifyFilesCleaned(); + } + + private TempFileCleanupJob CreateTempFileCleanupJob() + { + + _mockIOHelper = new Mock(); + _mockIOHelper.Setup(x => x.GetTempFolders()) + .Returns(new DirectoryInfo[] { new(_testPath) }); + _mockIOHelper.Setup(x => x.CleanFolder(It.IsAny(), It.IsAny())) + .Returns(CleanFolderResult.Success()); + + var mockLogger = new Mock>(); + + return new TempFileCleanupJob(_mockIOHelper.Object,mockLogger.Object); + } + + private void VerifyFilesNotCleaned() => VerifyFilesCleaned(Times.Never()); + + private void VerifyFilesCleaned() => VerifyFilesCleaned(Times.Once()); + + private void VerifyFilesCleaned(Times times) => _mockIOHelper.Verify(x => x.CleanFolder(It.Is(y => y.FullName == _testPath), It.IsAny()), times); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobHostedServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobHostedServiceTests.cs new file mode 100644 index 0000000000..806520bf41 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobHostedServiceTests.cs @@ -0,0 +1,208 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Runtime; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Infrastructure.BackgroundJobs; +using Umbraco.Cms.Infrastructure.HostedServices; +using Umbraco.Cms.Infrastructure.Notifications; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.BackgroundJobs; + +[TestFixture] +public class RecurringBackgroundJobHostedServiceTests +{ + + [TestCase(RuntimeLevel.Boot)] + [TestCase(RuntimeLevel.Install)] + [TestCase(RuntimeLevel.Unknown)] + [TestCase(RuntimeLevel.Upgrade)] + [TestCase(RuntimeLevel.BootFailed)] + public async Task Does_Not_Execute_When_Runtime_State_Is_Not_Run(RuntimeLevel runtimeLevel) + { + var mockJob = new Mock(); + + var sut = CreateRecurringBackgroundJobHostedService(mockJob, runtimeLevel: runtimeLevel); + await sut.PerformExecuteAsync(null); + + mockJob.Verify(job => job.RunJobAsync(), Times.Never); + } + + [Test] + public async Task Publishes_Ignored_Notification_When_Runtime_State_Is_Not_Run() + { + var mockJob = new Mock(); + var mockEventAggregator = new Mock(); + + var sut = CreateRecurringBackgroundJobHostedService(mockJob, runtimeLevel: RuntimeLevel.Unknown, mockEventAggregator: mockEventAggregator); + await sut.PerformExecuteAsync(null); + + mockEventAggregator.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + mockEventAggregator.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [TestCase(ServerRole.Unknown)] + [TestCase(ServerRole.Subscriber)] + public async Task Does_Not_Execute_When_Server_Role_Is_NotDefault(ServerRole serverRole) + { + var mockJob = new Mock(); + + var sut = CreateRecurringBackgroundJobHostedService(mockJob, serverRole: serverRole); + await sut.PerformExecuteAsync(null); + + mockJob.Verify(job => job.RunJobAsync(), Times.Never); + } + + [TestCase(ServerRole.Single)] + [TestCase(ServerRole.SchedulingPublisher)] + public async Task Does_Executes_When_Server_Role_Is_Default(ServerRole serverRole) + { + var mockJob = new Mock(); + mockJob.Setup(x => x.ServerRoles).Returns(IRecurringBackgroundJob.DefaultServerRoles); + + var sut = CreateRecurringBackgroundJobHostedService(mockJob, serverRole: serverRole); + await sut.PerformExecuteAsync(null); + + mockJob.Verify(job => job.RunJobAsync(), Times.Once); + } + + [Test] + public async Task Does_Execute_When_Server_Role_Is_Subscriber_And_Specified() + { + var mockJob = new Mock(); + mockJob.Setup(x => x.ServerRoles).Returns(new ServerRole[] { ServerRole.Subscriber }); + + var sut = CreateRecurringBackgroundJobHostedService(mockJob, serverRole: ServerRole.Subscriber); + await sut.PerformExecuteAsync(null); + + mockJob.Verify(job => job.RunJobAsync(), Times.Once); + } + + [Test] + public async Task Publishes_Ignored_Notification_When_Server_Role_Is_Not_Allowed() + { + var mockJob = new Mock(); + var mockEventAggregator = new Mock(); + + var sut = CreateRecurringBackgroundJobHostedService(mockJob, serverRole: ServerRole.Unknown, mockEventAggregator: mockEventAggregator); + await sut.PerformExecuteAsync(null); + + mockEventAggregator.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + mockEventAggregator.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Test] + public async Task Does_Not_Execute_When_Not_Main_Dom() + { + var mockJob = new Mock(); + + var sut = CreateRecurringBackgroundJobHostedService(mockJob, isMainDom: false); + await sut.PerformExecuteAsync(null); + + mockJob.Verify(job => job.RunJobAsync(), Times.Never); + } + + [Test] + public async Task Publishes_Ignored_Notification_When_Not_Main_Dom() + { + var mockJob = new Mock(); + var mockEventAggregator = new Mock(); + + var sut = CreateRecurringBackgroundJobHostedService(mockJob, isMainDom: false, mockEventAggregator: mockEventAggregator); + await sut.PerformExecuteAsync(null); + + mockEventAggregator.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + mockEventAggregator.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + + + [Test] + public async Task Publishes_Executed_Notification_When_Run() + { + var mockJob = new Mock(); + mockJob.Setup(x => x.ServerRoles).Returns(IRecurringBackgroundJob.DefaultServerRoles); + var mockEventAggregator = new Mock(); + + var sut = CreateRecurringBackgroundJobHostedService(mockJob, mockEventAggregator: mockEventAggregator); + await sut.PerformExecuteAsync(null); + + mockEventAggregator.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + mockEventAggregator.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Test] + public async Task Publishes_Failed_Notification_When_Fails() + { + var mockJob = new Mock(); + mockJob.Setup(x => x.ServerRoles).Returns(IRecurringBackgroundJob.DefaultServerRoles); + mockJob.Setup(x => x.RunJobAsync()).Throws(); + var mockEventAggregator = new Mock(); + + var sut = CreateRecurringBackgroundJobHostedService(mockJob, mockEventAggregator: mockEventAggregator); + await sut.PerformExecuteAsync(null); + + mockEventAggregator.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + mockEventAggregator.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Test] + public async Task Publishes_Start_And_Stop_Notifications() + { + var mockJob = new Mock(); + var mockEventAggregator = new Mock(); + + var sut = CreateRecurringBackgroundJobHostedService(mockJob, isMainDom: false, mockEventAggregator: mockEventAggregator); + await sut.StartAsync(CancellationToken.None); + await sut.StopAsync(CancellationToken.None); + + + mockEventAggregator.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + mockEventAggregator.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + + + mockEventAggregator.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + mockEventAggregator.Verify(x => x.PublishAsync(It.IsAny(), It.IsAny()), Times.Once); + + } + + + private RecurringHostedServiceBase CreateRecurringBackgroundJobHostedService( + Mock mockJob, + RuntimeLevel runtimeLevel = RuntimeLevel.Run, + ServerRole serverRole = ServerRole.Single, + bool isMainDom = true, + Mock mockEventAggregator = null) + { + var mockRunTimeState = new Mock(); + mockRunTimeState.SetupGet(x => x.Level).Returns(runtimeLevel); + + var mockServerRegistrar = new Mock(); + mockServerRegistrar.Setup(x => x.CurrentServerRole).Returns(serverRole); + + var mockMainDom = new Mock(); + mockMainDom.SetupGet(x => x.IsMainDom).Returns(isMainDom); + + var mockLogger = new Mock>>(); + if (mockEventAggregator == null) + { + mockEventAggregator = new Mock(); + } + + return new RecurringBackgroundJobHostedService( + mockRunTimeState.Object, + mockLogger.Object, + mockMainDom.Object, + mockServerRegistrar.Object, + mockEventAggregator.Object, + mockJob.Object); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/HealthCheckNotifierTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/HealthCheckNotifierTests.cs index 626129b3b7..03d7f344a6 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/HealthCheckNotifierTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/HealthCheckNotifierTests.cs @@ -23,6 +23,7 @@ using Umbraco.Cms.Tests.Common; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.HostedServices; [TestFixture] +[Obsolete("Replaced by BackgroundJobs.Jobs.HealthCheckNotifierJobTests")] public class HealthCheckNotifierTests { private Mock _mockNotificationMethod; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/KeepAliveTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/KeepAliveTests.cs index f0ef4cd278..4631bb21a1 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/KeepAliveTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/KeepAliveTests.cs @@ -21,6 +21,7 @@ using Umbraco.Cms.Tests.Common; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.HostedServices; [TestFixture] +[Obsolete("Replaced by BackgroundJobs.Jobs.KeepAliveJobTests")] public class KeepAliveTests { private Mock _mockHttpMessageHandler; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/LogScrubberTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/LogScrubberTests.cs index 553a4f451c..98fdf4c453 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/LogScrubberTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/LogScrubberTests.cs @@ -19,6 +19,7 @@ using Umbraco.Cms.Tests.Common; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.HostedServices; [TestFixture] +[Obsolete("Replaced by BackgroundJobs.Jobs.LogScrubberJobTests")] public class LogScrubberTests { private Mock _mockAuditService; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ScheduledPublishingTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ScheduledPublishingTests.cs index 609dfbb7fa..3eb7756d7f 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ScheduledPublishingTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ScheduledPublishingTests.cs @@ -19,6 +19,7 @@ using Umbraco.Cms.Infrastructure.HostedServices; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.HostedServices; [TestFixture] +[Obsolete("Replaced by BackgroundJobs.Jobs.ScheduledPublishingJobTests")] public class ScheduledPublishingTests { private Mock _mockContentService; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTaskTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTaskTests.cs index fd24d60019..1513c6a5d4 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTaskTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTaskTests.cs @@ -15,6 +15,7 @@ using Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.HostedServices.ServerRegistration; [TestFixture] +[Obsolete("Replaced by BackgroundJobs.Jobs.ServerRegistration.InstructionProcessJobTests")] public class InstructionProcessTaskTests { private Mock _mockDatabaseServerMessenger; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTaskTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTaskTests.cs index b379f8d34b..91d156e519 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTaskTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTaskTests.cs @@ -16,6 +16,7 @@ using Umbraco.Cms.Tests.Common; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.HostedServices.ServerRegistration; [TestFixture] +[Obsolete("Replaced by BackgroundJobs.Jobs.ServerRegistration.TouchServerJobTests")] public class TouchServerTaskTests { private Mock _mockServerRegistrationService; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/TempFileCleanupTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/TempFileCleanupTests.cs index 851afc269b..2128f917b9 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/TempFileCleanupTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/TempFileCleanupTests.cs @@ -13,6 +13,7 @@ using Umbraco.Cms.Infrastructure.HostedServices; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.HostedServices { [TestFixture] + [Obsolete("Replaced by BackgroundJobs.Jobs.TempFileCleanupTests")] public class TempFileCleanupTests { private Mock _mockIOHelper;