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:
Bjarke Berg
2021-01-25 09:26:38 +01:00
committed by GitHub
parent 647d6ac5ab
commit c79b31ed2e
29 changed files with 371 additions and 1571 deletions

View File

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

View File

@@ -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
}
}

View 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");
}
};
}
}

View File

@@ -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;
}

View File

@@ -123,7 +123,7 @@ namespace Umbraco.Infrastructure.DependencyInjection
() =>
{
var indexRebuilder = factory.GetRequiredService<BackgroundIndexRebuilder>();
indexRebuilder.RebuildIndexes(false, 5000);
indexRebuilder.RebuildIndexes(false, TimeSpan.FromSeconds(5));
}
}
};

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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()
{
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}));
});
}
}

View File

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

View File

@@ -102,4 +102,8 @@
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Scheduling" />
</ItemGroup>
</Project>

View 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;
}
}
}

View File

@@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>

View File

@@ -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
}

View 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

View File

@@ -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);
}
}
}

View File

@@ -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" />

View File

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