Merge branch 'netcore/dev' into netcore/feature/move-mappings-after-httpcontext

This commit is contained in:
Bjarke Berg
2020-02-24 08:22:42 +01:00
1158 changed files with 35 additions and 29 deletions

View File

@@ -0,0 +1,53 @@
namespace Umbraco.Web.Scheduling
{
/// <summary>
/// Provides options to the <see cref="BackgroundTaskRunner{T}"/> class.
/// </summary>
public class BackgroundTaskRunnerOptions
{
// TODO: Could add options for using a stack vs queue if required
/// <summary>
/// Initializes a new instance of the <see cref="BackgroundTaskRunnerOptions"/> class.
/// </summary>
public BackgroundTaskRunnerOptions()
{
LongRunning = false;
KeepAlive = false;
AutoStart = false;
PreserveRunningTask = false;
Hosted = true;
}
/// <summary>
/// Gets or sets a value indicating whether the running task should be a long-running,
/// coarse grained operation.
/// </summary>
public bool LongRunning { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the running task should block and wait
/// on the queue, or end, when the queue is empty.
/// </summary>
public bool KeepAlive { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the running task should start immediately
/// or only once a task has been added to the queue.
/// </summary>
public bool AutoStart { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the running task should be preserved
/// once completed, or reset to null. For unit tests.
/// </summary>
public bool PreserveRunningTask { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the runner should register with (and be
/// stopped by) the hosting. Otherwise, something else should take care of stopping
/// the runner. True by default.
/// </summary>
public bool Hosted { get; set; }
}
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Umbraco.Web.Scheduling
{
/// <summary>
/// Represents a background task.
/// </summary>
public interface IBackgroundTask : IDisposable
{
/// <summary>
/// Runs the background task.
/// </summary>
void Run();
/// <summary>
/// Runs the task asynchronously.
/// </summary>
/// <param name="token">A cancellation token.</param>
/// <returns>A <see cref="Task"/> instance representing the execution of the background task.</returns>
/// <exception cref="NotImplementedException">The background task cannot run asynchronously.</exception>
Task RunAsync(CancellationToken token);
/// <summary>
/// Indicates whether the background task can run asynchronously.
/// </summary>
bool IsAsync { get; }
}
}

View File

@@ -0,0 +1,20 @@
using System;
using Umbraco.Core;
namespace Umbraco.Web.Scheduling
{
/// <summary>
/// Defines a service managing a queue of tasks of type <typeparamref name="T"/> and running them in the background.
/// </summary>
/// <typeparam name="T">The type of the managed tasks.</typeparam>
/// <remarks>The interface is not complete and exists only to have the contravariance on T.</remarks>
public interface IBackgroundTaskRunner<in T> : IDisposable, IRegisteredObject
where T : class, IBackgroundTask
{
bool IsCompleted { get; }
void Add(T task);
bool TryAdd(T task);
// TODO: complete the interface?
}
}

View File

@@ -0,0 +1,32 @@
using System;
using System.Threading.Tasks;
namespace Umbraco.Web.Scheduling
{
/// <summary>
/// Represents a latched background task.
/// </summary>
/// <remarks>Latched background tasks can suspend their execution until
/// a condition is met. However if the tasks runner has to terminate,
/// latched background tasks can be executed immediately, depending on
/// the value returned by RunsOnShutdown.</remarks>
public interface ILatchedBackgroundTask : IBackgroundTask
{
/// <summary>
/// Gets a task on latch.
/// </summary>
/// <exception cref="InvalidOperationException">The task is not latched.</exception>
Task Latch { get; }
/// <summary>
/// Gets a value indicating whether the task is latched.
/// </summary>
/// <remarks>Should return false as soon as the condition is met.</remarks>
bool IsLatched { get; }
/// <summary>
/// Gets a value indicating whether the task can be executed immediately if the task runner has to terminate.
/// </summary>
bool RunsOnShutdown { get; }
}
}

View File

@@ -0,0 +1,81 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Umbraco.Core;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Logging;
using Umbraco.Core.Sync;
namespace Umbraco.Web.Scheduling
{
public class KeepAlive : RecurringTaskBase
{
private readonly IRuntimeState _runtime;
private readonly IKeepAliveSection _keepAliveSection;
private readonly IProfilingLogger _logger;
private static HttpClient _httpClient;
public KeepAlive(IBackgroundTaskRunner<RecurringTaskBase> runner, int delayMilliseconds, int periodMilliseconds,
IRuntimeState runtime, IKeepAliveSection keepAliveSection, IProfilingLogger logger)
: base(runner, delayMilliseconds, periodMilliseconds)
{
_runtime = runtime;
_keepAliveSection = keepAliveSection;
_logger = logger;
if (_httpClient == null)
_httpClient = new HttpClient();
}
public override async Task<bool> PerformRunAsync(CancellationToken token)
{
// not on replicas nor unknown role servers
switch (_runtime.ServerRole)
{
case ServerRole.Replica:
_logger.Debug<KeepAlive>("Does not run on replica servers.");
return true; // role may change!
case ServerRole.Unknown:
_logger.Debug<KeepAlive>("Does not run on servers with unknown role.");
return true; // role may change!
}
// ensure we do not run if not main domain, but do NOT lock it
if (_runtime.IsMainDom == false)
{
_logger.Debug<KeepAlive>("Does not run if not MainDom.");
return false; // do NOT repeat, going down
}
using (_logger.DebugDuration<KeepAlive>("Keep alive executing", "Keep alive complete"))
{
var keepAlivePingUrl = _keepAliveSection.KeepAlivePingUrl;
try
{
if (keepAlivePingUrl.Contains("{umbracoApplicationUrl}"))
{
var umbracoAppUrl = _runtime.ApplicationUrl.ToString();
if (umbracoAppUrl.IsNullOrWhiteSpace())
{
_logger.Warn<KeepAlive>("No umbracoApplicationUrl for service (yet), skip.");
return true; // repeat
}
keepAlivePingUrl = keepAlivePingUrl.Replace("{umbracoApplicationUrl}", umbracoAppUrl.TrimEnd('/'));
}
var request = new HttpRequestMessage(HttpMethod.Get, keepAlivePingUrl);
var result = await _httpClient.SendAsync(request, token);
}
catch (Exception ex)
{
_logger.Error<KeepAlive>(ex, "Keep alive failed (at '{keepAlivePingUrl}').", keepAlivePingUrl);
}
}
return true; // repeat
}
public override bool IsAsync => true;
}
}

View File

@@ -0,0 +1,61 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Umbraco.Core;
namespace Umbraco.Web.Scheduling
{
public abstract class LatchedBackgroundTaskBase : DisposableObjectSlim, ILatchedBackgroundTask
{
private TaskCompletionSource<bool> _latch;
protected LatchedBackgroundTaskBase()
{
_latch = new TaskCompletionSource<bool>();
}
/// <summary>
/// Implements IBackgroundTask.Run().
/// </summary>
public virtual void Run()
{
throw new NotSupportedException("This task cannot run synchronously.");
}
/// <summary>
/// Implements IBackgroundTask.RunAsync().
/// </summary>
public virtual Task RunAsync(CancellationToken token)
{
throw new NotSupportedException("This task cannot run asynchronously.");
}
/// <summary>
/// Indicates whether the background task can run asynchronously.
/// </summary>
public abstract bool IsAsync { get; }
public Task Latch => _latch.Task;
public bool IsLatched => _latch.Task.IsCompleted == false;
protected void Release()
{
_latch.SetResult(true);
}
protected void Reset()
{
_latch = new TaskCompletionSource<bool>();
}
public virtual bool RunsOnShutdown => false;
// the task is going to be disposed after execution,
// unless it is latched again, thus indicating it wants to
// remain active
protected override void DisposeResources()
{ }
}
}

View File

@@ -0,0 +1,107 @@
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 int _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, 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

@@ -0,0 +1,62 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Umbraco.Web.Scheduling
{
internal static class TaskAndFactoryExtensions
{
#region Task Extensions
static void SetCompletionSource<TResult>(TaskCompletionSource<TResult> completionSource, Task task)
{
if (task.IsFaulted)
completionSource.SetException(task.Exception.InnerException);
else
completionSource.SetResult(default(TResult));
}
static void SetCompletionSource<TResult>(TaskCompletionSource<TResult> completionSource, Task<TResult> task)
{
if (task.IsFaulted)
completionSource.SetException(task.Exception.InnerException);
else
completionSource.SetResult(task.Result);
}
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)));
return completionSource.Task;
}
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), token);
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,42 @@
using System;
namespace Umbraco.Web.Scheduling
{
/// <summary>
/// Provides arguments for task runner events.
/// </summary>
/// <typeparam name="T">The type of the task.</typeparam>
public class TaskEventArgs<T> : EventArgs
where T : IBackgroundTask
{
/// <summary>
/// Initializes a new instance of the <see cref="TaskEventArgs{T}"/> class with a task.
/// </summary>
/// <param name="task">The task.</param>
public TaskEventArgs(T task)
{
Task = task;
}
/// <summary>
/// Initializes a new instance of the <see cref="TaskEventArgs{T}"/> class with a task and an exception.
/// </summary>
/// <param name="task">The task.</param>
/// <param name="exception">An exception.</param>
public TaskEventArgs(T task, Exception exception)
{
Task = task;
Exception = exception;
}
/// <summary>
/// Gets or sets the task.
/// </summary>
public T Task { get; private set; }
/// <summary>
/// Gets or sets the exception.
/// </summary>
public Exception Exception { get; private set; }
}
}

View File

@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Logging;
namespace Umbraco.Web.Scheduling
{
/// <summary>
/// Used to cleanup temporary file locations
/// </summary>
public class TempFileCleanup : RecurringTaskBase
{
private readonly DirectoryInfo[] _tempFolders;
private readonly TimeSpan _age;
private readonly IRuntimeState _runtime;
private readonly IProfilingLogger _logger;
public TempFileCleanup(IBackgroundTaskRunner<RecurringTaskBase> runner, int delayMilliseconds, int periodMilliseconds,
IEnumerable<DirectoryInfo> tempFolders, TimeSpan age,
IRuntimeState runtime, IProfilingLogger logger)
: base(runner, delayMilliseconds, periodMilliseconds)
{
//SystemDirectories.TempFileUploads
_tempFolders = tempFolders.ToArray();
_age = age;
_runtime = runtime;
_logger = logger;
}
public override bool PerformRun()
{
// ensure we do not run if not main domain
if (_runtime.IsMainDom == false)
{
_logger.Debug<TempFileCleanup>("Does not run if not MainDom.");
return false; // do NOT repeat, going down
}
foreach (var dir in _tempFolders)
CleanupFolder(dir);
return true; //repeat
}
private void CleanupFolder(DirectoryInfo dir)
{
dir.Refresh(); //in case it's changed during runtime
if (!dir.Exists)
{
_logger.Debug<TempFileCleanup>("The cleanup folder doesn't exist {Folder}", dir.FullName);
}
var files = dir.GetFiles("*.*", SearchOption.AllDirectories);
foreach (var file in files)
{
if (DateTime.UtcNow - file.LastWriteTimeUtc > _age)
{
try
{
file.Delete();
}
catch (Exception ex)
{
_logger.Error<TempFileCleanup>(ex, "Could not delete temp file {FileName}", file.FullName);
}
}
}
}
public override bool IsAsync => false;
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
namespace Umbraco.Web.Scheduling
{
/// <summary>
/// Wraps a <see cref="Task"/> within an object that gives access to its GetAwaiter method and Status
/// property while ensuring that it cannot be modified in any way.
/// </summary>
public class ThreadingTaskImmutable
{
private readonly Task _task;
/// <summary>
/// Initializes a new instance of the <see cref="ThreadingTaskImmutable"/> class with a Task.
/// </summary>
/// <param name="task">The task.</param>
public ThreadingTaskImmutable(Task task)
{
if (task == null)
throw new ArgumentNullException("task");
_task = task;
}
/// <summary>
/// Gets an awaiter used to await the task.
/// </summary>
/// <returns>An awaiter instance.</returns>
public TaskAwaiter GetAwaiter()
{
return _task.GetAwaiter();
}
/// <summary>
/// Gets the TaskStatus of the task.
/// </summary>
/// <returns>The current TaskStatus of the task.</returns>
public TaskStatus Status
{
get { return _task.Status; }
}
}
}