Merge pull request #6757 from umbraco/v8/bugfix/6546-MainDom-Cleanup

Cleaning up MainDom and other resources preventing clean shutdown/startup
This commit is contained in:
Bjarke Berg
2019-12-05 14:12:54 +01:00
committed by GitHub
11 changed files with 326 additions and 196 deletions

View File

@@ -15,7 +15,7 @@ namespace Umbraco.Core
/// <para>When an AppDomain starts, it tries to acquire the main domain status.</para>
/// <para>When an AppDomain stops (eg the application is restarting) it should release the main domain status.</para>
/// </remarks>
internal class MainDom : IMainDom, IRegisteredObject
internal class MainDom : IMainDom, IRegisteredObject, IDisposable
{
#region Vars
@@ -25,8 +25,8 @@ namespace Umbraco.Core
private readonly object _locko = new object();
// async lock representing the main domain lock
private readonly AsyncLock _asyncLock;
private IDisposable _asyncLocker;
private readonly SystemLock _systemLock;
private IDisposable _systemLocker;
// event wait handle used to notify current main domain that it should
// release the lock because a new domain wants to be the main domain
@@ -48,13 +48,13 @@ namespace Umbraco.Core
// initializes a new instance of MainDom
public MainDom(ILogger logger)
{
HostingEnvironment.RegisterObject(this);
_logger = logger;
var appId = string.Empty;
// HostingEnvironment.ApplicationID is null in unit tests, making ReplaceNonAlphanumericChars fail
if (HostingEnvironment.ApplicationID != null)
appId = HostingEnvironment.ApplicationID.ReplaceNonAlphanumericChars(string.Empty);
var appId = HostingEnvironment.ApplicationID?.ReplaceNonAlphanumericChars(string.Empty) ?? string.Empty;
// combining with the physical path because if running on eg IIS Express,
// two sites could have the same appId even though they are different.
//
@@ -64,11 +64,11 @@ namespace Umbraco.Core
// we *cannot* use the process ID here because when an AppPool restarts it is
// a new process for the same application path
var appPath = HostingEnvironment.ApplicationPhysicalPath;
var appPath = HostingEnvironment.ApplicationPhysicalPath?.ToLowerInvariant() ?? string.Empty;
var hash = (appId + ":::" + appPath).GenerateHash<SHA1>();
var lockName = "UMBRACO-" + hash + "-MAINDOM-LCK";
_asyncLock = new AsyncLock(lockName);
_systemLock = new SystemLock(lockName);
var eventName = "UMBRACO-" + hash + "-MAINDOM-EVT";
_signal = new EventWaitHandle(false, EventResetMode.AutoReset, eventName);
@@ -99,6 +99,12 @@ namespace Umbraco.Core
lock (_locko)
{
if (_signaled) return false;
if (_isMainDom == false)
{
_logger.Warn<MainDom>("Register called when MainDom has not been acquired");
return false;
}
install?.Invoke();
if (release != null)
_callbacks.Add(new KeyValuePair<int, Action>(weight, release));
@@ -118,32 +124,32 @@ namespace Umbraco.Core
if (_signaled) return;
if (_isMainDom == false) return; // probably not needed
_signaled = true;
}
try
{
_logger.Info<MainDom>("Stopping ({SignalSource})", source);
foreach (var callback in _callbacks.OrderBy(x => x.Key).Select(x => x.Value))
try
{
try
_logger.Info<MainDom>("Stopping ({SignalSource})", source);
foreach (var callback in _callbacks.OrderBy(x => x.Key).Select(x => x.Value))
{
callback(); // no timeout on callbacks
try
{
callback(); // no timeout on callbacks
}
catch (Exception e)
{
_logger.Error<MainDom>(e, "Error while running callback");
continue;
}
}
catch (Exception e)
{
_logger.Error<MainDom>(e, "Error while running callback, remaining callbacks will not run.");
throw;
}
_logger.Debug<MainDom>("Stopped ({SignalSource})", source);
}
_logger.Debug<MainDom>("Stopped ({SignalSource})", source);
}
finally
{
// in any case...
_isMainDom = false;
_asyncLocker.Dispose();
_logger.Info<MainDom>("Released ({SignalSource})", source);
finally
{
// in any case...
_isMainDom = false;
_systemLocker?.Dispose();
_logger.Info<MainDom>("Released ({SignalSource})", source);
}
}
}
@@ -172,25 +178,29 @@ namespace Umbraco.Core
// if more than 1 instance reach that point, one will get the lock
// and the other one will timeout, which is accepted
//TODO: This can throw a TimeoutException - in which case should this be in a try/finally to ensure the signal is always reset?
_asyncLocker = _asyncLock.Lock(LockTimeoutMilliseconds);
//This can throw a TimeoutException - in which case should this be in a try/finally to ensure the signal is always reset.
try
{
_systemLocker = _systemLock.Lock(LockTimeoutMilliseconds);
}
finally
{
// we need to reset the event, because otherwise we would end up
// signaling ourselves and committing suicide immediately.
// only 1 instance can reach that point, but other instances may
// have started and be trying to get the lock - they will timeout,
// which is accepted
_signal.Reset();
}
_isMainDom = true;
// we need to reset the event, because otherwise we would end up
// signaling ourselves and committing suicide immediately.
// only 1 instance can reach that point, but other instances may
// have started and be trying to get the lock - they will timeout,
// which is accepted
_signal.Reset();
//WaitOneAsync (ext method) will wait for a signal without blocking the main thread, the waiting is done on a background thread
_signal.WaitOneAsync()
.ContinueWith(_ => OnSignal("signal"));
HostingEnvironment.RegisterObject(this);
_logger.Info<MainDom>("Acquired.");
return true;
}
@@ -204,14 +214,39 @@ namespace Umbraco.Core
// IRegisteredObject
void IRegisteredObject.Stop(bool immediate)
{
try
OnSignal("environment"); // will run once
// The web app is stopping, need to wind down
Dispose(true);
HostingEnvironment.UnregisterObject(this);
}
#region IDisposable Support
// This code added to correctly implement the disposable pattern.
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
OnSignal("environment"); // will run once
}
finally
{
HostingEnvironment.UnregisterObject(this);
if (disposing)
{
_signal?.Close();
_signal?.Dispose();
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(true);
}
#endregion
}
}

View File

@@ -157,6 +157,8 @@ namespace Umbraco.Core.Runtime
// create & initialize the components
_components = _factory.GetInstance<ComponentCollection>();
_components.Initialize();
}
catch (Exception e)
{

View File

@@ -21,18 +21,18 @@ namespace Umbraco.Core
// been closed, the Semaphore system object is destroyed - so in any case
// an iisreset should clean up everything
//
internal class AsyncLock
internal class SystemLock
{
private readonly SemaphoreSlim _semaphore;
private readonly Semaphore _semaphore2;
private readonly IDisposable _releaser;
private readonly Task<IDisposable> _releaserTask;
public AsyncLock()
: this (null)
public SystemLock()
: this(null)
{ }
public AsyncLock(string name)
public SystemLock(string name)
{
// WaitOne() waits until count > 0 then decrements count
// Release() increments count
@@ -67,35 +67,6 @@ namespace Umbraco.Core
: new NamedSemaphoreReleaser(_semaphore2);
}
//NOTE: We don't use the "Async" part of this lock at all
//TODO: Remove this and rename this class something like SystemWideLock, then we can re-instate this logic if we ever need an Async lock again
//public Task<IDisposable> LockAsync()
//{
// var wait = _semaphore != null
// ? _semaphore.WaitAsync()
// : _semaphore2.WaitOneAsync();
// return wait.IsCompleted
// ? _releaserTask ?? Task.FromResult(CreateReleaser()) // anonymous vs named
// : wait.ContinueWith((_, state) => (((AsyncLock) state).CreateReleaser()),
// this, CancellationToken.None,
// TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
//}
//public Task<IDisposable> LockAsync(int millisecondsTimeout)
//{
// var wait = _semaphore != null
// ? _semaphore.WaitAsync(millisecondsTimeout)
// : _semaphore2.WaitOneAsync(millisecondsTimeout);
// return wait.IsCompleted
// ? _releaserTask ?? Task.FromResult(CreateReleaser()) // anonymous vs named
// : wait.ContinueWith((_, state) => (((AsyncLock)state).CreateReleaser()),
// this, CancellationToken.None,
// TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
//}
public IDisposable Lock()
{
if (_semaphore != null)
@@ -121,14 +92,18 @@ namespace Umbraco.Core
private class NamedSemaphoreReleaser : CriticalFinalizerObject, IDisposable
{
private readonly Semaphore _semaphore;
private GCHandle _handle;
internal NamedSemaphoreReleaser(Semaphore semaphore)
{
_semaphore = semaphore;
_handle = GCHandle.Alloc(_semaphore);
}
#region IDisposable Support
// This code added to correctly implement the disposable pattern.
private bool disposedValue = false; // To detect redundant calls
public void Dispose()
{
Dispose(true);
@@ -137,10 +112,22 @@ namespace Umbraco.Core
private void Dispose(bool disposing)
{
// critical
_handle.Free();
_semaphore.Release();
_semaphore.Dispose();
if (!disposedValue)
{
try
{
_semaphore.Release();
}
finally
{
try
{
_semaphore.Dispose();
}
catch { }
}
disposedValue = true;
}
}
// we WANT to release the semaphore because it's a system object, ie a critical
@@ -171,6 +158,9 @@ namespace Umbraco.Core
// we do NOT want the finalizer to throw - never ever
}
}
#endregion
}
private class SemaphoreSlimReleaser : IDisposable

View File

@@ -128,7 +128,7 @@
</Compile>
-->
<Compile Include="AssemblyExtensions.cs" />
<Compile Include="AsyncLock.cs" />
<Compile Include="SystemLock.cs" />
<Compile Include="Attempt.cs" />
<Compile Include="AttemptOfTResult.cs" />
<Compile Include="AttemptOfTResultTStatus.cs" />

View File

@@ -39,7 +39,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache
return scopeProvider?.Context?.GetEnlisted<SafeXmlReaderWriter>(EnlistKey);
}
public static SafeXmlReaderWriter Get(IScopeProvider scopeProvider, AsyncLock xmlLock, XmlDocument xml, Action<XmlDocument> refresh, Action<XmlDocument, bool> apply, bool writer)
public static SafeXmlReaderWriter Get(IScopeProvider scopeProvider, SystemLock xmlLock, XmlDocument xml, Action<XmlDocument> refresh, Action<XmlDocument, bool> apply, bool writer)
{
var scopeContext = scopeProvider.Context;

View File

@@ -305,7 +305,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache
private XmlDocument _xmlDocument; // supplied xml document (for tests)
private volatile XmlDocument _xml; // master xml document
private readonly AsyncLock _xmlLock = new AsyncLock(); // protects _xml
private readonly SystemLock _xmlLock = new SystemLock(); // protects _xml
// to be used by PublishedContentCache only
// for non-preview content only

View File

@@ -24,7 +24,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache
private bool _released;
private Timer _timer;
private DateTime _initialTouch;
private readonly AsyncLock _runLock = new AsyncLock(); // ensure we run once at a time
private readonly SystemLock _runLock = new SystemLock(); // ensure we run once at a time
// note:
// as long as the runner controls the runs, we know that we run once at a time, but

View File

@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Tests.TestHelpers;
using Umbraco.Web.Scheduling;
namespace Umbraco.Tests.Scheduling
@@ -21,7 +22,7 @@ namespace Umbraco.Tests.Scheduling
[OneTimeSetUp]
public void InitializeFixture()
{
_logger = new DebugDiagnosticsLogger();
_logger = new ConsoleLogger();
}
[Test]
@@ -102,12 +103,12 @@ namespace Umbraco.Tests.Scheduling
{
using (var runner = new BackgroundTaskRunner<IBackgroundTask>(new BackgroundTaskRunnerOptions(), _logger))
{
MyTask t;
MyTask t1, t2, t3;
Assert.IsFalse(runner.IsRunning); // because AutoStart is false
runner.Add(new MyTask(5000));
runner.Add(new MyTask());
runner.Add(t = new MyTask());
runner.Add(t1 = new MyTask(5000));
runner.Add(t2 = new MyTask());
runner.Add(t3 = new MyTask());
Assert.IsTrue(runner.IsRunning); // is running tasks
// shutdown -force => run all queued tasks
@@ -115,7 +116,7 @@ namespace Umbraco.Tests.Scheduling
Assert.IsTrue(runner.IsRunning); // is running tasks
await runner.StoppedAwaitable; // runner stops, within test's timeout
Assert.AreNotEqual(DateTime.MinValue, t.Ended); // t has run
Assert.AreNotEqual(DateTime.MinValue, t3.Ended); // t3 has run
}
}
@@ -124,20 +125,25 @@ namespace Umbraco.Tests.Scheduling
{
using (var runner = new BackgroundTaskRunner<IBackgroundTask>(new BackgroundTaskRunnerOptions(), _logger))
{
MyTask t;
MyTask t1, t2, t3;
Assert.IsFalse(runner.IsRunning); // because AutoStart is false
runner.Add(new MyTask(5000));
runner.Add(new MyTask());
runner.Add(t = new MyTask());
runner.Add(t1 = new MyTask(5000));
runner.Add(t2 = new MyTask());
runner.Add(t3 = new MyTask());
Assert.IsTrue(runner.IsRunning); // is running tasks
Thread.Sleep(1000); // since we are forcing shutdown, we need to give it a chance to start, else it will be canceled before the queue is started
// shutdown +force => tries to cancel the current task, ignores queued tasks
runner.Shutdown(true, false); // +force -wait
Assert.IsTrue(runner.IsRunning); // is running that long task it cannot cancel
await runner.StoppedAwaitable; // runner stops, within test's timeout
Assert.AreEqual(DateTime.MinValue, t.Ended); // t has *not* run
await runner.StoppedAwaitable; // runner stops, within test's timeout (no cancelation token used, no need to catch OperationCanceledException)
Assert.AreNotEqual(DateTime.MinValue, t1.Ended); // t1 *has* run
Assert.AreEqual(DateTime.MinValue, t2.Ended); // t2 has *not* run
Assert.AreEqual(DateTime.MinValue, t3.Ended); // t3 has *not* run
}
}
@@ -163,7 +169,15 @@ namespace Umbraco.Tests.Scheduling
// shutdown +force => tries to cancel the current task, ignores queued tasks
runner.Shutdown(true, false); // +force -wait
await runner.StoppedAwaitable; // runner stops, within test's timeout
try
{
await runner.StoppedAwaitable; // runner stops, within test's timeout ... maybe
}
catch (OperationCanceledException)
{
// catch exception, this can occur because we are +force shutting down which will
// cancel a pending task if the queue hasn't completed in time
}
}
}
@@ -183,25 +197,20 @@ namespace Umbraco.Tests.Scheduling
runner.Terminated += (sender, args) => { terminated = true; };
Assert.IsFalse(runner.IsRunning); // because AutoStart is false
runner.Add(new MyTask(5000));
runner.Add(new MyTask());
runner.Add(t = new MyTask());
runner.Add(new MyTask()); // sleeps 500 ms
runner.Add(new MyTask()); // sleeps 500 ms
runner.Add(t = new MyTask()); // sleeps 500 ms ... total = 1500 ms until it's done
Assert.IsTrue(runner.IsRunning); // is running the task
runner.Stop(false); // -immediate = -force, -wait
runner.Stop(false); // -immediate = -force, -wait (max 2000 ms delay before +immediate)
await runner.TerminatedAwaitable;
Assert.IsTrue(stopped); // raised that one
Assert.IsTrue(terminating); // has raised that event
Assert.IsFalse(terminated); // but not terminated yet
Assert.IsTrue(terminated); // and that event
// all this before we await because -wait
Assert.IsTrue(runner.IsCompleted); // shutdown completes the runner
Assert.IsTrue(runner.IsRunning); // still running the task
await runner.StoppedAwaitable; // runner stops, within test's timeout
Assert.IsFalse(runner.IsRunning);
Assert.IsTrue(stopped);
await runner.TerminatedAwaitable; // runner terminates, within test's timeout
Assert.IsTrue(terminated); // has raised that event
Assert.IsFalse(runner.IsRunning); // done running
Assert.AreNotEqual(DateTime.MinValue, t.Ended); // t has run
}
@@ -222,23 +231,21 @@ namespace Umbraco.Tests.Scheduling
runner.Terminated += (sender, args) => { terminated = true; };
Assert.IsFalse(runner.IsRunning); // because AutoStart is false
runner.Add(new MyTask(5000));
runner.Add(new MyTask());
runner.Add(t = new MyTask());
runner.Add(new MyTask()); // sleeps 500 ms
runner.Add(new MyTask()); // sleeps 500 ms
runner.Add(t = new MyTask()); // sleeps 500 ms ... total = 1500 ms until it's done
Assert.IsTrue(runner.IsRunning); // is running the task
runner.Stop(true); // +immediate = +force, +wait
runner.Stop(true); // +immediate = +force, +wait (no delay)
await runner.TerminatedAwaitable;
Assert.IsTrue(stopped); // raised that one
Assert.IsTrue(terminating); // has raised that event
Assert.IsTrue(terminated); // and that event
Assert.IsTrue(stopped); // and that one
// and all this before we await because +wait
Assert.IsTrue(runner.IsCompleted); // shutdown completes the runner
Assert.IsFalse(runner.IsRunning); // done running
await runner.StoppedAwaitable; // runner stops, within test's timeout
await runner.TerminatedAwaitable; // runner terminates, within test's timeout
Assert.AreEqual(DateTime.MinValue, t.Ended); // t has *not* run
}
}
@@ -264,8 +271,7 @@ namespace Umbraco.Tests.Scheduling
}, _logger))
{
Assert.IsTrue(runner.IsRunning); // because AutoStart is true
runner.Stop(false); // keepalive = must be stopped
await runner.StoppedAwaitable; // runner stops, within test's timeout
await runner.StopInternal(false); // keepalive = must be stopped
}
}
@@ -291,13 +297,9 @@ namespace Umbraco.Tests.Scheduling
// dispose will stop it
}
await runner.StoppedAwaitable; // runner stops, within test's timeout
//await runner.TerminatedAwaitable; // NO! see note below
await runner.StoppedAwaitable;
Assert.Throws<InvalidOperationException>(() => runner.Add(new MyTask()));
// but do NOT await on TerminatedAwaitable - disposing just shuts the runner down
// so that we don't have a runaway task in tests, etc - but it does NOT terminate
// the runner - it really is NOT a nice way to end a runner - it's there for tests
}
[Test]
@@ -564,7 +566,7 @@ namespace Umbraco.Tests.Scheduling
Thread.Sleep(1000);
Assert.IsTrue(runner.IsRunning); // still waiting for the task to release
Assert.IsFalse(task.HasRun);
task.Release();
task.Release(); // unlatch
var runnerTask = runner.CurrentThreadingTask; // may be null if things go fast enough
if (runnerTask != null)
await runnerTask; // wait for current task to complete
@@ -574,7 +576,7 @@ namespace Umbraco.Tests.Scheduling
}
[Test]
public async Task LatchedTaskStops()
public async Task LatchedTaskStops_Runs_On_Shutdown()
{
using (var runner = new BackgroundTaskRunner<IBackgroundTask>(new BackgroundTaskRunnerOptions(), _logger))
{
@@ -584,7 +586,7 @@ namespace Umbraco.Tests.Scheduling
Thread.Sleep(5000);
Assert.IsTrue(runner.IsRunning); // still waiting for the task to release
Assert.IsFalse(task.HasRun);
runner.Shutdown(false, false);
runner.Shutdown(false, false); // -force, -wait
await runner.StoppedAwaitable; // wait for the entire runner operation to complete
Assert.IsTrue(task.HasRun);
}
@@ -880,7 +882,9 @@ namespace Umbraco.Tests.Scheduling
public override void PerformRun()
{
Console.WriteLine($"Sleeping {_milliseconds}...");
Thread.Sleep(_milliseconds);
Console.WriteLine("Wake up!");
}
}
@@ -997,7 +1001,9 @@ namespace Umbraco.Tests.Scheduling
public DateTime Ended { get; set; }
public virtual void Dispose()
{ }
{
}
}
}
}

View File

@@ -235,11 +235,24 @@ namespace Umbraco.Web.PublishedCache.NuCache
var lockInfo = new WriteLockInfo();
try
{
Lock(lockInfo);
try
{
// Trying to lock could throw exceptions so always make sure to clean up.
Lock(lockInfo);
}
finally
{
try
{
_localDb?.Dispose();
}
catch { /* TBD: May already be throwing so don't throw again */}
finally
{
_localDb = null;
}
}
if (_localDb == null) return;
_localDb.Dispose();
_localDb = null;
}
finally
{

View File

@@ -57,7 +57,8 @@ namespace Umbraco.Web.PublishedCache.NuCache
private BPlusTree<int, ContentNodeKit> _localContentDb;
private BPlusTree<int, ContentNodeKit> _localMediaDb;
private bool _localDbExists;
private bool _localContentDbExists;
private bool _localMediaDbExists;
// define constant - determines whether to use cache when previewing
// to store eg routes, property converted values, anything - caching
@@ -127,9 +128,9 @@ namespace Umbraco.Web.PublishedCache.NuCache
// stores need to be populated, happens in OnResolutionFrozen which uses _localDbExists to
// figure out whether it can read the databases or it should populate them from sql
_logger.Info<PublishedSnapshotService>("Creating the content store, localContentDbExists? {LocalContentDbExists}", _localDbExists);
_logger.Info<PublishedSnapshotService>("Creating the content store, localContentDbExists? {LocalContentDbExists}", _localContentDbExists);
_contentStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, logger, _localContentDb);
_logger.Info<PublishedSnapshotService>("Creating the media store, localMediaDbExists? {LocalMediaDbExists}", _localDbExists);
_logger.Info<PublishedSnapshotService>("Creating the media store, localMediaDbExists? {LocalMediaDbExists}", _localMediaDbExists);
_mediaStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, logger, _localMediaDb);
}
else
@@ -170,14 +171,15 @@ namespace Umbraco.Web.PublishedCache.NuCache
var path = GetLocalFilesPath();
var localContentDbPath = Path.Combine(path, "NuCache.Content.db");
var localMediaDbPath = Path.Combine(path, "NuCache.Media.db");
var localContentDbExists = File.Exists(localContentDbPath);
var localMediaDbExists = File.Exists(localMediaDbPath);
_localDbExists = localContentDbExists && localMediaDbExists;
// if both local databases exist then GetTree will open them, else new databases will be created
_localContentDb = BTree.GetTree(localContentDbPath, _localDbExists);
_localMediaDb = BTree.GetTree(localMediaDbPath, _localDbExists);
_localContentDbExists = File.Exists(localContentDbPath);
_localMediaDbExists = File.Exists(localMediaDbPath);
_logger.Info<PublishedSnapshotService>("Registered with MainDom, localContentDbExists? {LocalContentDbExists}, localMediaDbExists? {LocalMediaDbExists}", localContentDbExists, localMediaDbExists);
// if both local databases exist then GetTree will open them, else new databases will be created
_localContentDb = BTree.GetTree(localContentDbPath, _localContentDbExists);
_localMediaDb = BTree.GetTree(localMediaDbPath, _localMediaDbExists);
_logger.Info<PublishedSnapshotService>("Registered with MainDom, localContentDbExists? {LocalContentDbExists}, localMediaDbExists? {LocalMediaDbExists}", _localContentDbExists, _localMediaDbExists);
}
/// <summary>
@@ -210,11 +212,15 @@ namespace Umbraco.Web.PublishedCache.NuCache
try
{
if (_localDbExists)
if (_localContentDbExists)
{
okContent = LockAndLoadContent(scope => LoadContentFromLocalDbLocked(true));
if (!okContent)
_logger.Warn<PublishedSnapshotService>("Loading content from local db raised warnings, will reload from database.");
_logger.Warn<PublishedSnapshotService>("Loading content from local db raised warnings, will reload from database.");
}
if (_localMediaDbExists)
{
okMedia = LockAndLoadMedia(scope => LoadMediaFromLocalDbLocked(true));
if (!okMedia)
_logger.Warn<PublishedSnapshotService>("Loading media from local db raised warnings, will reload from database.");

View File

@@ -199,7 +199,7 @@ namespace Umbraco.Web.Scheduling
{
lock (_locker)
{
var task = _runningTask ?? Task.FromResult(0);
var task = _runningTask ?? Task.CompletedTask;
return new ThreadingTaskImmutable(task);
}
}
@@ -211,8 +211,9 @@ namespace Umbraco.Web.Scheduling
/// <returns>An awaitable object.</returns>
/// <remarks>
/// <para>Used to wait until the runner has terminated.</para>
/// <para>This is for unit tests and should not be used otherwise. In most cases when the runner
/// has terminated, the application domain is going down and it is not the right time to do things.</para>
/// <para>
/// The only time the runner will be terminated is by the Hosting Environment when the application is being shutdown.
/// </para>
/// </remarks>
internal ThreadingTaskImmutable TerminatedAwaitable
{
@@ -338,29 +339,37 @@ namespace Umbraco.Web.Scheduling
if (_isRunning == false) return; // done already
}
var hasTasks = TaskCount > 0;
if (!force && hasTasks)
_logger.Info<BackgroundTaskRunner>("{LogPrefix} Waiting for tasks to complete", _logPrefix);
// complete the queue
// will stop waiting on the queue or on a latch
_tasks.Complete();
if (force)
{
// we must bring everything down, now
Thread.Sleep(100); // give time to Complete()
// we must bring everything down, now
lock (_locker)
{
// was Complete() enough?
if (_isRunning == false) return;
// if _tasks.Complete() ended up triggering code to stop the runner and reset
// the _isRunning flag, then there's no need to initiate a cancel on the cancelation token.
if (_isRunning == false)
return;
}
// try to cancel running async tasks (cannot do much about sync tasks)
// break latched tasks
// stop processing the queue
_shutdownTokenSource.Cancel(false); // false is the default
_shutdownTokenSource.Dispose();
_shutdownTokenSource?.Cancel(false); // false is the default
_shutdownTokenSource?.Dispose();
_shutdownTokenSource = null;
}
// tasks in the queue will be executed...
if (wait == false) return;
if (!wait) return;
_runningTask?.Wait(CancellationToken.None); // wait for whatever is running to end...
}
@@ -428,7 +437,7 @@ namespace Umbraco.Web.Scheduling
lock (_locker)
{
// deal with race condition
if (_shutdownToken.IsCancellationRequested == false && _tasks.Count > 0) continue;
if (_shutdownToken.IsCancellationRequested == false && TaskCount > 0) continue;
// if we really have nothing to do, stop
_logger.Debug<BackgroundTaskRunner>("{LogPrefix} Stopping", _logPrefix);
@@ -453,7 +462,7 @@ namespace Umbraco.Web.Scheduling
// if KeepAlive is false then don't block, exit if there is
// no task in the buffer - yes, there is a race condition, which
// we'll take care of
if (_options.KeepAlive == false && _tasks.Count == 0)
if (_options.KeepAlive == false && TaskCount == 0)
return null;
try
@@ -503,15 +512,19 @@ namespace Umbraco.Web.Scheduling
// returns the task that completed
// - latched.Latch completes when the latch releases
// - _tasks.Completion completes when the runner completes
// - tokenTaskSource.Task completes when this task, or the whole runner, is cancelled
// - tokenTaskSource.Task completes when this task, or the whole runner is cancelled
var task = await Task.WhenAny(latched.Latch, _tasks.Completion, tokenTaskSource.Task);
// ok to run now
if (task == latched.Latch)
return bgTask;
// we are shutting down if the _tasks.Complete(); was called or the shutdown token was cancelled
var isShuttingDown = _shutdownToken.IsCancellationRequested || task == _tasks.Completion;
// if shutting down, return the task only if it runs on shutdown
if (_shutdownToken.IsCancellationRequested == false && latched.RunsOnShutdown) return bgTask;
if (isShuttingDown && latched.RunsOnShutdown)
return bgTask;
// else, either it does not run on shutdown or it's been cancelled, dispose
latched.Dispose();
@@ -578,17 +591,18 @@ namespace Umbraco.Web.Scheduling
// triggers when the hosting environment requests that the runner terminates
internal event TypedEventHandler<BackgroundTaskRunner<T>, EventArgs> Terminating;
// triggers when the runner has terminated (no task can be added, no task is running)
// triggers when the hosting environment has terminated (no task can be added, no task is running)
internal event TypedEventHandler<BackgroundTaskRunner<T>, EventArgs> Terminated;
private void OnEvent(TypedEventHandler<BackgroundTaskRunner<T>, EventArgs> handler, string name)
{
if (handler == null) return;
OnEvent(handler, name, EventArgs.Empty);
}
private void OnEvent<TArgs>(TypedEventHandler<BackgroundTaskRunner<T>, TArgs> handler, string name, TArgs e)
{
_logger.Debug<BackgroundTaskRunner>("{LogPrefix} OnEvent {EventName}", _logPrefix, name);
if (handler == null) return;
try
@@ -664,17 +678,16 @@ namespace Umbraco.Web.Scheduling
#endregion
#region IRegisteredObject.Stop
/// <summary>
/// Requests a registered object to un-register.
/// Used by IRegisteredObject.Stop and shutdown on threadpool threads to not block shutdown times.
/// </summary>
/// <param name="immediate">true to indicate the registered object should un-register from the hosting
/// environment before returning; otherwise, false.</param>
/// <remarks>
/// <para>"When the application manager needs to stop a registered object, it will call the Stop method."</para>
/// <para>The application manager will call the Stop method to ask a registered object to un-register. During
/// processing of the Stop method, the registered object must call the HostingEnvironment.UnregisterObject method.</para>
/// </remarks>
public void Stop(bool immediate)
/// <param name="immediate"></param>
/// <returns>
/// An awaitable Task that is used to handle the shutdown.
/// </returns>
internal Task StopInternal(bool immediate)
{
// the first time the hosting environment requests that the runner terminates,
// raise the Terminating event - that could be used to prevent any process that
@@ -693,33 +706,90 @@ namespace Umbraco.Web.Scheduling
if (onTerminating)
OnEvent(Terminating, "Terminating");
if (immediate == false)
// Run the Stop commands on another thread since IRegisteredObject.Stop calls are called sequentially
// with a single aspnet thread during shutdown and we don't want to delay other calls to IRegisteredObject.Stop.
if (!immediate)
{
// The Stop method is first called with the immediate parameter set to false. The object can either complete
// processing, call the UnregisterObject method, and then return or it can return immediately and complete
// processing asynchronously before calling the UnregisterObject method.
return Task.Run(StopInitial, CancellationToken.None);
}
else
{
lock (_locker)
{
if (_terminated) return Task.CompletedTask;
return Task.Run(StopImmediate, CancellationToken.None);
}
}
}
_logger.Info<BackgroundTaskRunner>("{LogPrefix} Waiting for tasks to complete", _logPrefix);
/// <summary>
/// Requests a registered object to un-register.
/// </summary>
/// <param name="immediate">true to indicate the registered object should un-register from the hosting
/// environment before returning; otherwise, false.</param>
/// <remarks>
/// <para>"When the application manager needs to stop a registered object, it will call the Stop method."</para>
/// <para>The application manager will call the Stop method to ask a registered object to un-register. During
/// processing of the Stop method, the registered object must call the HostingEnvironment.UnregisterObject method.</para>
/// </remarks>
public void Stop(bool immediate) => StopInternal(immediate);
/// <summary>
/// Called when immediate == false for IRegisteredObject.Stop(bool immediate)
/// </summary>
/// <remarks>
/// Called on a threadpool thread
/// </remarks>
private void StopInitial()
{
// immediate == false when the app is trying to wind down, immediate == true will be called either:
// after a call with immediate == false or if the app is not trying to wind down and needs to immediately stop.
// So Stop may be called twice or sometimes only once.
try
{
Shutdown(false, false); // do not accept any more tasks, flush the queue, do not wait
}
finally
{
// raise the completed event only after the running threading task has completed
lock (_locker)
{
if (_runningTask != null)
_runningTask.ContinueWith(_ => Terminate(false));
_runningTask.ContinueWith(_ => StopImmediate());
else
Terminate(false);
StopImmediate();
}
}
else
{
// If the registered object does not complete processing before the application manager's time-out
// period expires, the Stop method is called again with the immediate parameter set to true. When the
// immediate parameter is true, the registered object must call the UnregisterObject method before returning;
// otherwise, its registration will be removed by the application manager.
_logger.Info<BackgroundTaskRunner>("{LogPrefix} Canceling tasks", _logPrefix);
// If the shutdown token was not canceled in the Shutdown call above, it means there was still tasks
// being processed, in which case we'll give it a couple seconds
if (!_shutdownToken.IsCancellationRequested)
{
// If we are called with immediate == false, wind down above and then shutdown within 2 seconds,
// we want to shut down the app as quick as possible, if we wait until immediate == true, this can
// take a very long time since immediate will only be true when a new request is received on the new
// appdomain (or another iis timeout occurs ... which can take some time).
Thread.Sleep(2000); //we are already on a threadpool thread
StopImmediate();
}
}
/// <summary>
/// Called when immediate == true for IRegisteredObject.Stop(bool immediate)
/// </summary>
/// <remarks>
/// Called on a threadpool thread
/// </remarks>
private void StopImmediate()
{
_logger.Info<BackgroundTaskRunner>("{LogPrefix} Canceling tasks", _logPrefix);
try
{
Shutdown(true, true); // cancel all tasks, wait for the current one to end
}
finally
{
Terminate(true);
}
}
@@ -732,7 +802,13 @@ namespace Umbraco.Web.Scheduling
// raise the Terminated event
// complete the awaitable completion source, if any
HostingEnvironment.UnregisterObject(this);
if (immediate)
{
//only unregister when it's the final call, else we won't be notified of the final call
HostingEnvironment.UnregisterObject(this);
}
if (_terminated) return; // already taken care of
TaskCompletionSource<int> terminatedSource;
lock (_locker)
@@ -747,7 +823,9 @@ namespace Umbraco.Web.Scheduling
OnEvent(Terminated, "Terminated");
terminatedSource.SetResult(0);
terminatedSource.TrySetResult(0);
}
#endregion
}
}