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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +157,8 @@ namespace Umbraco.Core.Runtime
|
||||
// create & initialize the components
|
||||
_components = _factory.GetInstance<ComponentCollection>();
|
||||
_components.Initialize();
|
||||
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
{ }
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user