From c79b31ed2e23f77d01a3a42b4ea55ae66d923758 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 25 Jan 2021 09:26:38 +0100 Subject: [PATCH] Changed remaining background jobs to be either hosted services or real fire and forget + Cleanup + moved classes to the legacy test project, that is only needed there. (#9700) --- .../Scheduling/RecurringTaskBase.cs | 127 -- .../Scheduling/TaskAndFactoryExtensions.cs | 80 -- src/Umbraco.Core/TaskHelper.cs | 52 + .../UmbracoBuilder.CoreServices.cs | 5 + .../UmbracoBuilder.DistributedCache.cs | 2 +- .../HostedServices/BackgroundTaskQueue.cs | 42 + .../HostedServices/IBackgroundTaskQueue.cs | 25 + .../HostedServices/QueuedHostedService.cs | 60 + .../Scheduling/SimpleTask.cs | 32 - .../Search/BackgroundIndexRebuilder.cs | 120 +- .../Search/ExamineComponent.cs | 64 +- .../Search/ExamineFinalComponent.cs | 12 +- .../Umbraco.Infrastructure.csproj | 4 + .../TestHelpers/LogTestHelper.cs | 41 + .../Umbraco.Tests.Common.csproj | 1 + .../UmbracoBuilderExtensions.cs | 13 +- .../Umbraco.Core/TaskHelperTests.cs | 47 + .../BackgroundTaskRunner.cs | 0 .../BackgroundTaskRunnerOptions.cs | 0 .../LegacyBackgroundTask}/IBackgroundTask.cs | 0 .../IBackgroundTaskRunner.cs | 0 .../ILatchedBackgroundTask.cs | 0 .../LatchedBackgroundTaskBase.cs | 0 .../LegacyBackgroundTask}/TaskEventArgs.cs | 0 .../ThreadingTaskImmutable.cs | 0 .../Scheduling/BackgroundTaskRunnerTests.cs | 1026 ----------------- .../Scheduling/BackgroundTaskRunnerTests2.cs | 178 --- src/Umbraco.Tests/Umbraco.Tests.csproj | 10 +- .../UmbracoBuilderExtensions.cs | 1 + 29 files changed, 371 insertions(+), 1571 deletions(-) delete mode 100644 src/Umbraco.Core/Scheduling/RecurringTaskBase.cs delete mode 100644 src/Umbraco.Core/Scheduling/TaskAndFactoryExtensions.cs create mode 100644 src/Umbraco.Core/TaskHelper.cs create mode 100644 src/Umbraco.Infrastructure/HostedServices/BackgroundTaskQueue.cs create mode 100644 src/Umbraco.Infrastructure/HostedServices/IBackgroundTaskQueue.cs create mode 100644 src/Umbraco.Infrastructure/HostedServices/QueuedHostedService.cs delete mode 100644 src/Umbraco.Infrastructure/Scheduling/SimpleTask.cs create mode 100644 src/Umbraco.Tests.Common/TestHelpers/LogTestHelper.cs create mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Core/TaskHelperTests.cs rename src/{Umbraco.Infrastructure/Scheduling => Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask}/BackgroundTaskRunner.cs (100%) rename src/{Umbraco.Core/Scheduling => Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask}/BackgroundTaskRunnerOptions.cs (100%) rename src/{Umbraco.Core/Scheduling => Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask}/IBackgroundTask.cs (100%) rename src/{Umbraco.Core/Scheduling => Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask}/IBackgroundTaskRunner.cs (100%) rename src/{Umbraco.Core/Scheduling => Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask}/ILatchedBackgroundTask.cs (100%) rename src/{Umbraco.Core/Scheduling => Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask}/LatchedBackgroundTaskBase.cs (100%) rename src/{Umbraco.Core/Scheduling => Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask}/TaskEventArgs.cs (100%) rename src/{Umbraco.Core/Scheduling => Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask}/ThreadingTaskImmutable.cs (100%) delete mode 100644 src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs delete mode 100644 src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests2.cs diff --git a/src/Umbraco.Core/Scheduling/RecurringTaskBase.cs b/src/Umbraco.Core/Scheduling/RecurringTaskBase.cs deleted file mode 100644 index 4544711946..0000000000 --- a/src/Umbraco.Core/Scheduling/RecurringTaskBase.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Umbraco.Web.Scheduling -{ - /// - /// Provides a base class for recurring background tasks. - /// - /// Implement by overriding PerformRun or PerformRunAsync and then IsAsync accordingly, - /// depending on whether the task is implemented as a sync or async method. Run nor RunAsync are - /// sealed here as overriding them would break recurrence. And then optionally override - /// RunsOnShutdown, in order to indicate whether the latched task should run immediately on - /// shutdown, or just be abandoned (default). - public abstract class RecurringTaskBase : LatchedBackgroundTaskBase - { - private readonly IBackgroundTaskRunner _runner; - private readonly long _periodMilliseconds; - private readonly Timer _timer; - - /// - /// Initializes a new instance of the class. - /// - /// The task runner. - /// The delay. - /// The period. - /// The task will repeat itself periodically. Use this constructor to create a new task. - protected RecurringTaskBase(IBackgroundTaskRunner runner, long delayMilliseconds, long periodMilliseconds) - { - _runner = runner; - _periodMilliseconds = periodMilliseconds; - - // note - // must use the single-parameter constructor on Timer to avoid it from being GC'd - // read http://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer - - _timer = new Timer(_ => Release()); - _timer.Change(delayMilliseconds, 0); - } - - /// - /// Initializes a new instance of the class. - /// - /// The task runner. - /// The delay. - /// The period. - /// The task will repeat itself periodically. Use this constructor to create a new task. - protected RecurringTaskBase(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds) - { - _runner = runner; - _periodMilliseconds = periodMilliseconds; - - // note - // must use the single-parameter constructor on Timer to avoid it from being GC'd - // read http://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer - - _timer = new Timer(_ => Release()); - _timer.Change(delayMilliseconds, 0); - } - - /// - /// Implements IBackgroundTask.Run(). - /// - /// Classes inheriting from RecurringTaskBase must implement PerformRun. - public sealed override void Run() - { - var shouldRepeat = PerformRun(); - if (shouldRepeat) Repeat(); - } - - /// - /// Implements IBackgroundTask.RunAsync(). - /// - /// Classes inheriting from RecurringTaskBase must implement PerformRun. - public sealed override async Task RunAsync(CancellationToken token) - { - var shouldRepeat = await PerformRunAsync(token); - if (shouldRepeat) Repeat(); - } - - private void Repeat() - { - // again? - if (_runner.IsCompleted) return; // fail fast - - if (_periodMilliseconds == 0) return; // safe - - Reset(); // re-latch - - // try to add again (may fail if runner has completed) - // if added, re-start the timer, else kill it - if (_runner.TryAdd(this)) - _timer.Change(_periodMilliseconds, 0); - else - Dispose(); - } - - /// - /// Runs the background task. - /// - /// A value indicating whether to repeat the task. - public virtual bool PerformRun() - { - throw new NotSupportedException("This task cannot run synchronously."); - } - - /// - /// Runs the task asynchronously. - /// - /// A cancellation token. - /// A instance representing the execution of the background task, - /// and returning a value indicating whether to repeat the task. - public virtual Task PerformRunAsync(CancellationToken token) - { - throw new NotSupportedException("This task cannot run asynchronously."); - } - - protected override void DisposeResources() - { - base.DisposeResources(); - - // stop the timer - _timer.Change(Timeout.Infinite, Timeout.Infinite); - _timer.Dispose(); - } - } -} diff --git a/src/Umbraco.Core/Scheduling/TaskAndFactoryExtensions.cs b/src/Umbraco.Core/Scheduling/TaskAndFactoryExtensions.cs deleted file mode 100644 index 557fe37709..0000000000 --- a/src/Umbraco.Core/Scheduling/TaskAndFactoryExtensions.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Umbraco.Web.Scheduling -{ - internal static class TaskAndFactoryExtensions - { - #region Task Extensions - - // TODO: Not used, is this used in Deploy or something? - static void SetCompletionSource(TaskCompletionSource completionSource, Task task) - { - if (task.IsFaulted) - completionSource.SetException(task.Exception.InnerException); - else - completionSource.SetResult(default(TResult)); - } - - // TODO: Not used, is this used in Deploy or something? - static void SetCompletionSource(TaskCompletionSource completionSource, Task task) - { - if (task.IsFaulted) - completionSource.SetException(task.Exception.InnerException); - else - completionSource.SetResult(task.Result); - } - - // TODO: Not used, is this used in Deploy or something? - public static Task ContinueWithTask(this Task task, Func continuation) - { - var completionSource = new TaskCompletionSource(); - task.ContinueWith(atask => continuation(atask).ContinueWith( - atask2 => SetCompletionSource(completionSource, atask2), - // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html - TaskScheduler.Default), - // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html - TaskScheduler.Default); - return completionSource.Task; - } - - // TODO: Not used, is this used in Deploy or something? - public static Task ContinueWithTask(this Task task, Func continuation, CancellationToken token) - { - var completionSource = new TaskCompletionSource(); - task.ContinueWith(atask => continuation(atask).ContinueWith( - atask2 => SetCompletionSource(completionSource, atask2), - token, - TaskContinuationOptions.None, - // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html - TaskScheduler.Default), - token, - TaskContinuationOptions.None, - // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html - TaskScheduler.Default); - return completionSource.Task; - } - - #endregion - - #region TaskFactory Extensions - - public static Task Completed(this TaskFactory factory) - { - var taskSource = new TaskCompletionSource(); - taskSource.SetResult(null); - return taskSource.Task; - } - - public static Task Sync(this TaskFactory factory, Action action) - { - var taskSource = new TaskCompletionSource(); - action(); - taskSource.SetResult(null); - return taskSource.Task; - } - - #endregion - } -} diff --git a/src/Umbraco.Core/TaskHelper.cs b/src/Umbraco.Core/TaskHelper.cs new file mode 100644 index 0000000000..6afa94a941 --- /dev/null +++ b/src/Umbraco.Core/TaskHelper.cs @@ -0,0 +1,52 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Umbraco.Core +{ + /// + /// Helper class to not repeat common patterns with Task. + /// + public class TaskHelper + { + private readonly ILogger _logger; + + public TaskHelper(ILogger logger) + { + _logger = logger; + } + + /// + /// Runs a TPL Task fire-and-forget style, the right way - in the + /// background, separate from the current thread, with no risk + /// of it trying to rejoin the current thread. + /// + public void RunBackgroundTask(Func fn) => Task.Run(LoggingWrapper(fn)).ConfigureAwait(false); + + /// + /// Runs a task fire-and-forget style and notifies the TPL that this + /// will not need a Thread to resume on for a long time, or that there + /// are multiple gaps in thread use that may be long. + /// Use for example when talking to a slow webservice. + /// + public void RunLongRunningBackgroundTask(Func fn) => + Task.Factory.StartNew(LoggingWrapper(fn), TaskCreationOptions.LongRunning) + .ConfigureAwait(false); + + private Func LoggingWrapper(Func fn) => + async () => + { + try + { + await fn(); + } + catch (Exception e) + { + _logger.LogError(e, "Exception thrown in a background thread"); + } + }; + } +} diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 8ce9839f6b..53f2d29709 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -27,6 +27,7 @@ using Umbraco.Core.Strings; using Umbraco.Core.Templates; using Umbraco.Examine; using Umbraco.Infrastructure.Examine; +using Umbraco.Infrastructure.HostedServices; using Umbraco.Infrastructure.Logging.Serilog.Enrichers; using Umbraco.Infrastructure.Media; using Umbraco.Infrastructure.Runtime; @@ -170,6 +171,10 @@ namespace Umbraco.Infrastructure.DependencyInjection builder.AddInstaller(); + // Services required to run background jobs (with out the handler) + builder.Services.AddUnique(); + builder.Services.AddUnique(); + return builder; } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs index db1d22e86c..bff55c12e0 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs @@ -123,7 +123,7 @@ namespace Umbraco.Infrastructure.DependencyInjection () => { var indexRebuilder = factory.GetRequiredService(); - indexRebuilder.RebuildIndexes(false, 5000); + indexRebuilder.RebuildIndexes(false, TimeSpan.FromSeconds(5)); } } }; diff --git a/src/Umbraco.Infrastructure/HostedServices/BackgroundTaskQueue.cs b/src/Umbraco.Infrastructure/HostedServices/BackgroundTaskQueue.cs new file mode 100644 index 0000000000..152bb7c14f --- /dev/null +++ b/src/Umbraco.Infrastructure/HostedServices/BackgroundTaskQueue.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; + +namespace Umbraco.Infrastructure.HostedServices +{ + /// + /// A Background Task Queue, to enqueue tasks for executing in the background. + /// + /// + /// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0 + /// + public class BackgroundTaskQueue : IBackgroundTaskQueue + { + private readonly ConcurrentQueue> _workItems = + new ConcurrentQueue>(); + + private readonly SemaphoreSlim _signal = new SemaphoreSlim(0); + + /// + public void QueueBackgroundWorkItem(Func workItem) + { + if (workItem == null) + { + throw new ArgumentNullException(nameof(workItem)); + } + + _workItems.Enqueue(workItem); + _signal.Release(); + } + + /// + public async Task> DequeueAsync(CancellationToken cancellationToken) + { + await _signal.WaitAsync(cancellationToken); + _workItems.TryDequeue(out Func workItem); + + return workItem; + } + } +} diff --git a/src/Umbraco.Infrastructure/HostedServices/IBackgroundTaskQueue.cs b/src/Umbraco.Infrastructure/HostedServices/IBackgroundTaskQueue.cs new file mode 100644 index 0000000000..7dba27bccb --- /dev/null +++ b/src/Umbraco.Infrastructure/HostedServices/IBackgroundTaskQueue.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Umbraco.Infrastructure.HostedServices +{ + /// + /// A Background Task Queue, to enqueue tasks for executing in the background. + /// + /// + /// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0 + /// + public interface IBackgroundTaskQueue + { + /// + /// Enqueue a work item to be executed on in the background. + /// + void QueueBackgroundWorkItem(Func workItem); + + /// + /// Dequeue the first item on the queue. + /// + Task> DequeueAsync(CancellationToken cancellationToken); + } +} diff --git a/src/Umbraco.Infrastructure/HostedServices/QueuedHostedService.cs b/src/Umbraco.Infrastructure/HostedServices/QueuedHostedService.cs new file mode 100644 index 0000000000..a330cb7811 --- /dev/null +++ b/src/Umbraco.Infrastructure/HostedServices/QueuedHostedService.cs @@ -0,0 +1,60 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Umbraco.Infrastructure.HostedServices +{ + + /// + /// A queue based hosted service used to executing tasks on a background thread. + /// + /// + /// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0 + /// + public class QueuedHostedService : BackgroundService + { + private readonly ILogger _logger; + + public QueuedHostedService(IBackgroundTaskQueue taskQueue, + ILogger logger) + { + TaskQueue = taskQueue; + _logger = logger; + } + + public IBackgroundTaskQueue TaskQueue { get; } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await BackgroundProcessing(stoppingToken); + } + + private async Task BackgroundProcessing(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + var workItem = + await TaskQueue.DequeueAsync(stoppingToken); + + try + { + await workItem(stoppingToken); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error occurred executing {WorkItem}.", nameof(workItem)); + } + } + } + + public override async Task StopAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Queued Hosted Service is stopping."); + + await base.StopAsync(stoppingToken); + } + } +} diff --git a/src/Umbraco.Infrastructure/Scheduling/SimpleTask.cs b/src/Umbraco.Infrastructure/Scheduling/SimpleTask.cs deleted file mode 100644 index a8603915b0..0000000000 --- a/src/Umbraco.Infrastructure/Scheduling/SimpleTask.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Umbraco.Web.Scheduling -{ - /// - /// A simple task that executes a delegate synchronously - /// - internal class SimpleTask : IBackgroundTask - { - private readonly Action _action; - - public SimpleTask(Action action) - { - _action = action; - } - - public bool IsAsync => false; - - public void Run() => _action(); - - public Task RunAsync(CancellationToken token) - { - throw new NotImplementedException(); - } - - public void Dispose() - { - } - } -} diff --git a/src/Umbraco.Infrastructure/Search/BackgroundIndexRebuilder.cs b/src/Umbraco.Infrastructure/Search/BackgroundIndexRebuilder.cs index 3f50c0b38c..6f69dd0ad8 100644 --- a/src/Umbraco.Infrastructure/Search/BackgroundIndexRebuilder.cs +++ b/src/Umbraco.Infrastructure/Search/BackgroundIndexRebuilder.cs @@ -1,12 +1,13 @@ -using System; +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System; using System.Threading; -using Umbraco.Core.Logging; -using Umbraco.Examine; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Umbraco.Core; -using Umbraco.Core.Hosting; -using Umbraco.Web.Scheduling; +using Umbraco.Examine; +using Umbraco.Infrastructure.HostedServices; namespace Umbraco.Web.Search { @@ -15,115 +16,68 @@ namespace Umbraco.Web.Search /// public class BackgroundIndexRebuilder { - private static readonly object RebuildLocker = new object(); private readonly IndexRebuilder _indexRebuilder; - private readonly IMainDom _mainDom; - // TODO: Remove unused ProfilingLogger? - private readonly IProfilingLogger _profilingLogger; - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - private readonly IApplicationShutdownRegistry _hostingEnvironment; - private static BackgroundTaskRunner _rebuildOnStartupRunner; + private readonly IBackgroundTaskQueue _backgroundTaskQueue; - public BackgroundIndexRebuilder(IMainDom mainDom, IProfilingLogger profilingLogger , ILoggerFactory loggerFactory, IApplicationShutdownRegistry hostingEnvironment, IndexRebuilder indexRebuilder) + private readonly IMainDom _mainDom; + private readonly ILogger _logger; + + private volatile bool _isRunning = false; + private static readonly object s_rebuildLocker = new object(); + + /// + /// Initializes a new instance of the class. + /// + public BackgroundIndexRebuilder( + IMainDom mainDom, + ILogger logger, + IndexRebuilder indexRebuilder, + IBackgroundTaskQueue backgroundTaskQueue) { _mainDom = mainDom; - _profilingLogger = profilingLogger ; - _loggerFactory = loggerFactory; - _logger = loggerFactory.CreateLogger(); - _hostingEnvironment = hostingEnvironment; + _logger = logger; _indexRebuilder = indexRebuilder; + _backgroundTaskQueue = backgroundTaskQueue; } + /// /// Called to rebuild empty indexes on startup /// - /// - /// - public virtual void RebuildIndexes(bool onlyEmptyIndexes, int waitMilliseconds = 0) + public virtual void RebuildIndexes(bool onlyEmptyIndexes, TimeSpan? delay = null) { - // TODO: need a way to disable rebuilding on startup - lock (RebuildLocker) + lock (s_rebuildLocker) { - if (_rebuildOnStartupRunner != null && _rebuildOnStartupRunner.IsRunning) + if (_isRunning) { _logger.LogWarning("Call was made to RebuildIndexes but the task runner for rebuilding is already running"); return; } _logger.LogInformation("Starting initialize async background thread."); - //do the rebuild on a managed background thread - var task = new RebuildOnStartupTask(_mainDom, _indexRebuilder, _loggerFactory.CreateLogger(), onlyEmptyIndexes, waitMilliseconds); - _rebuildOnStartupRunner = new BackgroundTaskRunner( - "RebuildIndexesOnStartup", - _loggerFactory.CreateLogger>(), _hostingEnvironment); + _backgroundTaskQueue.QueueBackgroundWorkItem(cancellationToken => RebuildIndexes(onlyEmptyIndexes, delay ?? TimeSpan.Zero, cancellationToken)); - _rebuildOnStartupRunner.TryAdd(task); } } - /// - /// Background task used to rebuild empty indexes on startup - /// - private class RebuildOnStartupTask : IBackgroundTask + private Task RebuildIndexes(bool onlyEmptyIndexes, TimeSpan delay, CancellationToken cancellationToken) { - private readonly IMainDom _mainDom; - - private readonly IndexRebuilder _indexRebuilder; - private readonly ILogger _logger; - private readonly bool _onlyEmptyIndexes; - private readonly int _waitMilliseconds; - - public RebuildOnStartupTask(IMainDom mainDom, - IndexRebuilder indexRebuilder, ILogger logger, bool onlyEmptyIndexes, int waitMilliseconds = 0) + if (!_mainDom.IsMainDom) { - _mainDom = mainDom; - _indexRebuilder = indexRebuilder ?? throw new ArgumentNullException(nameof(indexRebuilder)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _onlyEmptyIndexes = onlyEmptyIndexes; - _waitMilliseconds = waitMilliseconds; + return Task.CompletedTask; } - public bool IsAsync => false; - - public void Dispose() + if (delay > TimeSpan.Zero) { + Thread.Sleep(delay); } - public void Run() - { - try - { - // rebuilds indexes - RebuildIndexes(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to rebuild empty indexes."); - } - } - - public Task RunAsync(CancellationToken token) - { - throw new NotImplementedException(); - } - - /// - /// Used to rebuild indexes on startup or cold boot - /// - private void RebuildIndexes() - { - //do not attempt to do this if this has been disabled since we are not the main dom. - //this can be called during a cold boot - if (!_mainDom.IsMainDom) return; - - if (_waitMilliseconds > 0) - Thread.Sleep(_waitMilliseconds); - - _indexRebuilder.RebuildIndexes(_onlyEmptyIndexes); - } + _isRunning = true; + _indexRebuilder.RebuildIndexes(onlyEmptyIndexes); + _isRunning = false; + return Task.CompletedTask; } } } diff --git a/src/Umbraco.Infrastructure/Search/ExamineComponent.cs b/src/Umbraco.Infrastructure/Search/ExamineComponent.cs index 6c6e566acb..6aed199202 100644 --- a/src/Umbraco.Infrastructure/Search/ExamineComponent.cs +++ b/src/Umbraco.Infrastructure/Search/ExamineComponent.cs @@ -1,21 +1,19 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using Examine; +using Microsoft.Extensions.Logging; using Umbraco.Core; using Umbraco.Core.Cache; -using Umbraco.Core.Hosting; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Services.Changes; using Umbraco.Core.Sync; -using Umbraco.Web.Cache; using Umbraco.Examine; -using Microsoft.Extensions.Logging; -using Umbraco.Web.Scheduling; +using Umbraco.Web.Cache; namespace Umbraco.Web.Search { @@ -29,13 +27,13 @@ namespace Umbraco.Web.Search private readonly IValueSetBuilder _mediaValueSetBuilder; private readonly IValueSetBuilder _memberValueSetBuilder; private readonly BackgroundIndexRebuilder _backgroundIndexRebuilder; + private readonly TaskHelper _taskHelper; private readonly IScopeProvider _scopeProvider; private readonly ServiceContext _services; private readonly IMainDom _mainDom; private readonly IProfilingLogger _profilingLogger; private readonly ILogger _logger; private readonly IUmbracoIndexesCreator _indexCreator; - private readonly BackgroundTaskRunner _indexItemTaskRunner; // the default enlist priority is 100 // enlist with a lower priority to ensure that anything "default" runs after us @@ -52,7 +50,7 @@ namespace Umbraco.Web.Search IValueSetBuilder mediaValueSetBuilder, IValueSetBuilder memberValueSetBuilder, BackgroundIndexRebuilder backgroundIndexRebuilder, - IApplicationShutdownRegistry applicationShutdownRegistry) + TaskHelper taskHelper) { _services = services; _scopeProvider = scopeProvider; @@ -62,11 +60,11 @@ namespace Umbraco.Web.Search _mediaValueSetBuilder = mediaValueSetBuilder; _memberValueSetBuilder = memberValueSetBuilder; _backgroundIndexRebuilder = backgroundIndexRebuilder; + _taskHelper = taskHelper; _mainDom = mainDom; _profilingLogger = profilingLogger; _logger = loggerFactory.CreateLogger(); _indexCreator = indexCreator; - _indexItemTaskRunner = new BackgroundTaskRunner(loggerFactory.CreateLogger>(), applicationShutdownRegistry); } public void Initialize() @@ -510,27 +508,27 @@ namespace Umbraco.Web.Search { var actions = DeferedActions.Get(_scopeProvider); if (actions != null) - actions.Add(new DeferedReIndexForContent(this, sender, isPublished)); + actions.Add(new DeferedReIndexForContent(_taskHelper, this, sender, isPublished)); else - DeferedReIndexForContent.Execute(this, sender, isPublished); + DeferedReIndexForContent.Execute(_taskHelper, this, sender, isPublished); } private void ReIndexForMember(IMember member) { var actions = DeferedActions.Get(_scopeProvider); if (actions != null) - actions.Add(new DeferedReIndexForMember(this, member)); + actions.Add(new DeferedReIndexForMember(_taskHelper, this, member)); else - DeferedReIndexForMember.Execute(this, member); + DeferedReIndexForMember.Execute(_taskHelper, this, member); } private void ReIndexForMedia(IMedia sender, bool isPublished) { var actions = DeferedActions.Get(_scopeProvider); if (actions != null) - actions.Add(new DeferedReIndexForMedia(this, sender, isPublished)); + actions.Add(new DeferedReIndexForMedia(_taskHelper, this, sender, isPublished)); else - DeferedReIndexForMedia.Execute(this, sender, isPublished); + DeferedReIndexForMedia.Execute(_taskHelper, this, sender, isPublished); } /// @@ -594,12 +592,14 @@ namespace Umbraco.Web.Search /// private class DeferedReIndexForContent : DeferedAction { + private readonly TaskHelper _taskHelper; private readonly ExamineComponent _examineComponent; private readonly IContent _content; private readonly bool _isPublished; - public DeferedReIndexForContent(ExamineComponent examineComponent, IContent content, bool isPublished) + public DeferedReIndexForContent(TaskHelper taskHelper, ExamineComponent examineComponent, IContent content, bool isPublished) { + _taskHelper = taskHelper; _examineComponent = examineComponent; _content = content; _isPublished = isPublished; @@ -607,13 +607,12 @@ namespace Umbraco.Web.Search public override void Execute() { - Execute(_examineComponent, _content, _isPublished); + Execute(_taskHelper, _examineComponent, _content, _isPublished); } - public static void Execute(ExamineComponent examineComponent, IContent content, bool isPublished) + public static void Execute(TaskHelper taskHelper, ExamineComponent examineComponent, IContent content, bool isPublished) { - // perform the ValueSet lookup on a background thread - examineComponent._indexItemTaskRunner.Add(new SimpleTask(() => + taskHelper.RunBackgroundTask(async () => { // for content we have a different builder for published vs unpublished // we don't want to build more value sets than is needed so we'll lazily build 2 one for published one for non-published @@ -631,7 +630,8 @@ namespace Umbraco.Web.Search var valueSet = builders[index.PublishedValuesOnly].Value; index.IndexItems(valueSet); } - })); + }); + } } @@ -640,12 +640,14 @@ namespace Umbraco.Web.Search /// private class DeferedReIndexForMedia : DeferedAction { + private readonly TaskHelper _taskHelper; private readonly ExamineComponent _examineComponent; private readonly IMedia _media; private readonly bool _isPublished; - public DeferedReIndexForMedia(ExamineComponent examineComponent, IMedia media, bool isPublished) + public DeferedReIndexForMedia(TaskHelper taskHelper, ExamineComponent examineComponent, IMedia media, bool isPublished) { + _taskHelper = taskHelper; _examineComponent = examineComponent; _media = media; _isPublished = isPublished; @@ -653,13 +655,13 @@ namespace Umbraco.Web.Search public override void Execute() { - Execute(_examineComponent, _media, _isPublished); + Execute(_taskHelper, _examineComponent, _media, _isPublished); } - public static void Execute(ExamineComponent examineComponent, IMedia media, bool isPublished) + public static void Execute(TaskHelper taskHelper, ExamineComponent examineComponent, IMedia media, bool isPublished) { // perform the ValueSet lookup on a background thread - examineComponent._indexItemTaskRunner.Add(new SimpleTask(() => + taskHelper.RunBackgroundTask(async () => { var valueSet = examineComponent._mediaValueSetBuilder.GetValueSets(media).ToList(); @@ -670,7 +672,7 @@ namespace Umbraco.Web.Search { index.IndexItems(valueSet); } - })); + }); } } @@ -681,22 +683,24 @@ namespace Umbraco.Web.Search { private readonly ExamineComponent _examineComponent; private readonly IMember _member; + private readonly TaskHelper _taskHelper; - public DeferedReIndexForMember(ExamineComponent examineComponent, IMember member) + public DeferedReIndexForMember(TaskHelper taskHelper, ExamineComponent examineComponent, IMember member) { _examineComponent = examineComponent; _member = member; + _taskHelper = taskHelper; } public override void Execute() { - Execute(_examineComponent, _member); + Execute(_taskHelper, _examineComponent, _member); } - public static void Execute(ExamineComponent examineComponent, IMember member) + public static void Execute(TaskHelper taskHelper, ExamineComponent examineComponent, IMember member) { // perform the ValueSet lookup on a background thread - examineComponent._indexItemTaskRunner.Add(new SimpleTask(() => + taskHelper.RunBackgroundTask(async () => { var valueSet = examineComponent._memberValueSetBuilder.GetValueSets(member).ToList(); foreach (var index in examineComponent._examineManager.Indexes.OfType() @@ -705,7 +709,7 @@ namespace Umbraco.Web.Search { index.IndexItems(valueSet); } - })); + }); } } diff --git a/src/Umbraco.Infrastructure/Search/ExamineFinalComponent.cs b/src/Umbraco.Infrastructure/Search/ExamineFinalComponent.cs index ffcd96af7e..199c6482f4 100644 --- a/src/Umbraco.Infrastructure/Search/ExamineFinalComponent.cs +++ b/src/Umbraco.Infrastructure/Search/ExamineFinalComponent.cs @@ -1,8 +1,6 @@ -using Examine; -using Umbraco.Core.Logging; -using Umbraco.Examine; -using Umbraco.Core.Composing; +using System; using Umbraco.Core; +using Umbraco.Core.Composing; namespace Umbraco.Web.Search { @@ -11,10 +9,10 @@ namespace Umbraco.Web.Search /// Executes after all other examine components have executed /// public sealed class ExamineFinalComponent : IComponent - { + { BackgroundIndexRebuilder _indexRebuilder; private readonly IMainDom _mainDom; - + public ExamineFinalComponent(BackgroundIndexRebuilder indexRebuilder, IMainDom mainDom) { _indexRebuilder = indexRebuilder; @@ -26,7 +24,7 @@ namespace Umbraco.Web.Search if (!_mainDom.IsMainDom) return; // TODO: Instead of waiting 5000 ms, we could add an event handler on to fulfilling the first request, then start? - _indexRebuilder.RebuildIndexes(true, 5000); + _indexRebuilder.RebuildIndexes(true, TimeSpan.FromSeconds(5)); } public void Terminate() diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index b9368da89b..70218d931f 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -102,4 +102,8 @@ + + + + diff --git a/src/Umbraco.Tests.Common/TestHelpers/LogTestHelper.cs b/src/Umbraco.Tests.Common/TestHelpers/LogTestHelper.cs new file mode 100644 index 0000000000..9f04ef7307 --- /dev/null +++ b/src/Umbraco.Tests.Common/TestHelpers/LogTestHelper.cs @@ -0,0 +1,41 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System; +using Microsoft.Extensions.Logging; +using Moq; + +namespace Umbraco.Tests.Common.TestHelpers +{ + public static class LogTestHelper + { + public static Mock> VerifyLogError( + this Mock> logger, + Exception exception, + string expectedMessage, + Times? times = null) => VerifyLogging(logger, exception, expectedMessage, LogLevel.Error, times); + + private static Mock> VerifyLogging( + this Mock> logger, + Exception exception, + string expectedMessage, + LogLevel expectedLogLevel = LogLevel.Debug, + Times? times = null) + { + times ??= Times.Once(); + + Func state = (v, t) => + string.Compare(v.ToString(), expectedMessage, StringComparison.Ordinal) == 0; + + logger.Verify( + x => x.Log( + It.Is(l => l == expectedLogLevel), + It.IsAny(), + It.Is((v, t) => state(v, t)), + exception, + It.Is>((v, t) => true)), (Times)times); + + return logger; + } + } +} diff --git a/src/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj b/src/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj index bdb703753d..9c9b6516d8 100644 --- a/src/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj +++ b/src/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj @@ -2,6 +2,7 @@ netstandard2.0 + latest diff --git a/src/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs index 6af99086c8..531bc610bf 100644 --- a/src/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs @@ -11,10 +11,8 @@ using Moq; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Cache; -using Umbraco.Core.Composing; using Umbraco.Core.Configuration.Models; using Umbraco.Core.DependencyInjection; -using Umbraco.Core.Hosting; using Umbraco.Core.Logging; using Umbraco.Core.Runtime; using Umbraco.Core.Services; @@ -22,6 +20,7 @@ using Umbraco.Core.Services.Implement; using Umbraco.Core.Sync; using Umbraco.Core.WebAssets; using Umbraco.Examine; +using Umbraco.Infrastructure.HostedServices; using Umbraco.Tests.Integration.Implementations; using Umbraco.Tests.TestHelpers.Stubs; using Umbraco.Web.PublishedCache.NuCache; @@ -100,12 +99,16 @@ namespace Umbraco.Tests.Integration.DependencyInjection // replace the default so there is no background index rebuilder private class TestBackgroundIndexRebuilder : BackgroundIndexRebuilder { - public TestBackgroundIndexRebuilder(IMainDom mainDom, IProfilingLogger profilingLogger, ILoggerFactory loggerFactory, IApplicationShutdownRegistry hostingEnvironment, IndexRebuilder indexRebuilder) - : base(mainDom, profilingLogger, loggerFactory, hostingEnvironment, indexRebuilder) + public TestBackgroundIndexRebuilder( + IMainDom mainDom, + ILogger logger, + IndexRebuilder indexRebuilder, + IBackgroundTaskQueue backgroundTaskQueue) + : base(mainDom, logger, indexRebuilder, backgroundTaskQueue) { } - public override void RebuildIndexes(bool onlyEmptyIndexes, int waitMilliseconds = 0) + public override void RebuildIndexes(bool onlyEmptyIndexes, TimeSpan? delay = null) { // noop } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/TaskHelperTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/TaskHelperTests.cs new file mode 100644 index 0000000000..7a9a215746 --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/TaskHelperTests.cs @@ -0,0 +1,47 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System; +using System.Threading; +using System.Threading.Tasks; +using AutoFixture.NUnit3; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Tests.Common.TestHelpers; +using Umbraco.Tests.UnitTests.AutoFixture; + +namespace Umbraco.Tests.UnitTests.Umbraco.Core +{ + [TestFixture] + public class TaskHelperTests + { + [Test] + [AutoMoqData] + public void RunBackgroundTask__must_run_func([Frozen] ILogger logger, TaskHelper sut) + { + var i = 0; + sut.RunBackgroundTask(() => + { + Interlocked.Increment(ref i); + return Task.CompletedTask; + }); + + Thread.Sleep(5); // Wait for background task to execute + + Assert.AreEqual(1, i); + } + + [Test] + [AutoMoqData] + public void RunBackgroundTask__Log_error_when_exception_happen_in_background_task([Frozen] ILogger logger, Exception exception, TaskHelper sut) + { + sut.RunBackgroundTask(() => throw exception); + + Thread.Sleep(5); // Wait for background task to execute + + Mock.Get(logger).VerifyLogError(exception, "Exception thrown in a background thread", Times.Once()); + } + } +} diff --git a/src/Umbraco.Infrastructure/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask/BackgroundTaskRunner.cs similarity index 100% rename from src/Umbraco.Infrastructure/Scheduling/BackgroundTaskRunner.cs rename to src/Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask/BackgroundTaskRunner.cs diff --git a/src/Umbraco.Core/Scheduling/BackgroundTaskRunnerOptions.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask/BackgroundTaskRunnerOptions.cs similarity index 100% rename from src/Umbraco.Core/Scheduling/BackgroundTaskRunnerOptions.cs rename to src/Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask/BackgroundTaskRunnerOptions.cs diff --git a/src/Umbraco.Core/Scheduling/IBackgroundTask.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask/IBackgroundTask.cs similarity index 100% rename from src/Umbraco.Core/Scheduling/IBackgroundTask.cs rename to src/Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask/IBackgroundTask.cs diff --git a/src/Umbraco.Core/Scheduling/IBackgroundTaskRunner.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask/IBackgroundTaskRunner.cs similarity index 100% rename from src/Umbraco.Core/Scheduling/IBackgroundTaskRunner.cs rename to src/Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask/IBackgroundTaskRunner.cs diff --git a/src/Umbraco.Core/Scheduling/ILatchedBackgroundTask.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask/ILatchedBackgroundTask.cs similarity index 100% rename from src/Umbraco.Core/Scheduling/ILatchedBackgroundTask.cs rename to src/Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask/ILatchedBackgroundTask.cs diff --git a/src/Umbraco.Core/Scheduling/LatchedBackgroundTaskBase.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask/LatchedBackgroundTaskBase.cs similarity index 100% rename from src/Umbraco.Core/Scheduling/LatchedBackgroundTaskBase.cs rename to src/Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask/LatchedBackgroundTaskBase.cs diff --git a/src/Umbraco.Core/Scheduling/TaskEventArgs.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask/TaskEventArgs.cs similarity index 100% rename from src/Umbraco.Core/Scheduling/TaskEventArgs.cs rename to src/Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask/TaskEventArgs.cs diff --git a/src/Umbraco.Core/Scheduling/ThreadingTaskImmutable.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask/ThreadingTaskImmutable.cs similarity index 100% rename from src/Umbraco.Core/Scheduling/ThreadingTaskImmutable.cs rename to src/Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask/ThreadingTaskImmutable.cs diff --git a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs deleted file mode 100644 index f0a9721b08..0000000000 --- a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs +++ /dev/null @@ -1,1026 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Core.Hosting; -using Umbraco.Tests.TestHelpers; -using Umbraco.Web.Scheduling; - -namespace Umbraco.Tests.Scheduling -{ - [TestFixture] - [Timeout(30000)] - [Category("Slow")] - public class BackgroundTaskRunnerTests - { - private ILoggerFactory _loggerFactory; - private ILogger> _backgroundTaskLogger; - private ILogger> _baseTaskLogger; - private IApplicationShutdownRegistry _hostingEnvironment; - - [OneTimeSetUp] - public void InitializeFixture() - { - _loggerFactory = LoggerFactory.Create(builder => builder.AddDebug()); - _backgroundTaskLogger = _loggerFactory.CreateLogger>(); - _baseTaskLogger = _loggerFactory.CreateLogger>(); - _hostingEnvironment = TestHelper.GetHostingEnvironmentLifetime(); - } - - [Test] - public async Task ShutdownWhenRunningWithWait() - { - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _backgroundTaskLogger, _hostingEnvironment)) - { - var stopped = false; - runner.Stopped += (sender, args) => { stopped = true; }; - - Assert.IsFalse(runner.IsRunning); // because AutoStart is false - runner.Add(new MyTask(5000)); - Assert.IsTrue(runner.IsRunning); // is running the task - - runner.Shutdown(false, true); // -force +wait - - // all this before we await because +wait - Assert.IsTrue(runner.IsCompleted); // shutdown completes the runner - Assert.IsFalse(runner.IsRunning); // no more running tasks - Assert.IsTrue(stopped); - - await runner.StoppedAwaitable; // runner stops, within test's timeout - } - } - - [Test] - public async Task ShutdownWhenRunningWithoutWait() - { - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _backgroundTaskLogger, _hostingEnvironment)) - { - var stopped = false; - runner.Stopped += (sender, args) => { stopped = true; }; - - Assert.IsFalse(runner.IsRunning); // because AutoStart is false - runner.Add(new MyTask(5000)); - Assert.IsTrue(runner.IsRunning); // is running the task - - runner.Shutdown(false, false); // -force +wait - - // all this before we await because -wait - Assert.IsTrue(runner.IsCompleted); // shutdown completes the runner - Assert.IsTrue(runner.IsRunning); // still running the task - Assert.IsFalse(stopped); - - await runner.StoppedAwaitable; // runner stops, within test's timeout - - // and then... - Assert.IsFalse(runner.IsRunning); // no more running tasks - Assert.IsTrue(stopped); - } - } - - [Test] - public async Task ShutdownCompletesTheRunner() - { - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _backgroundTaskLogger, _hostingEnvironment)) - { - Assert.IsFalse(runner.IsRunning); // because AutoStart is false - - // shutdown -force => run all queued tasks - runner.Shutdown(false, false); // -force -wait - await runner.StoppedAwaitable; // runner stops, within test's timeout - - Assert.IsFalse(runner.IsRunning); // still not running anything - Assert.IsTrue(runner.IsCompleted); // shutdown completes the runner - - // cannot add tasks to it anymore - Assert.IsFalse(runner.TryAdd(new MyTask())); - Assert.Throws(() => - { - runner.Add(new MyTask()); - }); - } - } - - [Test] - public async Task ShutdownFlushesTheQueue() - { - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _backgroundTaskLogger, _hostingEnvironment)) - { - MyTask t1, t2, t3; - - Assert.IsFalse(runner.IsRunning); // because AutoStart is false - runner.Add(t1 = new MyTask(5000)); - runner.Add(t2 = new MyTask()); - runner.Add(t3 = new MyTask()); - Assert.IsTrue(runner.IsRunning); // is running tasks - - // shutdown -force => run all queued tasks - runner.Shutdown(false, false); // -force -wait - Assert.IsTrue(runner.IsRunning); // is running tasks - await runner.StoppedAwaitable; // runner stops, within test's timeout - - Assert.AreNotEqual(DateTime.MinValue, t3.Ended); // t3 has run - } - } - - [Test] - public async Task ShutdownForceTruncatesTheQueue() - { - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _backgroundTaskLogger, _hostingEnvironment)) - { - MyTask t1, t2, t3; - - Assert.IsFalse(runner.IsRunning); // because AutoStart is false - runner.Add(t1 = new MyTask(5000)); - runner.Add(t2 = new MyTask()); - runner.Add(t3 = new MyTask()); - Assert.IsTrue(runner.IsRunning); // is running tasks - - Thread.Sleep(1000); // since we are forcing shutdown, we need to give it a chance to start, else it will be canceled before the queue is started - - // shutdown +force => tries to cancel the current task, ignores queued tasks - runner.Shutdown(true, false); // +force -wait - Assert.IsTrue(runner.IsRunning); // is running that long task it cannot cancel - - await runner.StoppedAwaitable; // runner stops, within test's timeout (no cancelation token used, no need to catch OperationCanceledException) - - Assert.AreNotEqual(DateTime.MinValue, t1.Ended); // t1 *has* run - Assert.AreEqual(DateTime.MinValue, t2.Ended); // t2 has *not* run - Assert.AreEqual(DateTime.MinValue, t3.Ended); // t3 has *not* run - } - } - - [Test] - public async Task ShutdownThenForce() - { - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _backgroundTaskLogger, _hostingEnvironment)) - { - - Assert.IsFalse(runner.IsRunning); // because AutoStart is false - runner.Add(new MyTask(5000)); - runner.Add(new MyTask()); - runner.Add(new MyTask()); - Assert.IsTrue(runner.IsRunning); // is running tasks - - // shutdown -force => run all queued tasks - runner.Shutdown(false, false); // -force -wait - - Assert.IsTrue(runner.IsCompleted); // shutdown completes the runner - Assert.IsTrue(runner.IsRunning); // still running a task - Thread.Sleep(3000); - Assert.IsTrue(runner.IsRunning); // still running a task - - // shutdown +force => tries to cancel the current task, ignores queued tasks - runner.Shutdown(true, false); // +force -wait - try - { - await runner.StoppedAwaitable; // runner stops, within test's timeout ... maybe - } - catch (OperationCanceledException) - { - // catch exception, this can occur because we are +force shutting down which will - // cancel a pending task if the queue hasn't completed in time - } - } - } - - - [Test] - public async Task HostingStopNonImmediate() - { - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _backgroundTaskLogger, _hostingEnvironment)) - { - MyTask t; - - var stopped = false; - runner.Stopped += (sender, args) => { stopped = true; }; - var terminating = false; - runner.Terminating += (sender, args) => { terminating = true; }; - var terminated = false; - runner.Terminated += (sender, args) => { terminated = true; }; - - Assert.IsFalse(runner.IsRunning); // because AutoStart is false - runner.Add(new MyTask()); // sleeps 500 ms - runner.Add(new MyTask()); // sleeps 500 ms - runner.Add(t = new MyTask()); // sleeps 500 ms ... total = 1500 ms until it's done - Assert.IsTrue(runner.IsRunning); // is running the task - - runner.Stop(false); // -immediate = -force, -wait (max 2000 ms delay before +immediate) - await runner.TerminatedAwaitable; - - Assert.IsTrue(stopped); // raised that one - Assert.IsTrue(terminating); // has raised that event - Assert.IsTrue(terminated); // and that event - - Assert.IsTrue(runner.IsCompleted); // shutdown completes the runner - Assert.IsFalse(runner.IsRunning); // done running - - Assert.AreNotEqual(DateTime.MinValue, t.Ended); // t has run - } - } - - [Test] - public async Task HostingStopImmediate() - { - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _backgroundTaskLogger, _hostingEnvironment)) - { - MyTask t; - - var stopped = false; - runner.Stopped += (sender, args) => { stopped = true; }; - var terminating = false; - runner.Terminating += (sender, args) => { terminating = true; }; - var terminated = false; - runner.Terminated += (sender, args) => { terminated = true; }; - - Assert.IsFalse(runner.IsRunning); // because AutoStart is false - runner.Add(new MyTask()); // sleeps 500 ms - runner.Add(new MyTask()); // sleeps 500 ms - runner.Add(t = new MyTask()); // sleeps 500 ms ... total = 1500 ms until it's done - Assert.IsTrue(runner.IsRunning); // is running the task - - runner.Stop(true); // +immediate = +force, +wait (no delay) - await runner.TerminatedAwaitable; - - Assert.IsTrue(stopped); // raised that one - Assert.IsTrue(terminating); // has raised that event - Assert.IsTrue(terminated); // and that event - - Assert.IsTrue(runner.IsCompleted); // shutdown completes the runner - Assert.IsFalse(runner.IsRunning); // done running - - Assert.AreEqual(DateTime.MinValue, t.Ended); // t has *not* run - } - } - - - [Test] - public void Create_IsNotRunning() - { - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _backgroundTaskLogger, _hostingEnvironment)) - { - Assert.IsFalse(runner.IsRunning); - } - } - - - [Test] - public async Task Create_AutoStart_IsRunning() - { - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions - { - AutoStart = true, - KeepAlive = true // else stops! - }, _backgroundTaskLogger, _hostingEnvironment)) - { - Assert.IsTrue(runner.IsRunning); // because AutoStart is true - await runner.StopInternal(false); // keepalive = must be stopped - } - } - - [Test] - public void Create_AutoStartAndKeepAlive_IsRunning() - { - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { AutoStart = true, KeepAlive = true }, _backgroundTaskLogger, _hostingEnvironment)) - { - Assert.IsTrue(runner.IsRunning); // because AutoStart is true - Thread.Sleep(800); // for long - Assert.IsTrue(runner.IsRunning); - // dispose will stop it - } - } - - [Test] - public async Task Dispose_IsRunning() - { - BackgroundTaskRunner runner; - using (runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { AutoStart = true, KeepAlive = true }, _backgroundTaskLogger, _hostingEnvironment)) - { - Assert.IsTrue(runner.IsRunning); - // dispose will stop it - } - - try - { - await runner.StoppedAwaitable; - } - catch (OperationCanceledException) - { - // swallow this exception, it can be expected to throw since when disposing we are calling Shutdown +force - // which depending on a timing operation may cancel the cancelation token - } - - - Assert.Throws(() => runner.Add(new MyTask())); - - } - - [Test] - public void Startup_KeepAlive_IsRunning() - { - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { KeepAlive = true }, _backgroundTaskLogger, _hostingEnvironment)) - { - Assert.IsFalse(runner.IsRunning); - runner.StartUp(); - Assert.IsTrue(runner.IsRunning); - // dispose will stop it - } - } - - [Test] - public async Task Create_AddTask_IsRunning() - { - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _baseTaskLogger, _hostingEnvironment)) - { - var waitHandle = new ManualResetEvent(false); - runner.TaskCompleted += (sender, args) => - { - waitHandle.Set(); - }; - runner.Add(new MyTask()); - Assert.IsTrue(runner.IsRunning); - waitHandle.WaitOne(); - await runner.StoppedAwaitable; //since we are not being kept alive, it will quit - Assert.IsFalse(runner.IsRunning); - } - } - - [Test] - public void Create_KeepAliveAndAddTask_IsRunning() - { - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { KeepAlive = true }, _baseTaskLogger, _hostingEnvironment)) - { - var waitHandle = new ManualResetEvent(false); - runner.TaskCompleted += (sender, args) => - { - Assert.IsTrue(sender.IsRunning); - waitHandle.Set(); - }; - runner.Add(new MyTask()); - waitHandle.WaitOne(); - Thread.Sleep(1000); // we are waiting a second just to prove that it's still running and hasn't been shut off - Assert.IsTrue(runner.IsRunning); - } - } - - [Test] - public async Task WaitOnRunner_OneTask() - { - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _baseTaskLogger, _hostingEnvironment)) - { - var task = new MyTask(); - Assert.IsTrue(task.Ended == default(DateTime)); - runner.Add(task); - await runner.CurrentThreadingTask; // wait for the Task operation to complete - Assert.IsTrue(task.Ended != default(DateTime)); // task is done - await runner.StoppedAwaitable; // wait for the entire runner operation to complete - } - } - - - [Test] - public async Task WaitOnRunner_Tasks() - { - var tasks = new List(); - for (var i = 0; i < 10; i++) - tasks.Add(new MyTask()); - - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { KeepAlive = false, LongRunning = true, PreserveRunningTask = true }, _baseTaskLogger, _hostingEnvironment)) - { - tasks.ForEach(runner.Add); - - await runner.StoppedAwaitable; // wait for the entire runner operation to complete - - // check that tasks are done - Assert.IsTrue(tasks.All(x => x.Ended != default(DateTime))); - - Assert.AreEqual(TaskStatus.RanToCompletion, runner.CurrentThreadingTask.Status); - Assert.IsFalse(runner.IsRunning); - Assert.IsFalse(runner.IsDisposed); - } - } - - [Test] - public async Task WaitOnTask() - { - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _baseTaskLogger, _hostingEnvironment)) - { - var task = new MyTask(); - var waitHandle = new ManualResetEvent(false); - runner.TaskCompleted += (sender, t) => waitHandle.Set(); - Assert.IsTrue(task.Ended == default(DateTime)); - runner.Add(task); - waitHandle.WaitOne(); // wait 'til task is done - Assert.IsTrue(task.Ended != default(DateTime)); // task is done - await runner.StoppedAwaitable; // wait for the entire runner operation to complete - } - } - - [Test] - public async Task WaitOnTasks() - { - var tasks = new Dictionary(); - for (var i = 0; i < 10; i++) - tasks.Add(new MyTask(), new ManualResetEvent(false)); - - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _baseTaskLogger, _hostingEnvironment)) - { - runner.TaskCompleted += (sender, task) => tasks[task.Task].Set(); - foreach (var t in tasks) runner.Add(t.Key); - - // wait 'til tasks are done, check that tasks are done - WaitHandle.WaitAll(tasks.Values.Select(x => (WaitHandle)x).ToArray()); - Assert.IsTrue(tasks.All(x => x.Key.Ended != default(DateTime))); - - await runner.StoppedAwaitable; // wait for the entire runner operation to complete - } - } - - [Test] - public void Tasks_Can_Keep_Being_Added_And_Will_Execute() - { - Func> getTasks = () => - { - var newTasks = new Dictionary(); - for (var i = 0; i < 10; i++) - { - newTasks.Add(new MyTask(), new ManualResetEvent(false)); - } - return newTasks; - }; - - IDictionary tasks = getTasks(); - - BackgroundTaskRunner tManager; - using (tManager = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { LongRunning = true, KeepAlive = true }, _baseTaskLogger, _hostingEnvironment)) - { - tManager.TaskCompleted += (sender, task) => tasks[task.Task].Set(); - - //execute first batch - foreach (var t in tasks) tManager.Add(t.Key); - - //wait for all ITasks to complete - WaitHandle.WaitAll(tasks.Values.Select(x => (WaitHandle)x).ToArray()); - - foreach (var task in tasks) - { - Assert.IsTrue(task.Key.Ended != default(DateTime)); - } - - //execute another batch after a bit - Thread.Sleep(2000); - - tasks = getTasks(); - foreach (var t in tasks) tManager.Add(t.Key); - - //wait for all ITasks to complete - WaitHandle.WaitAll(tasks.Values.Select(x => (WaitHandle)x).ToArray()); - - foreach (var task in tasks) - { - Assert.IsTrue(task.Key.Ended != default(DateTime)); - } - } - } - - [Test] - public async Task Non_Persistent_Runner_Will_Start_New_Threads_When_Required() - { - Func> getTasks = () => - { - var newTasks = new List(); - for (var i = 0; i < 10; i++) - { - newTasks.Add(new MyTask()); - } - return newTasks; - }; - - List tasks = getTasks(); - - using (var tManager = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { LongRunning = true, PreserveRunningTask = true }, _baseTaskLogger, _hostingEnvironment)) - { - tasks.ForEach(tManager.Add); - - //wait till the thread is done - await tManager.CurrentThreadingTask; - - Assert.AreEqual(TaskStatus.RanToCompletion, tManager.CurrentThreadingTask.Status); - Assert.IsFalse(tManager.IsRunning); - Assert.IsFalse(tManager.IsDisposed); - - foreach (var task in tasks) - { - Assert.IsTrue(task.Ended != default(DateTime)); - } - - //create more tasks - tasks = getTasks(); - - //add more tasks - tasks.ForEach(tManager.Add); - - //wait till the thread is done - await tManager.CurrentThreadingTask; - - foreach (var task in tasks) - { - Assert.IsTrue(task.Ended != default(DateTime)); - } - - Assert.AreEqual(TaskStatus.RanToCompletion, tManager.CurrentThreadingTask.Status); - Assert.IsFalse(tManager.IsRunning); - Assert.IsFalse(tManager.IsDisposed); - } - } - - - [Test] - public void RecurringTaskTest() - { - var runCount = 0; - var waitHandle = new ManualResetEvent(false); - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _backgroundTaskLogger, _hostingEnvironment)) - { - runner.TaskCompleted += (sender, args) => - { - runCount++; - if (runCount > 3) - waitHandle.Set(); - }; - - var task = new MyRecurringTask(runner, 200, 500); - - runner.Add(task); - - Assert.IsTrue(runner.IsRunning); // waiting on delay - Assert.AreEqual(0, runCount); - - waitHandle.WaitOne(); - - Assert.GreaterOrEqual(runCount, 4); - - // stops recurring - runner.Shutdown(false, true); - - // check that task has been disposed (timer has been killed, etc) - Assert.IsTrue(task.Disposed); - } - } - - [Test] - public async Task LatchedTaskRuns() - { - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _backgroundTaskLogger, _hostingEnvironment)) - { - var task = new MyLatchedTask(200, false); - runner.Add(task); - Assert.IsTrue(runner.IsRunning); - Thread.Sleep(1000); - Assert.IsTrue(runner.IsRunning); // still waiting for the task to release - Assert.IsFalse(task.HasRun); - task.Release(); // unlatch - var runnerTask = runner.CurrentThreadingTask; // may be null if things go fast enough - if (runnerTask != null) - await runnerTask; // wait for current task to complete - Assert.IsTrue(task.HasRun); - await runner.StoppedAwaitable; // wait for the entire runner operation to complete - } - } - - [Test] - public async Task LatchedTaskStops_Runs_On_Shutdown() - { - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _backgroundTaskLogger, _hostingEnvironment)) - { - var task = new MyLatchedTask(200, true); - runner.Add(task); - Assert.IsTrue(runner.IsRunning); - Thread.Sleep(5000); - Assert.IsTrue(runner.IsRunning); // still waiting for the task to release - Assert.IsFalse(task.HasRun); - runner.Shutdown(false, false); // -force, -wait - await runner.StoppedAwaitable; // wait for the entire runner operation to complete - Assert.IsTrue(task.HasRun); - } - } - - - [Test] - public void LatchedRecurring() - { - var runCount = 0; - var waitHandle = new ManualResetEvent(false); - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _backgroundTaskLogger, _hostingEnvironment)) - { - runner.TaskCompleted += (sender, args) => - { - runCount++; - if (runCount > 3) - waitHandle.Set(); - - }; - - var task = new MyDelayedRecurringTask(runner, 2000, 1000); - runner.Add(task); - Assert.IsTrue(runner.IsRunning); // waiting on delay - Assert.AreEqual(0, runCount); - - waitHandle.WaitOne(); - Assert.GreaterOrEqual(runCount, 4); - Assert.IsTrue(task.HasRun); - - // stops recurring - runner.Shutdown(false, false); - } - } - - [Test] - public async Task FailingTaskSync() - { - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _backgroundTaskLogger, _hostingEnvironment)) - { - var exceptions = new ConcurrentQueue(); - runner.TaskError += (sender, args) => exceptions.Enqueue(args.Exception); - - var task = new MyFailingTask(false, true, false); // -async, +running, -disposing - runner.Add(task); - Assert.IsTrue(runner.IsRunning); - await runner.StoppedAwaitable; // wait for the entire runner operation to complete - - Assert.AreEqual(1, exceptions.Count); // traced and reported - } - } - - [Test] - public async Task FailingTaskDisposing() - { - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _backgroundTaskLogger, _hostingEnvironment)) - { - var exceptions = new ConcurrentQueue(); - runner.TaskError += (sender, args) => exceptions.Enqueue(args.Exception); - - var task = new MyFailingTask(false, false, true); // -async, -running, +disposing - runner.Add(task); - Assert.IsTrue(runner.IsRunning); - await runner.StoppedAwaitable; // wait for the entire runner operation to complete - - Assert.AreEqual(1, exceptions.Count); // traced and reported - } - } - - [Test] - public async Task FailingTaskAsync() - { - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _backgroundTaskLogger, _hostingEnvironment)) - { - var exceptions = new ConcurrentQueue(); - runner.TaskError += (sender, args) => exceptions.Enqueue(args.Exception); - - var task = new MyFailingTask(true, true, false); // +async, +running, -disposing - runner.Add(task); - Assert.IsTrue(runner.IsRunning); - await runner.StoppedAwaitable; // wait for the entire runner operation to complete - Assert.AreEqual(1, exceptions.Count); // traced and reported - } - } - - [Test] - public async Task FailingTaskDisposingAsync() - { - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _backgroundTaskLogger, _hostingEnvironment)) - { - var exceptions = new ConcurrentQueue(); - runner.TaskError += (sender, args) => exceptions.Enqueue(args.Exception); - - var task = new MyFailingTask(false, false, true); // -async, -running, +disposing - runner.Add(task); - Assert.IsTrue(runner.IsRunning); - await runner.StoppedAwaitable; // wait for the entire runner operation to complete - - Assert.AreEqual(1, exceptions.Count); // traced and reported - } - } - - [Test] - public async Task CancelAsyncTask() - { - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _backgroundTaskLogger, _hostingEnvironment)) - { - var task = new MyAsyncTask(4000); - runner.Add(task); - Assert.IsTrue(runner.IsRunning); - await Task.Delay(1000); // ensure the task *has* started else cannot cancel - runner.CancelCurrentBackgroundTask(); - - await runner.StoppedAwaitable; // wait for the entire runner operation to complete - Assert.AreEqual(default(DateTime), task.Ended); - } - } - - [Test] - public async Task CancelLatchedTask() - { - using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _backgroundTaskLogger, _hostingEnvironment)) - { - var task = new MyLatchedTask(4000, false); - runner.Add(task); - Assert.IsTrue(runner.IsRunning); - await Task.Delay(1000); // ensure the task *has* started else cannot cancel - runner.CancelCurrentBackgroundTask(); - - await runner.StoppedAwaitable; // wait for the entire runner operation to complete - Assert.IsFalse(task.HasRun); - } - } - - private class MyFailingTask : IBackgroundTask - { - private readonly bool _isAsync; - private readonly bool _running; - private readonly bool _disposing; - - public MyFailingTask(bool isAsync, bool running, bool disposing) - { - _isAsync = isAsync; - _running = running; - _disposing = disposing; - } - - public void Run() - { - Thread.Sleep(1000); - if (_running) - throw new Exception("Task has thrown."); - } - - public async Task RunAsync(CancellationToken token) - { - await Task.Delay(1000, token); - if (_running) - throw new Exception("Task has thrown."); - } - - public bool IsAsync - { - get { return _isAsync; } - } - - public void Dispose() - { - if (_disposing) - throw new Exception("Task has thrown."); - } - } - - private class MyDelayedRecurringTask : RecurringTaskBase - { - public bool HasRun { get; private set; } - - public MyDelayedRecurringTask(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds) - : base(runner, delayMilliseconds, periodMilliseconds) - { } - - public override bool IsAsync - { - get { return false; } - } - - public override bool PerformRun() - { - HasRun = true; - return true; // repeat - } - - public override Task PerformRunAsync(CancellationToken token) - { - throw new NotImplementedException(); - } - - public override bool RunsOnShutdown { get { return true; } } - } - - private class MyLatchedTask : ILatchedBackgroundTask - { - private readonly int _runMilliseconds; - private readonly TaskCompletionSource _latch; - - public bool HasRun { get; private set; } - - public MyLatchedTask(int runMilliseconds, bool runsOnShutdown) - { - _runMilliseconds = runMilliseconds; - _latch = new TaskCompletionSource(); - RunsOnShutdown = runsOnShutdown; - } - - public Task Latch - { - get { return _latch.Task; } - } - - public bool IsLatched - { - get { return _latch.Task.IsCompleted == false; } - } - - public bool RunsOnShutdown { get; private set; } - - public void Run() - { - Thread.Sleep(_runMilliseconds); - HasRun = true; - } - - public void Release() - { - _latch.SetResult(true); - } - - public Task RunAsync(CancellationToken token) - { - throw new NotImplementedException(); - } - - public bool IsAsync - { - get { return false; } - } - - public void Dispose() - { } - } - - private class MyRecurringTask : RecurringTaskBase - { - private readonly int _runMilliseconds; - - public MyRecurringTask(IBackgroundTaskRunner runner, int runMilliseconds, int periodMilliseconds) - : base(runner, 0, periodMilliseconds) - { - _runMilliseconds = runMilliseconds; - } - - public override bool PerformRun() - { - Thread.Sleep(_runMilliseconds); - return true; // repeat - } - - public override Task PerformRunAsync(CancellationToken token) - { - throw new NotImplementedException(); - } - - public override bool IsAsync - { - get { return false; } - } - - public override bool RunsOnShutdown { get { return false; } } - } - - private class MyTask : BaseTask - { - private readonly int _milliseconds; - - public MyTask() - : this(500) - { } - - public MyTask(int milliseconds) - { - _milliseconds = milliseconds; - } - - public override void PerformRun() - { - Console.WriteLine($"Sleeping {_milliseconds}..."); - Thread.Sleep(_milliseconds); - Console.WriteLine("Wake up!"); - } - } - - private class MyAsyncTask : BaseTask - { - private readonly int _milliseconds; - - public MyAsyncTask() - : this(500) - { } - - public MyAsyncTask(int milliseconds) - { - _milliseconds = milliseconds; - } - - public override void PerformRun() - { - throw new NotImplementedException(); - } - - public override async Task RunAsync(CancellationToken token) - { - await Task.Delay(_milliseconds, token); - Ended = DateTime.Now; - } - - public override bool IsAsync - { - get { return true; } - } - } - - [Test] - public void SourceTaskTest() - { - var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { KeepAlive = true, LongRunning = true }, _backgroundTaskLogger, TestHelper.GetHostingEnvironmentLifetime()); - - var task = new SourceTask(); - runner.Add(task); - Assert.IsTrue(runner.IsRunning); - Console.WriteLine("completing"); - task.Complete(); // in Deploy this does not return ffs - no point until I cannot repro - Console.WriteLine("completed"); - Console.WriteLine("done"); - } - - private class SourceTask : IBackgroundTask - { - private readonly SemaphoreSlim _timeout = new SemaphoreSlim(0, 1); - private readonly TaskCompletionSource _source = new TaskCompletionSource(); - - public void Complete() - { - _source.SetResult(null); - } - - public void Dispose() - { } - - public void Run() - { - throw new NotImplementedException(); - } - - public async Task RunAsync(CancellationToken token) - { - Console.WriteLine("boom"); - var timeout = _timeout.WaitAsync(token); - var task = WorkItemRunAsync(); - var anyTask = await Task.WhenAny(task, timeout).ConfigureAwait(false); - } - - private async Task WorkItemRunAsync() - { - await _source.Task.ConfigureAwait(false); - } - - public bool IsAsync { get { return true; } } - } - - public abstract class BaseTask : IBackgroundTask - { - public bool WasCancelled { get; set; } - - public Guid UniqueId { get; protected set; } - - public abstract void PerformRun(); - - public void Run() - { - PerformRun(); - Ended = DateTime.Now; - } - - public virtual Task RunAsync(CancellationToken token) - { - throw new NotImplementedException(); - //return Task.Delay(500); - } - - public virtual bool IsAsync - { - get { return false; } - } - - public virtual void Cancel() - { - WasCancelled = true; - } - - public DateTime Queued { get; set; } - public DateTime Started { get; set; } - public DateTime Ended { get; set; } - - public virtual void Dispose() - { - - } - } - } -} diff --git a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests2.cs b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests2.cs deleted file mode 100644 index c2860b57d0..0000000000 --- a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests2.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using NUnit.Framework; -using Umbraco.Tests.TestHelpers; -using Umbraco.Web.Scheduling; - -namespace Umbraco.Tests.Scheduling -{ - [TestFixture] - [Timeout(60000)] - public class BackgroundTaskRunnerTests2 - { - private static ILoggerFactory _loggerFactory = NullLoggerFactory.Instance; - // this tests was used to debug a background task runner issue that was unearthed by Deploy, - // where work items would never complete under certain circumstances, due to threading issues. - // (fixed now) - // - [Test] - [Timeout(4000)] - public async Task ThreadResumeIssue() - { - var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { KeepAlive = true, LongRunning = true }, _loggerFactory.CreateLogger>(), TestHelper.GetHostingEnvironmentLifetime()); - var work = new ThreadResumeIssueWorkItem(); - runner.Add(work); - - Console.WriteLine("running"); - await Task.Delay(1000); // don't complete too soon - - Console.WriteLine("completing"); - - // this never returned, never reached "completed" because the same thread - // resumed executing the waiting on queue operation in the runner - work.Complete(); - Console.WriteLine("completed"); - - Console.WriteLine("done"); - } - - public class ThreadResumeIssueWorkItem : IBackgroundTask - { - private TaskCompletionSource _completionSource; - - public async Task RunAsync(CancellationToken token) - { - _completionSource = new TaskCompletionSource(); - token.Register(() => _completionSource.TrySetCanceled()); // propagate - Console.WriteLine("item running..."); - await _completionSource.Task.ConfigureAwait(false); - Console.WriteLine("item returning"); - } - - public bool Complete(bool success = true) - { - Console.WriteLine("item completing"); - // this never returned, see test - _completionSource.SetResult(0); - Console.WriteLine("item returning from completing"); - return true; - } - - public void Run() - { - throw new NotImplementedException(); - } - - public bool IsAsync { get { return true; } } - - public void Dispose() - { } - } - - [Test] - [Ignore("Only runs in the debugger.")] - public async Task DebuggerInterferenceIssue() - { - var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { KeepAlive = true, LongRunning = true }, _loggerFactory.CreateLogger>(), TestHelper.GetHostingEnvironmentLifetime()); - var taskCompleted = false; - runner.TaskCompleted += (sender, args) => - { - Console.WriteLine("runner task completed"); - taskCompleted = true; - }; - var work = new DebuggerInterferenceIssueWorkitem(); - - // add the workitem to the runner and wait until it is running - runner.Add(work); - work.Running.Wait(); - - // then wait a little bit more to ensure that the WhenAny has been entered - await Task.Delay(500); - - // then break - // when the timeout triggers, we cannot handle it - // taskCompleted value does *not* change & nothing happens - Debugger.Break(); - - // release after 15s - // WhenAny should return the timeout task - // and then taskCompleted should turn to true - // = debugging does not prevent task completion - - Console.WriteLine("*"); - Assert.IsFalse(taskCompleted); - await Task.Delay(1000); - Console.WriteLine("*"); - Assert.IsTrue(taskCompleted); - } - - public class DebuggerInterferenceIssueWorkitem : IBackgroundTask - { - private readonly SemaphoreSlim _timeout = new SemaphoreSlim(0, 1); - private readonly ManualResetEventSlim _running = new ManualResetEventSlim(false); - - private Timer _timer; - - public ManualResetEventSlim Running { get { return _running; } } - - public async Task RunAsync(CancellationToken token) - { - // timeout timer - _timer = new Timer(_ => { _timeout.Release(); }); - _timer.Change(1000, 0); - - var timeout = _timeout.WaitAsync(token); - var source = CancellationTokenSource.CreateLinkedTokenSource(token); // cancels when token cancels - - _running.Set(); - var task = WorkExecuteAsync(source.Token); - Console.WriteLine("execute"); - var anyTask = await Task.WhenAny(task, timeout).ConfigureAwait(false); - - Console.Write("anyTask: "); - Console.WriteLine(anyTask == timeout ? "timeout" : "task"); - - Console.WriteLine("return"); - } - - private async Task WorkExecuteAsync(CancellationToken token) - { - await Task.Delay(30000); - } - - public void Run() - { - throw new NotImplementedException(); - } - - public bool IsAsync { get { return true; } } - - public void Dispose() - { } - } - - [Test] - [Ignore("Only runs in the debugger.")] - public void TimerDebuggerTest() - { - var triggered = false; - var timer = new Timer(_ => { triggered = true; }); - timer.Change(1000, 0); - Debugger.Break(); - - // pause in debugger for 10s - // means the timer triggers while execution is suspended - // 'triggered' remains false all along - // then resume execution - // and 'triggered' becomes true, so the trigger "catches up" - // = debugging should not prevent triggered code from executing - - Thread.Sleep(200); - Assert.IsTrue(triggered); - } - } -} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index d84908062d..ff6688280c 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -135,6 +135,14 @@ + + + + + + + + @@ -169,7 +177,6 @@ - @@ -201,7 +208,6 @@ - diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index faa838673f..f04c579786 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -150,6 +150,7 @@ namespace Umbraco.Web.Common.DependencyInjection /// public static IUmbracoBuilder AddHostedServices(this IUmbracoBuilder builder) { + builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService();