diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs index 05ee4fcd14..870d65c4e1 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs @@ -37,7 +37,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache private const int MaxWaitMilliseconds = 30000; // save the cache after some time (ie no more than 30s of changes) // save the cache when the app goes down - public override bool RunsOnShutdown { get { return true; } } + public override bool RunsOnShutdown { get { return _timer != null; } } // initialize the first instance, which is inactive (not touched yet) public XmlCacheFilePersister(IBackgroundTaskRunner runner, content content) @@ -140,13 +140,8 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache lock (_locko) { LogHelper.Debug("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 Release(); } } @@ -189,5 +184,15 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache _content.SaveXmlToFile(); } } + + protected override void DisposeResources() + { + base.DisposeResources(); + + // stop the timer + if (_timer == null) return; + _timer.Change(Timeout.Infinite, Timeout.Infinite); + _timer.Dispose(); + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index b10f8987ef..0bb67bc517 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -1,31 +1,30 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web; -using System.Web.Hosting; using System.Xml; +using umbraco.BusinessLogic; +using umbraco.cms.businesslogic; +using umbraco.cms.businesslogic.web; +using umbraco.DataLayer; +using umbraco.presentation.nodeFactory; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; -using umbraco.BusinessLogic; -using umbraco.cms.businesslogic; -using umbraco.cms.businesslogic.web; using Umbraco.Core.Models; using Umbraco.Core.Profiling; -using umbraco.DataLayer; -using umbraco.presentation.nodeFactory; using Umbraco.Web; using Umbraco.Web.PublishedCache.XmlPublishedCache; using Umbraco.Web.Scheduling; -using Node = umbraco.NodeFactory.Node; using File = System.IO.File; +using Node = umbraco.NodeFactory.Node; +using Task = System.Threading.Tasks.Task; namespace umbraco { @@ -36,58 +35,44 @@ namespace umbraco { private XmlCacheFilePersister _persisterTask; + private volatile bool _released; + #region Constructors private content() { if (SyncToXmlFile) { - // if we write to file, prepare the lock - // (if we don't use the file, or just read from it, no need to lock) - InitializeFileLock(); - - // and prepare the persister task + // prepare the persister task // there's always be one task keeping a ref to the runner // so it's safe to just create it as a local var here var runner = new BackgroundTaskRunner("XmlCacheFilePersister", new BackgroundTaskRunnerOptions { LongRunning = true, - KeepAlive = true + KeepAlive = true, + Hosted = false // main domain will take care of stopping the runner (see below) }); - // when the runner is terminating we need to ensure that no modifications - // to content are possible anymore, as they would not be written out to - // the xml file - unfortunately that is not possible in 7.x because we - // cannot lock the content service... and so we do nothing... - //runner.Terminating += (sender, args) => - //{ - //}; - - // when the runner has terminated we know we will not be writing to the file - // anymore, so we can release the lock now - no need to wait for the AppDomain - // unload - which means any "last minute" saves will be lost - but waiting for - // the AppDomain to unload has issues... - runner.Terminated += (sender, args) => - { - if (_fileLock == null) return; // not locking (testing?) - if (_fileLocked == null) return; // not locked - - // thread-safety - // lock something that's readonly and not null.. - lock (_xmlFileName) - { - // double-check - if (_fileLocked == null) return; - - LogHelper.Debug("Release file lock."); - _fileLocked.Dispose(); - _fileLocked = null; - _fileLock = null; // ensure we don't lock again - } - }; - // create (and add to runner) _persisterTask = new XmlCacheFilePersister(runner, this); + + var registered = ApplicationContext.Current.MainDom.Register( + null, + () => + { + // once released, the cache still works but does not write to file anymore, + // which is OK with database server messenger but will cause data loss with + // another messenger... + + runner.Shutdown(false, true); // wait until flushed + _released = true; + }); + + // failed to become the main domain, we will never use the file + if (registered == false) + runner.Shutdown(false, true); + + _released = (registered == false); } // initialize content - populate the cache @@ -1004,100 +989,6 @@ order by umbracoNode.level, umbracoNode.sortOrder"; private readonly string _xmlFileName = IOHelper.MapPath(SystemFiles.ContentCacheXml); private DateTime _lastFileRead; // last time the file was read private DateTime _nextFileCheck; // last time we checked whether the file was changed - private AsyncLock _fileLock; // protects the file - private IDisposable _fileLocked; // protects the file - - private const int FileLockTimeoutMilliseconds = 4*60*1000; // 4' - - private void InitializeFileLock() - { - // initialize file lock - // ApplicationId will look like "/LM/W3SVC/1/Root/AppName" - // name is system-wide and must be less than 260 chars - // - // From MSDN C++ CreateSemaphore doc: - // "The name can have a "Global\" or "Local\" prefix to explicitly create the object in - // the global or session namespace. The remainder of the name can contain any character - // except the backslash character (\). For more information, see Kernel Object Namespaces." - // - // From MSDN "Kernel object namespaces" doc: - // "The separate client session namespaces enable multiple clients to run the same - // applications without interfering with each other. For processes started under - // a client session, the system uses the session namespace by default. However, these - // processes can use the global namespace by prepending the "Global\" prefix to the object name." - // - // just use "default" (whatever it is) for now - ie, no prefix - // - var name = HostingEnvironment.ApplicationID + "/XmlStore/XmlFile"; - _fileLock = new AsyncLock(name); - - // the file lock works with a shared, system-wide, semaphore - and we don't want - // to leak a count on that semaphore else the whole process will hang - so we have - // to ensure we dispose of the locker when the domain goes down - in theory the - // async lock should do it via its finalizer, but then there are some weird cases - // where the semaphore has been disposed of before it's been released, and then - // we'd need to GC-pin the semaphore... better dispose the locker explicitely - // when the app domain unloads. - - if (AppDomain.CurrentDomain.IsDefaultAppDomain()) - { - LogHelper.Debug("Registering Unload handler for default app domain."); - AppDomain.CurrentDomain.ProcessExit += OnDomainUnloadReleaseFileLock; - } - else - { - LogHelper.Debug("Registering Unload handler for non-default app domain."); - AppDomain.CurrentDomain.DomainUnload += OnDomainUnloadReleaseFileLock; - } - } - - private void EnsureFileLock() - { - if (_fileLock == null) return; // not locking (testing?) - if (_fileLocked != null) return; // locked already - - // thread-safety, acquire lock only once! - // lock something that's readonly and not null.. - lock (_xmlFileName) - { - // double-check - if (_fileLock == null) return; - if (_fileLocked != null) return; - - // don't hang forever, throws if it cannot lock within the timeout - LogHelper.Debug("Acquiring exclusive access to file for this AppDomain..."); - _fileLocked = _fileLock.Lock(FileLockTimeoutMilliseconds); - LogHelper.Debug("Acquired exclusive access to file for this AppDomain."); - } - } - - private void OnDomainUnloadReleaseFileLock(object sender, EventArgs args) - { - // the unload event triggers AFTER all hosted objects (eg the file persister - // background task runner) have been stopped, so we should have released the - // lock already - this is for safety - might be possible to get rid of it - - // NOTE - // trying to write to the log via LogHelper at that point is a BAD idea - // it can lead to ugly deadlocks with the named semaphore - DONT do it - - if (_fileLock == null) return; // not locking (testing?) - if (_fileLocked == null) return; // not locked - - // thread-safety - // lock something that's readonly and not null.. - lock (_xmlFileName) - { - // double-check - if (_fileLocked == null) return; - - // in case you really need to debug... that should be safe... - //System.IO.File.AppendAllText(HostingEnvironment.MapPath("~/App_Data/log.txt"), string.Format("{0} {1} unlock", DateTime.Now, AppDomain.CurrentDomain.Id)); - _fileLocked.Dispose(); - - _fileLock = null; // ensure we don't lock again - } - } // not used - just try to read the file //private bool XmlFileExists @@ -1119,7 +1010,9 @@ order by umbracoNode.level, umbracoNode.sortOrder"; } } - // assumes file lock + // invoked by XmlCacheFilePersister ONLY and that one manages the MainDom, ie it + // will NOT try to save once the current app domain is not the main domain anymore + // (no need to test _released) internal void SaveXmlToFile() { LogHelper.Info("Save Xml to file..."); @@ -1129,8 +1022,6 @@ order by umbracoNode.level, umbracoNode.sortOrder"; var xml = _xmlContent; // capture (atomic + volatile), immutable anyway if (xml == null) return; - EnsureFileLock(); - // delete existing file, if any DeleteXmlFile(); @@ -1138,7 +1029,7 @@ order by umbracoNode.level, umbracoNode.sortOrder"; var directoryName = Path.GetDirectoryName(_xmlFileName); if (directoryName == null) throw new Exception(string.Format("Invalid XmlFileName \"{0}\".", _xmlFileName)); - if (System.IO.File.Exists(_xmlFileName) == false && Directory.Exists(directoryName) == false) + if (File.Exists(_xmlFileName) == false && Directory.Exists(directoryName) == false) Directory.CreateDirectory(directoryName); // save @@ -1159,8 +1050,10 @@ order by umbracoNode.level, umbracoNode.sortOrder"; } } - // assumes file lock - internal async System.Threading.Tasks.Task SaveXmlToFileAsync() + // invoked by XmlCacheFilePersister ONLY and that one manages the MainDom, ie it + // will NOT try to save once the current app domain is not the main domain anymore + // (no need to test _released) + internal async Task SaveXmlToFileAsync() { LogHelper.Info("Save Xml to file..."); @@ -1169,8 +1062,6 @@ order by umbracoNode.level, umbracoNode.sortOrder"; var xml = _xmlContent; // capture (atomic + volatile), immutable anyway if (xml == null) return; - EnsureFileLock(); - // delete existing file, if any DeleteXmlFile(); @@ -1178,7 +1069,7 @@ order by umbracoNode.level, umbracoNode.sortOrder"; var directoryName = Path.GetDirectoryName(_xmlFileName); if (directoryName == null) throw new Exception(string.Format("Invalid XmlFileName \"{0}\".", _xmlFileName)); - if (System.IO.File.Exists(_xmlFileName) == false && Directory.Exists(directoryName) == false) + if (File.Exists(_xmlFileName) == false && Directory.Exists(directoryName) == false) Directory.CreateDirectory(directoryName); // save @@ -1227,17 +1118,15 @@ order by umbracoNode.level, umbracoNode.sortOrder"; return sb.ToString(); } - // assumes file lock private XmlDocument LoadXmlFromFile() { + // do NOT try to load if we are not the main domain anymore + if (_released) return null; + LogHelper.Info("Load Xml from file..."); try { - // if we're not writing back to the file, no need to lock - if (SyncToXmlFile) - EnsureFileLock(); - var xml = new XmlDocument(); using (var fs = new FileStream(_xmlFileName, FileMode.Open, FileAccess.Read, FileShare.Read)) { @@ -1260,12 +1149,11 @@ order by umbracoNode.level, umbracoNode.sortOrder"; } } - // (files is always locked) private void DeleteXmlFile() { - if (System.IO.File.Exists(_xmlFileName) == false) return; - System.IO.File.SetAttributes(_xmlFileName, FileAttributes.Normal); - System.IO.File.Delete(_xmlFileName); + if (File.Exists(_xmlFileName) == false) return; + File.SetAttributes(_xmlFileName, FileAttributes.Normal); + File.Delete(_xmlFileName); } private void ReloadXmlFromFileIfChanged()