2015-01-29 12:45:44 +11:00
|
|
|
using System;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using System.Threading.Tasks;
|
2015-02-08 16:25:30 +01:00
|
|
|
using umbraco;
|
2015-01-29 12:45:44 +11:00
|
|
|
using Umbraco.Core;
|
|
|
|
|
using Umbraco.Core.Logging;
|
|
|
|
|
using Umbraco.Web.Scheduling;
|
|
|
|
|
|
|
|
|
|
namespace Umbraco.Web.PublishedCache.XmlPublishedCache
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// This is the background task runner that persists the xml file to the file system
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <remarks>
|
|
|
|
|
/// This is used so that all file saving is done on a web aware worker background thread and all logic is performed async so this
|
|
|
|
|
/// process will not interfere with any web requests threads. This is also done as to not require any global locks and to ensure that
|
|
|
|
|
/// if multiple threads are performing publishing tasks that the file will be persisted in accordance with the final resulting
|
|
|
|
|
/// xml structure since the file writes are queued.
|
|
|
|
|
/// </remarks>
|
2015-07-08 17:41:20 +02:00
|
|
|
internal class XmlCacheFilePersister : LatchedBackgroundTaskBase
|
2015-01-29 12:45:44 +11:00
|
|
|
{
|
2015-02-08 16:25:30 +01:00
|
|
|
private readonly IBackgroundTaskRunner<XmlCacheFilePersister> _runner;
|
|
|
|
|
private readonly content _content;
|
|
|
|
|
private readonly object _locko = new object();
|
|
|
|
|
private bool _released;
|
|
|
|
|
private Timer _timer;
|
|
|
|
|
private DateTime _initialTouch;
|
2015-04-13 19:11:12 +02:00
|
|
|
private readonly AsyncLock _runLock = new AsyncLock(); // 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
|
|
|
|
|
// when the AppDomain goes down and the runner has completed and yet the persister is
|
|
|
|
|
// asked to save, then we need to run immediately - but the runner may be running, so
|
|
|
|
|
// we need to make sure there's no collision - hence _runLock
|
2015-01-29 12:45:44 +11:00
|
|
|
|
2015-02-08 16:25:30 +01:00
|
|
|
private const int WaitMilliseconds = 4000; // save the cache 4s after the last change (ie every 4s min)
|
2015-03-27 15:19:31 +11:00
|
|
|
private const int MaxWaitMilliseconds = 30000; // save the cache after some time (ie no more than 30s of changes)
|
2015-02-08 16:25:30 +01:00
|
|
|
|
|
|
|
|
// save the cache when the app goes down
|
2015-07-08 17:41:20 +02:00
|
|
|
public override bool RunsOnShutdown { get { return true; } }
|
2015-02-08 16:25:30 +01:00
|
|
|
|
2015-04-13 19:11:12 +02:00
|
|
|
// initialize the first instance, which is inactive (not touched yet)
|
|
|
|
|
public XmlCacheFilePersister(IBackgroundTaskRunner<XmlCacheFilePersister> runner, content content)
|
|
|
|
|
: this(runner, content, false)
|
|
|
|
|
{ }
|
|
|
|
|
|
|
|
|
|
private XmlCacheFilePersister(IBackgroundTaskRunner<XmlCacheFilePersister> runner, content content, bool touched)
|
2015-01-29 12:45:44 +11:00
|
|
|
{
|
2015-02-08 16:25:30 +01:00
|
|
|
_runner = runner;
|
|
|
|
|
_content = content;
|
|
|
|
|
|
2015-04-13 19:11:12 +02:00
|
|
|
if (runner.TryAdd(this) == false)
|
|
|
|
|
{
|
|
|
|
|
_runner = null; // runner's down
|
|
|
|
|
_released = true; // don't mess with timer
|
|
|
|
|
return;
|
|
|
|
|
}
|
2015-02-08 16:25:30 +01:00
|
|
|
|
2015-04-13 19:11:12 +02:00
|
|
|
// runner could decide to run it anytime now
|
2015-02-08 16:25:30 +01:00
|
|
|
|
2015-04-13 19:11:12 +02:00
|
|
|
if (touched == false) return;
|
2015-02-08 16:25:30 +01:00
|
|
|
|
2015-04-13 19:11:12 +02:00
|
|
|
LogHelper.Debug<XmlCacheFilePersister>("Created, save in {0}ms.", () => WaitMilliseconds);
|
|
|
|
|
_initialTouch = DateTime.Now;
|
|
|
|
|
_timer = new Timer(_ => TimerRelease());
|
2015-02-08 16:25:30 +01:00
|
|
|
_timer.Change(WaitMilliseconds, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public XmlCacheFilePersister Touch()
|
|
|
|
|
{
|
2015-04-13 19:11:12 +02:00
|
|
|
// if _released is false then we're going to setup a timer
|
|
|
|
|
// then the runner wants to shutdown & run immediately
|
|
|
|
|
// this sets _released to true & the timer will trigger eventualy & who cares?
|
|
|
|
|
// if _released is true, either it's a normal release, or
|
|
|
|
|
// a runner shutdown, in which case we won't be able to
|
|
|
|
|
// add a new task, and so we'll run immediately
|
|
|
|
|
|
|
|
|
|
var ret = this;
|
|
|
|
|
var runNow = false;
|
|
|
|
|
|
2015-02-08 16:25:30 +01:00
|
|
|
lock (_locko)
|
|
|
|
|
{
|
2015-04-13 19:11:12 +02:00
|
|
|
if (_released) // our timer has triggered OR the runner is shutting down
|
2015-02-08 16:25:30 +01:00
|
|
|
{
|
2015-04-13 19:11:12 +02:00
|
|
|
LogHelper.Debug<XmlCacheFilePersister>("Touched, was released...");
|
2015-02-08 16:25:30 +01:00
|
|
|
|
2015-04-13 19:11:12 +02:00
|
|
|
// release: has run or is running, too late, return a new task (adds itself to runner)
|
|
|
|
|
if (_runner == null)
|
|
|
|
|
{
|
|
|
|
|
LogHelper.Debug<XmlCacheFilePersister>("Runner is down, run now.");
|
|
|
|
|
runNow = true;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
LogHelper.Debug<XmlCacheFilePersister>("Create new...");
|
|
|
|
|
ret = new XmlCacheFilePersister(_runner, _content, true);
|
|
|
|
|
if (ret._runner == null)
|
|
|
|
|
{
|
|
|
|
|
// could not enlist with the runner, runner is completed, must run now
|
|
|
|
|
LogHelper.Debug<XmlCacheFilePersister>("Runner is down, run now.");
|
|
|
|
|
runNow = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2015-02-08 16:25:30 +01:00
|
|
|
}
|
|
|
|
|
|
2015-04-13 19:11:12 +02:00
|
|
|
else if (_timer == null) // we don't have a timer yet
|
2015-02-08 16:25:30 +01:00
|
|
|
{
|
2015-05-20 18:26:08 +02:00
|
|
|
LogHelper.Debug<XmlCacheFilePersister>("Touched, was idle, start and save in {0}ms.", () => WaitMilliseconds);
|
2015-02-08 16:25:30 +01:00
|
|
|
_initialTouch = DateTime.Now;
|
2015-04-13 19:11:12 +02:00
|
|
|
_timer = new Timer(_ => TimerRelease());
|
2015-02-08 16:25:30 +01:00
|
|
|
_timer.Change(WaitMilliseconds, 0);
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-13 19:11:12 +02:00
|
|
|
else // we have a timer
|
2015-02-08 16:25:30 +01:00
|
|
|
{
|
2015-04-13 19:11:12 +02:00
|
|
|
// change the timer to trigger in WaitMilliseconds unless we've been touched first more
|
|
|
|
|
// than MaxWaitMilliseconds ago and then leave the time unchanged
|
2015-02-08 16:25:30 +01:00
|
|
|
|
2015-04-13 19:11:12 +02:00
|
|
|
if (DateTime.Now - _initialTouch < TimeSpan.FromMilliseconds(MaxWaitMilliseconds))
|
|
|
|
|
{
|
|
|
|
|
LogHelper.Debug<XmlCacheFilePersister>("Touched, was waiting, can delay, save in {0}ms.", () => WaitMilliseconds);
|
|
|
|
|
_timer.Change(WaitMilliseconds, 0);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
LogHelper.Debug<XmlCacheFilePersister>("Touched, was waiting, cannot delay.");
|
|
|
|
|
}
|
|
|
|
|
}
|
2015-02-08 16:25:30 +01:00
|
|
|
}
|
2015-04-13 19:11:12 +02:00
|
|
|
|
|
|
|
|
if (runNow)
|
2015-05-19 20:08:52 +02:00
|
|
|
//Run();
|
|
|
|
|
LogHelper.Warn<XmlCacheFilePersister>("Cannot write now because we are going down, changes may be lost.");
|
2015-04-13 19:11:12 +02:00
|
|
|
|
|
|
|
|
return ret; // this, by default, unless we created a new one
|
2015-01-29 12:45:44 +11:00
|
|
|
}
|
|
|
|
|
|
2015-04-13 19:11:12 +02:00
|
|
|
private void TimerRelease()
|
2015-02-08 16:25:30 +01:00
|
|
|
{
|
|
|
|
|
lock (_locko)
|
|
|
|
|
{
|
2015-04-13 19:11:12 +02:00
|
|
|
LogHelper.Debug<XmlCacheFilePersister>("Timer: release.");
|
|
|
|
|
if (_timer != null)
|
|
|
|
|
_timer.Dispose();
|
|
|
|
|
_timer = null;
|
|
|
|
|
_released = true;
|
|
|
|
|
|
|
|
|
|
// if running (because of shutdown) this will have no effect
|
|
|
|
|
// else it tells the runner it is time to run the task
|
2015-07-08 17:41:20 +02:00
|
|
|
Release();
|
2015-02-08 16:25:30 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-08 17:41:20 +02:00
|
|
|
public override async Task RunAsync(CancellationToken token)
|
2015-01-29 12:45:44 +11:00
|
|
|
{
|
2015-04-13 19:11:12 +02:00
|
|
|
lock (_locko)
|
2015-01-29 12:45:44 +11:00
|
|
|
{
|
2015-04-13 19:11:12 +02:00
|
|
|
LogHelper.Debug<XmlCacheFilePersister>("Run now (async).");
|
|
|
|
|
// just make sure - in case the runner is running the task on shutdown
|
|
|
|
|
_released = true;
|
|
|
|
|
}
|
2015-01-29 12:45:44 +11:00
|
|
|
|
2015-04-13 19:11:12 +02:00
|
|
|
// http://stackoverflow.com/questions/13489065/best-practice-to-call-configureawait-for-all-server-side-code
|
|
|
|
|
// http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html
|
|
|
|
|
// do we really need that ConfigureAwait here?
|
2015-01-29 12:45:44 +11:00
|
|
|
|
2015-04-13 19:11:12 +02:00
|
|
|
using (await _runLock.LockAsync())
|
|
|
|
|
{
|
|
|
|
|
await _content.SaveXmlToFileAsync().ConfigureAwait(false);
|
2015-01-29 12:45:44 +11:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-07-08 17:41:20 +02:00
|
|
|
public override bool IsAsync
|
2015-01-29 12:45:44 +11:00
|
|
|
{
|
2015-04-13 19:11:12 +02:00
|
|
|
get { return true; }
|
2015-01-29 12:45:44 +11:00
|
|
|
}
|
|
|
|
|
|
2015-07-08 17:41:20 +02:00
|
|
|
public override void Run()
|
2015-01-29 12:45:44 +11:00
|
|
|
{
|
2015-04-13 19:11:12 +02:00
|
|
|
lock (_locko)
|
|
|
|
|
{
|
|
|
|
|
LogHelper.Debug<XmlCacheFilePersister>("Run now (sync).");
|
|
|
|
|
// not really needed but safer (it's only us invoking Run, but the method is public...)
|
|
|
|
|
_released = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
using (_runLock.Lock())
|
|
|
|
|
{
|
|
|
|
|
_content.SaveXmlToFile();
|
|
|
|
|
}
|
2015-01-29 12:45:44 +11:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|