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();