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)
This commit is contained in:
@@ -1,127 +0,0 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Umbraco.Web.Scheduling
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a base class for recurring background tasks.
|
||||
/// </summary>
|
||||
/// <remarks>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).</remarks>
|
||||
public abstract class RecurringTaskBase : LatchedBackgroundTaskBase
|
||||
{
|
||||
private readonly IBackgroundTaskRunner<RecurringTaskBase> _runner;
|
||||
private readonly long _periodMilliseconds;
|
||||
private readonly Timer _timer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RecurringTaskBase"/> class.
|
||||
/// </summary>
|
||||
/// <param name="runner">The task runner.</param>
|
||||
/// <param name="delayMilliseconds">The delay.</param>
|
||||
/// <param name="periodMilliseconds">The period.</param>
|
||||
/// <remarks>The task will repeat itself periodically. Use this constructor to create a new task.</remarks>
|
||||
protected RecurringTaskBase(IBackgroundTaskRunner<RecurringTaskBase> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RecurringTaskBase"/> class.
|
||||
/// </summary>
|
||||
/// <param name="runner">The task runner.</param>
|
||||
/// <param name="delayMilliseconds">The delay.</param>
|
||||
/// <param name="periodMilliseconds">The period.</param>
|
||||
/// <remarks>The task will repeat itself periodically. Use this constructor to create a new task.</remarks>
|
||||
protected RecurringTaskBase(IBackgroundTaskRunner<RecurringTaskBase> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements IBackgroundTask.Run().
|
||||
/// </summary>
|
||||
/// <remarks>Classes inheriting from <c>RecurringTaskBase</c> must implement <c>PerformRun</c>.</remarks>
|
||||
public sealed override void Run()
|
||||
{
|
||||
var shouldRepeat = PerformRun();
|
||||
if (shouldRepeat) Repeat();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implements IBackgroundTask.RunAsync().
|
||||
/// </summary>
|
||||
/// <remarks>Classes inheriting from <c>RecurringTaskBase</c> must implement <c>PerformRun</c>.</remarks>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the background task.
|
||||
/// </summary>
|
||||
/// <returns>A value indicating whether to repeat the task.</returns>
|
||||
public virtual bool PerformRun()
|
||||
{
|
||||
throw new NotSupportedException("This task cannot run synchronously.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the task asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="token">A cancellation token.</param>
|
||||
/// <returns>A <see cref="Task{T}"/> instance representing the execution of the background task,
|
||||
/// and returning a value indicating whether to repeat the task.</returns>
|
||||
public virtual Task<bool> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<TResult>(TaskCompletionSource<TResult> 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<TResult>(TaskCompletionSource<TResult> completionSource, Task<TResult> 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<Task, Task> continuation)
|
||||
{
|
||||
var completionSource = new TaskCompletionSource<object>();
|
||||
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<Task, Task> continuation, CancellationToken token)
|
||||
{
|
||||
var completionSource = new TaskCompletionSource<object>();
|
||||
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<object>();
|
||||
taskSource.SetResult(null);
|
||||
return taskSource.Task;
|
||||
}
|
||||
|
||||
public static Task Sync(this TaskFactory factory, Action action)
|
||||
{
|
||||
var taskSource = new TaskCompletionSource<object>();
|
||||
action();
|
||||
taskSource.SetResult(null);
|
||||
return taskSource.Task;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
52
src/Umbraco.Core/TaskHelper.cs
Normal file
52
src/Umbraco.Core/TaskHelper.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class to not repeat common patterns with Task.
|
||||
/// </summary>
|
||||
public class TaskHelper
|
||||
{
|
||||
private readonly ILogger<TaskHelper> _logger;
|
||||
|
||||
public TaskHelper(ILogger<TaskHelper> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public void RunBackgroundTask(Func<Task> fn) => Task.Run(LoggingWrapper(fn)).ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public void RunLongRunningBackgroundTask(Func<Task> fn) =>
|
||||
Task.Factory.StartNew(LoggingWrapper(fn), TaskCreationOptions.LongRunning)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
private Func<Task> LoggingWrapper(Func<Task> fn) =>
|
||||
async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await fn();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Exception thrown in a background thread");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<IBackgroundTaskQueue, BackgroundTaskQueue>();
|
||||
builder.Services.AddUnique<TaskHelper>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ namespace Umbraco.Infrastructure.DependencyInjection
|
||||
() =>
|
||||
{
|
||||
var indexRebuilder = factory.GetRequiredService<BackgroundIndexRebuilder>();
|
||||
indexRebuilder.RebuildIndexes(false, 5000);
|
||||
indexRebuilder.RebuildIndexes(false, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Umbraco.Infrastructure.HostedServices
|
||||
{
|
||||
/// <summary>
|
||||
/// A Background Task Queue, to enqueue tasks for executing in the background.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0
|
||||
/// </remarks>
|
||||
public class BackgroundTaskQueue : IBackgroundTaskQueue
|
||||
{
|
||||
private readonly ConcurrentQueue<Func<CancellationToken, Task>> _workItems =
|
||||
new ConcurrentQueue<Func<CancellationToken, Task>>();
|
||||
|
||||
private readonly SemaphoreSlim _signal = new SemaphoreSlim(0);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem)
|
||||
{
|
||||
if (workItem == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(workItem));
|
||||
}
|
||||
|
||||
_workItems.Enqueue(workItem);
|
||||
_signal.Release();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _signal.WaitAsync(cancellationToken);
|
||||
_workItems.TryDequeue(out Func<CancellationToken, Task> workItem);
|
||||
|
||||
return workItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Umbraco.Infrastructure.HostedServices
|
||||
{
|
||||
/// <summary>
|
||||
/// A Background Task Queue, to enqueue tasks for executing in the background.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0
|
||||
/// </remarks>
|
||||
public interface IBackgroundTaskQueue
|
||||
{
|
||||
/// <summary>
|
||||
/// Enqueue a work item to be executed on in the background.
|
||||
/// </summary>
|
||||
void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
|
||||
|
||||
/// <summary>
|
||||
/// Dequeue the first item on the queue.
|
||||
/// </summary>
|
||||
Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// A queue based hosted service used to executing tasks on a background thread.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0
|
||||
/// </remarks>
|
||||
public class QueuedHostedService : BackgroundService
|
||||
{
|
||||
private readonly ILogger<QueuedHostedService> _logger;
|
||||
|
||||
public QueuedHostedService(IBackgroundTaskQueue taskQueue,
|
||||
ILogger<QueuedHostedService> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Umbraco.Web.Scheduling
|
||||
{
|
||||
/// <summary>
|
||||
/// A simple task that executes a delegate synchronously
|
||||
/// </summary>
|
||||
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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
/// </summary>
|
||||
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<BackgroundIndexRebuilder> _logger;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly IApplicationShutdownRegistry _hostingEnvironment;
|
||||
private static BackgroundTaskRunner<IBackgroundTask> _rebuildOnStartupRunner;
|
||||
private readonly IBackgroundTaskQueue _backgroundTaskQueue;
|
||||
|
||||
public BackgroundIndexRebuilder(IMainDom mainDom, IProfilingLogger profilingLogger , ILoggerFactory loggerFactory, IApplicationShutdownRegistry hostingEnvironment, IndexRebuilder indexRebuilder)
|
||||
private readonly IMainDom _mainDom;
|
||||
private readonly ILogger<BackgroundIndexRebuilder> _logger;
|
||||
|
||||
private volatile bool _isRunning = false;
|
||||
private static readonly object s_rebuildLocker = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BackgroundIndexRebuilder"/> class.
|
||||
/// </summary>
|
||||
public BackgroundIndexRebuilder(
|
||||
IMainDom mainDom,
|
||||
ILogger<BackgroundIndexRebuilder> logger,
|
||||
IndexRebuilder indexRebuilder,
|
||||
IBackgroundTaskQueue backgroundTaskQueue)
|
||||
{
|
||||
_mainDom = mainDom;
|
||||
_profilingLogger = profilingLogger ;
|
||||
_loggerFactory = loggerFactory;
|
||||
_logger = loggerFactory.CreateLogger<BackgroundIndexRebuilder>();
|
||||
_hostingEnvironment = hostingEnvironment;
|
||||
_logger = logger;
|
||||
_indexRebuilder = indexRebuilder;
|
||||
_backgroundTaskQueue = backgroundTaskQueue;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Called to rebuild empty indexes on startup
|
||||
/// </summary>
|
||||
/// <param name="onlyEmptyIndexes"></param>
|
||||
/// <param name="waitMilliseconds"></param>
|
||||
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<RebuildOnStartupTask>(), onlyEmptyIndexes, waitMilliseconds);
|
||||
|
||||
_rebuildOnStartupRunner = new BackgroundTaskRunner<IBackgroundTask>(
|
||||
"RebuildIndexesOnStartup",
|
||||
_loggerFactory.CreateLogger<BackgroundTaskRunner<IBackgroundTask>>(), _hostingEnvironment);
|
||||
_backgroundTaskQueue.QueueBackgroundWorkItem(cancellationToken => RebuildIndexes(onlyEmptyIndexes, delay ?? TimeSpan.Zero, cancellationToken));
|
||||
|
||||
_rebuildOnStartupRunner.TryAdd(task);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Background task used to rebuild empty indexes on startup
|
||||
/// </summary>
|
||||
private class RebuildOnStartupTask : IBackgroundTask
|
||||
private Task RebuildIndexes(bool onlyEmptyIndexes, TimeSpan delay, CancellationToken cancellationToken)
|
||||
{
|
||||
private readonly IMainDom _mainDom;
|
||||
|
||||
private readonly IndexRebuilder _indexRebuilder;
|
||||
private readonly ILogger<RebuildOnStartupTask> _logger;
|
||||
private readonly bool _onlyEmptyIndexes;
|
||||
private readonly int _waitMilliseconds;
|
||||
|
||||
public RebuildOnStartupTask(IMainDom mainDom,
|
||||
IndexRebuilder indexRebuilder, ILogger<RebuildOnStartupTask> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to rebuild indexes on startup or cold boot
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<IMedia> _mediaValueSetBuilder;
|
||||
private readonly IValueSetBuilder<IMember> _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<ExamineComponent> _logger;
|
||||
private readonly IUmbracoIndexesCreator _indexCreator;
|
||||
private readonly BackgroundTaskRunner<IBackgroundTask> _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<IMedia> mediaValueSetBuilder,
|
||||
IValueSetBuilder<IMember> 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<ExamineComponent>();
|
||||
_indexCreator = indexCreator;
|
||||
_indexItemTaskRunner = new BackgroundTaskRunner<IBackgroundTask>(loggerFactory.CreateLogger<BackgroundTaskRunner<IBackgroundTask>>(), 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -594,12 +592,14 @@ namespace Umbraco.Web.Search
|
||||
/// </summary>
|
||||
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
|
||||
/// </summary>
|
||||
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<IUmbracoIndex>()
|
||||
@@ -705,7 +709,7 @@ namespace Umbraco.Web.Search
|
||||
{
|
||||
index.IndexItems(valueSet);
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
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()
|
||||
|
||||
@@ -102,4 +102,8 @@
|
||||
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Scheduling" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
41
src/Umbraco.Tests.Common/TestHelpers/LogTestHelper.cs
Normal file
41
src/Umbraco.Tests.Common/TestHelpers/LogTestHelper.cs
Normal file
@@ -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<ILogger<T>> VerifyLogError<T>(
|
||||
this Mock<ILogger<T>> logger,
|
||||
Exception exception,
|
||||
string expectedMessage,
|
||||
Times? times = null) => VerifyLogging(logger, exception, expectedMessage, LogLevel.Error, times);
|
||||
|
||||
private static Mock<ILogger<T>> VerifyLogging<T>(
|
||||
this Mock<ILogger<T>> logger,
|
||||
Exception exception,
|
||||
string expectedMessage,
|
||||
LogLevel expectedLogLevel = LogLevel.Debug,
|
||||
Times? times = null)
|
||||
{
|
||||
times ??= Times.Once();
|
||||
|
||||
Func<object, Type, bool> state = (v, t) =>
|
||||
string.Compare(v.ToString(), expectedMessage, StringComparison.Ordinal) == 0;
|
||||
|
||||
logger.Verify(
|
||||
x => x.Log(
|
||||
It.Is<LogLevel>(l => l == expectedLogLevel),
|
||||
It.IsAny<EventId>(),
|
||||
It.Is<It.IsAnyType>((v, t) => state(v, t)),
|
||||
exception,
|
||||
It.Is<Func<It.IsAnyType, Exception, string>>((v, t) => true)), (Times)times);
|
||||
|
||||
return logger;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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<BackgroundIndexRebuilder> 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
|
||||
}
|
||||
|
||||
47
src/Umbraco.Tests.UnitTests/Umbraco.Core/TaskHelperTests.cs
Normal file
47
src/Umbraco.Tests.UnitTests/Umbraco.Core/TaskHelperTests.cs
Normal file
@@ -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<TaskHelper> 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<TaskHelper> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<IBackgroundTask>(new BackgroundTaskRunnerOptions { KeepAlive = true, LongRunning = true }, _loggerFactory.CreateLogger<BackgroundTaskRunner<IBackgroundTask>>(), 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<int> _completionSource;
|
||||
|
||||
public async Task RunAsync(CancellationToken token)
|
||||
{
|
||||
_completionSource = new TaskCompletionSource<int>();
|
||||
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<IBackgroundTask>(new BackgroundTaskRunnerOptions { KeepAlive = true, LongRunning = true }, _loggerFactory.CreateLogger<BackgroundTaskRunner<IBackgroundTask>>(), 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,6 +135,14 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="LegacyXmlPublishedCache\ContentXmlDto.cs" />
|
||||
<Compile Include="LegacyXmlPublishedCache\LegacyBackgroundTask\BackgroundTaskRunner.cs" />
|
||||
<Compile Include="LegacyXmlPublishedCache\LegacyBackgroundTask\BackgroundTaskRunnerOptions.cs" />
|
||||
<Compile Include="LegacyXmlPublishedCache\LegacyBackgroundTask\IBackgroundTask.cs" />
|
||||
<Compile Include="LegacyXmlPublishedCache\LegacyBackgroundTask\IBackgroundTaskRunner.cs" />
|
||||
<Compile Include="LegacyXmlPublishedCache\LegacyBackgroundTask\ILatchedBackgroundTask.cs" />
|
||||
<Compile Include="LegacyXmlPublishedCache\LegacyBackgroundTask\LatchedBackgroundTaskBase.cs" />
|
||||
<Compile Include="LegacyXmlPublishedCache\LegacyBackgroundTask\TaskEventArgs.cs" />
|
||||
<Compile Include="LegacyXmlPublishedCache\LegacyBackgroundTask\ThreadingTaskImmutable.cs" />
|
||||
<Compile Include="LegacyXmlPublishedCache\PreviewXmlDto.cs" />
|
||||
<Compile Include="Models\ContentXmlTest.cs" />
|
||||
<Compile Include="PublishedContent\SolidPublishedSnapshot.cs" />
|
||||
@@ -169,7 +177,6 @@
|
||||
<Compile Include="Routing\UrlProviderWithoutHideTopLevelNodeFromPathTests.cs" />
|
||||
<Compile Include="Routing\UrlRoutesTests.cs" />
|
||||
<Compile Include="Routing\UrlsProviderWithDomainsTests.cs" />
|
||||
<Compile Include="Scheduling\BackgroundTaskRunnerTests2.cs" />
|
||||
<Compile Include="Scoping\PassThroughEventDispatcherTests.cs" />
|
||||
<Compile Include="Scoping\ScopedXmlTests.cs" />
|
||||
<Compile Include="Scoping\ScopedNuCacheTests.cs" />
|
||||
@@ -201,7 +208,6 @@
|
||||
<Compile Include="PublishedContent\PublishedContentExtensionTests.cs" />
|
||||
<Compile Include="PublishedContent\PublishedRouterTests.cs" />
|
||||
<Compile Include="PublishedContent\RootNodeTests.cs" />
|
||||
<Compile Include="Scheduling\BackgroundTaskRunnerTests.cs" />
|
||||
<Compile Include="Cache\PublishedCache\PublishedMediaCacheTests.cs" />
|
||||
<Compile Include="Models\MediaXmlTest.cs" />
|
||||
<Compile Include="Persistence\FaultHandling\ConnectionRetryTest.cs" />
|
||||
|
||||
@@ -150,6 +150,7 @@ namespace Umbraco.Web.Common.DependencyInjection
|
||||
/// </summary>
|
||||
public static IUmbracoBuilder AddHostedServices(this IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.AddHostedService<QueuedHostedService>();
|
||||
builder.Services.AddHostedService<HealthCheckNotifier>();
|
||||
builder.Services.AddHostedService<KeepAlive>();
|
||||
builder.Services.AddHostedService<LogScrubber>();
|
||||
|
||||
Reference in New Issue
Block a user