Merge pull request #7306 from umbraco/v8/bugfix/AB3323-SqlMainDom
Introduce a new IMainDomLock and both default and sql implementations
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using CSharpTest.Net.Collections;
|
||||
using Newtonsoft.Json;
|
||||
using Umbraco.Core;
|
||||
@@ -144,7 +145,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
_domainStore = new SnapDictionary<int, Domain>();
|
||||
|
||||
LoadCachesOnStartup();
|
||||
LoadCachesOnStartup();
|
||||
}
|
||||
|
||||
Guid GetUid(ContentStore store, int id) => store.LiveSnapshot.Get(id)?.Uid ?? default;
|
||||
@@ -165,14 +166,14 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
/// to not run if MainDom wasn't acquired.
|
||||
/// If MainDom was not acquired, then _localContentDb and _localMediaDb will remain null which means this appdomain
|
||||
/// will load in published content via the DB and in that case this appdomain will probably not exist long enough to
|
||||
/// serve more than a page of content.
|
||||
/// serve more than a page of content.
|
||||
/// </remarks>
|
||||
private void MainDomRegister()
|
||||
{
|
||||
var path = GetLocalFilesPath();
|
||||
var localContentDbPath = Path.Combine(path, "NuCache.Content.db");
|
||||
var localMediaDbPath = Path.Combine(path, "NuCache.Media.db");
|
||||
|
||||
|
||||
_localContentDbExists = File.Exists(localContentDbPath);
|
||||
_localMediaDbExists = File.Exists(localMediaDbPath);
|
||||
|
||||
@@ -191,10 +192,15 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
/// </remarks>
|
||||
private void MainDomRelease()
|
||||
{
|
||||
_logger.Debug<PublishedSnapshotService>("Releasing from MainDom...");
|
||||
|
||||
lock (_storesLock)
|
||||
{
|
||||
_logger.Debug<PublishedSnapshotService>("Releasing content store...");
|
||||
_contentStore?.ReleaseLocalDb(); //null check because we could shut down before being assigned
|
||||
_localContentDb = null;
|
||||
|
||||
_logger.Debug<PublishedSnapshotService>("Releasing media store...");
|
||||
_mediaStore?.ReleaseLocalDb(); //null check because we could shut down before being assigned
|
||||
_localMediaDb = null;
|
||||
|
||||
@@ -217,7 +223,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
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)
|
||||
@@ -378,7 +384,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
var contentTypes = _serviceContext.ContentTypeService.GetAll()
|
||||
.Select(x => _publishedContentTypeFactory.CreateContentType(x));
|
||||
|
||||
_contentStore.SetAllContentTypes(contentTypes);
|
||||
_contentStore.SetAllContentTypesLocked(contentTypes);
|
||||
|
||||
using (_logger.TraceDuration<PublishedSnapshotService>("Loading content from database"))
|
||||
{
|
||||
@@ -389,7 +395,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
// IMPORTANT GetAllContentSources sorts kits by level + parentId + sortOrder
|
||||
var kits = _dataSource.GetAllContentSources(scope);
|
||||
return onStartup ? _contentStore.SetAllFastSorted(kits, true) : _contentStore.SetAll(kits);
|
||||
return onStartup ? _contentStore.SetAllFastSortedLocked(kits, true) : _contentStore.SetAllLocked(kits);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,7 +403,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
var contentTypes = _serviceContext.ContentTypeService.GetAll()
|
||||
.Select(x => _publishedContentTypeFactory.CreateContentType(x));
|
||||
_contentStore.SetAllContentTypes(contentTypes);
|
||||
_contentStore.SetAllContentTypesLocked(contentTypes);
|
||||
|
||||
using (_logger.TraceDuration<PublishedSnapshotService>("Loading content from local cache file"))
|
||||
{
|
||||
@@ -449,7 +455,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
var mediaTypes = _serviceContext.MediaTypeService.GetAll()
|
||||
.Select(x => _publishedContentTypeFactory.CreateContentType(x));
|
||||
_mediaStore.SetAllContentTypes(mediaTypes);
|
||||
_mediaStore.SetAllContentTypesLocked(mediaTypes);
|
||||
|
||||
using (_logger.TraceDuration<PublishedSnapshotService>("Loading media from database"))
|
||||
{
|
||||
@@ -461,7 +467,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
_logger.Debug<PublishedSnapshotService>("Loading media from database...");
|
||||
// IMPORTANT GetAllMediaSources sorts kits by level + parentId + sortOrder
|
||||
var kits = _dataSource.GetAllMediaSources(scope);
|
||||
return onStartup ? _mediaStore.SetAllFastSorted(kits, true) : _mediaStore.SetAll(kits);
|
||||
return onStartup ? _mediaStore.SetAllFastSortedLocked(kits, true) : _mediaStore.SetAllLocked(kits);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,7 +475,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
var mediaTypes = _serviceContext.MediaTypeService.GetAll()
|
||||
.Select(x => _publishedContentTypeFactory.CreateContentType(x));
|
||||
_mediaStore.SetAllContentTypes(mediaTypes);
|
||||
_mediaStore.SetAllContentTypesLocked(mediaTypes);
|
||||
|
||||
using (_logger.TraceDuration<PublishedSnapshotService>("Loading media from local cache file"))
|
||||
{
|
||||
@@ -510,7 +516,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
return false;
|
||||
}
|
||||
|
||||
return onStartup ? store.SetAllFastSorted(kits, false) : store.SetAll(kits);
|
||||
return onStartup ? store.SetAllFastSortedLocked(kits, false) : store.SetAllLocked(kits);
|
||||
}
|
||||
|
||||
// keep these around - might be useful
|
||||
@@ -616,7 +622,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
.Where(x => x.RootContentId.HasValue && x.LanguageIsoCode.IsNullOrWhiteSpace() == false)
|
||||
.Select(x => new Domain(x.Id, x.DomainName, x.RootContentId.Value, CultureInfo.GetCultureInfo(x.LanguageIsoCode), x.IsWildcard)))
|
||||
{
|
||||
_domainStore.Set(domain.Id, domain);
|
||||
_domainStore.SetLocked(domain.Id, domain);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -664,10 +670,12 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
publishedChanged = publishedChanged2;
|
||||
}
|
||||
|
||||
|
||||
if (draftChanged || publishedChanged)
|
||||
((PublishedSnapshot)CurrentPublishedSnapshot)?.Resync();
|
||||
}
|
||||
|
||||
// Calling this method means we have a lock on the contentStore (i.e. GetScopedWriteLock)
|
||||
private void NotifyLocked(IEnumerable<ContentCacheRefresher.JsonPayload> payloads, out bool draftChanged, out bool publishedChanged)
|
||||
{
|
||||
publishedChanged = false;
|
||||
@@ -677,7 +685,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
// content (and content types) are read-locked while reading content
|
||||
// contentStore is wlocked (so readable, only no new views)
|
||||
// and it can be wlocked by 1 thread only at a time
|
||||
// contentStore is write-locked during changes
|
||||
// contentStore is write-locked during changes - see note above, calls to this method are wrapped in contentStore.GetScopedWriteLock
|
||||
|
||||
foreach (var payload in payloads)
|
||||
{
|
||||
@@ -697,7 +705,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove))
|
||||
{
|
||||
if (_contentStore.Clear(payload.Id))
|
||||
if (_contentStore.ClearLocked(payload.Id))
|
||||
draftChanged = publishedChanged = true;
|
||||
continue;
|
||||
}
|
||||
@@ -720,7 +728,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
// ?? should we do some RV check here?
|
||||
// IMPORTANT GetbranchContentSources sorts kits by level and by sort order
|
||||
var kits = _dataSource.GetBranchContentSources(scope, capture.Id);
|
||||
_contentStore.SetBranch(capture.Id, kits);
|
||||
_contentStore.SetBranchLocked(capture.Id, kits);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -728,11 +736,11 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
var kit = _dataSource.GetContentSource(scope, capture.Id);
|
||||
if (kit.IsEmpty)
|
||||
{
|
||||
_contentStore.Clear(capture.Id);
|
||||
_contentStore.ClearLocked(capture.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
_contentStore.Set(kit);
|
||||
_contentStore.SetLocked(kit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -790,7 +798,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove))
|
||||
{
|
||||
if (_mediaStore.Clear(payload.Id))
|
||||
if (_mediaStore.ClearLocked(payload.Id))
|
||||
anythingChanged = true;
|
||||
continue;
|
||||
}
|
||||
@@ -813,7 +821,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
// ?? should we do some RV check here?
|
||||
// IMPORTANT GetbranchContentSources sorts kits by level and by sort order
|
||||
var kits = _dataSource.GetBranchMediaSources(scope, capture.Id);
|
||||
_mediaStore.SetBranch(capture.Id, kits);
|
||||
_mediaStore.SetBranchLocked(capture.Id, kits);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -821,11 +829,11 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
var kit = _dataSource.GetMediaSource(scope, capture.Id);
|
||||
if (kit.IsEmpty)
|
||||
{
|
||||
_mediaStore.Clear(capture.Id);
|
||||
_mediaStore.ClearLocked(capture.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
_mediaStore.Set(kit);
|
||||
_mediaStore.SetLocked(kit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -927,14 +935,14 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
using (var scope = _scopeProvider.CreateScope())
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.ContentTree);
|
||||
_contentStore.UpdateDataTypes(idsA, id => CreateContentType(PublishedItemType.Content, id));
|
||||
_contentStore.UpdateDataTypesLocked(idsA, id => CreateContentType(PublishedItemType.Content, id));
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
using (var scope = _scopeProvider.CreateScope())
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.MediaTree);
|
||||
_mediaStore.UpdateDataTypes(idsA, id => CreateContentType(PublishedItemType.Media, id));
|
||||
_mediaStore.UpdateDataTypesLocked(idsA, id => CreateContentType(PublishedItemType.Media, id));
|
||||
scope.Complete();
|
||||
}
|
||||
}
|
||||
@@ -964,7 +972,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
}
|
||||
break;
|
||||
case DomainChangeTypes.Remove:
|
||||
_domainStore.Clear(payload.Id);
|
||||
_domainStore.ClearLocked(payload.Id);
|
||||
break;
|
||||
case DomainChangeTypes.Refresh:
|
||||
var domain = _serviceContext.DomainService.GetById(payload.Id);
|
||||
@@ -972,14 +980,14 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
if (domain.RootContentId.HasValue == false) continue; // anomaly
|
||||
if (domain.LanguageIsoCode.IsNullOrWhiteSpace()) continue; // anomaly
|
||||
var culture = CultureInfo.GetCultureInfo(domain.LanguageIsoCode);
|
||||
_domainStore.Set(domain.Id, new Domain(domain.Id, domain.DomainName, domain.RootContentId.Value, culture, domain.IsWildcard));
|
||||
_domainStore.SetLocked(domain.Id, new Domain(domain.Id, domain.DomainName, domain.RootContentId.Value, culture, domain.IsWildcard));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Methods used to prevent allocations of lists
|
||||
//Methods used to prevent allocations of lists
|
||||
private void AddToList(ref List<int> list, int val) => GetOrCreateList(ref list).Add(val);
|
||||
private List<int> GetOrCreateList(ref List<int> list) => list ?? (list = new List<int>());
|
||||
|
||||
@@ -1057,11 +1065,11 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
? Array.Empty<ContentNodeKit>()
|
||||
: _dataSource.GetTypeContentSources(scope, refreshedIds).ToArray();
|
||||
|
||||
_contentStore.UpdateContentTypes(removedIds, typesA, kits);
|
||||
_contentStore.UpdateContentTypesLocked(removedIds, typesA, kits);
|
||||
if (!otherIds.IsCollectionEmpty())
|
||||
_contentStore.UpdateContentTypes(CreateContentTypes(PublishedItemType.Content, otherIds.ToArray()));
|
||||
_contentStore.UpdateContentTypesLocked(CreateContentTypes(PublishedItemType.Content, otherIds.ToArray()));
|
||||
if (!newIds.IsCollectionEmpty())
|
||||
_contentStore.NewContentTypes(CreateContentTypes(PublishedItemType.Content, newIds.ToArray()));
|
||||
_contentStore.NewContentTypesLocked(CreateContentTypes(PublishedItemType.Content, newIds.ToArray()));
|
||||
scope.Complete();
|
||||
}
|
||||
}
|
||||
@@ -1088,11 +1096,11 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
? Array.Empty<ContentNodeKit>()
|
||||
: _dataSource.GetTypeMediaSources(scope, refreshedIds).ToArray();
|
||||
|
||||
_mediaStore.UpdateContentTypes(removedIds, typesA, kits);
|
||||
_mediaStore.UpdateContentTypesLocked(removedIds, typesA, kits);
|
||||
if (!otherIds.IsCollectionEmpty())
|
||||
_mediaStore.UpdateContentTypes(CreateContentTypes(PublishedItemType.Media, otherIds.ToArray()).ToArray());
|
||||
_mediaStore.UpdateContentTypesLocked(CreateContentTypes(PublishedItemType.Media, otherIds.ToArray()).ToArray());
|
||||
if (!newIds.IsCollectionEmpty())
|
||||
_mediaStore.NewContentTypes(CreateContentTypes(PublishedItemType.Media, newIds.ToArray()).ToArray());
|
||||
_mediaStore.NewContentTypesLocked(CreateContentTypes(PublishedItemType.Media, newIds.ToArray()).ToArray());
|
||||
scope.Complete();
|
||||
}
|
||||
}
|
||||
@@ -1163,12 +1171,14 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
// elements
|
||||
// just need to make sure nothing gets elements in another enlisted action... so using
|
||||
// a MaxValue to make sure this one runs last, and it should be ok
|
||||
|
||||
scopeContext.Enlist("Umbraco.Web.PublishedCache.NuCache.PublishedSnapshotService.Resync", () => this, (completed, svc) =>
|
||||
{
|
||||
((PublishedSnapshot)svc.CurrentPublishedSnapshot)?.Resync();
|
||||
}, int.MaxValue);
|
||||
}
|
||||
|
||||
|
||||
// create a new snapshot cache if snapshots are different gens
|
||||
if (contentSnap.Gen != _contentGen || mediaSnap.Gen != _mediaGen || domainSnap.Gen != _domainGen || _elementsCache == null)
|
||||
{
|
||||
@@ -1335,7 +1345,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
//culture changed on an existing language
|
||||
var cultureChanged = e.SavedEntities.Any(x => !x.WasPropertyDirty(nameof(ILanguage.Id)) && x.WasPropertyDirty(nameof(ILanguage.IsoCode)));
|
||||
if(cultureChanged)
|
||||
if (cultureChanged)
|
||||
{
|
||||
RebuildContentDbCache();
|
||||
}
|
||||
|
||||
@@ -22,6 +22,11 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
// This class is optimized for many readers, few writers
|
||||
// Readers are lock-free
|
||||
|
||||
// NOTE - we used to lock _rlocko the long hand way with Monitor.Enter(_rlocko, ref lockTaken) but this has
|
||||
// been replaced with a normal c# lock because that's exactly how the normal c# lock works,
|
||||
// see https://blogs.msdn.microsoft.com/ericlippert/2009/03/06/locks-and-exceptions-do-not-mix/
|
||||
// for the readlock, there's no reason here to use the long hand way.
|
||||
|
||||
private readonly ConcurrentDictionary<TKey, LinkedNode<TValue>> _items;
|
||||
private readonly ConcurrentQueue<GenObj> _genObjs;
|
||||
private GenObj _genObj;
|
||||
@@ -30,7 +35,6 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
private long _liveGen, _floorGen;
|
||||
private bool _nextGen, _collectAuto;
|
||||
private Task _collectTask;
|
||||
private volatile int _wlocked;
|
||||
|
||||
// minGenDelta to be adjusted
|
||||
// we may want to throttle collects even if delta is reached
|
||||
@@ -71,20 +75,14 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
// are all ignored - Release is private and meant to be invoked with 'commit' being false only
|
||||
// only on the outermost lock (by SnapDictionaryWriter)
|
||||
|
||||
// using (...) {} for locking is prone to nasty leaks in case of weird exceptions
|
||||
// such as thread-abort or out-of-memory, but let's not worry about it now
|
||||
// side note - using (...) {} for locking is prone to nasty leaks in case of weird exceptions
|
||||
// such as thread-abort or out-of-memory, which is why we've moved away from the old using wrapper we had on locking.
|
||||
|
||||
private readonly string _instanceId = Guid.NewGuid().ToString("N");
|
||||
|
||||
private class ReadLockInfo
|
||||
{
|
||||
public bool Taken;
|
||||
}
|
||||
|
||||
private class WriteLockInfo
|
||||
{
|
||||
public bool Taken;
|
||||
public bool Count;
|
||||
}
|
||||
|
||||
// a scope contextual that represents a locked writer to the dictionary
|
||||
@@ -92,8 +90,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
private readonly WriteLockInfo _lockinfo = new WriteLockInfo();
|
||||
private readonly SnapDictionary<TKey, TValue> _dictionary;
|
||||
private int _released;
|
||||
|
||||
|
||||
public ScopedWriteLock(SnapDictionary<TKey, TValue> dictionary, bool scoped)
|
||||
{
|
||||
_dictionary = dictionary;
|
||||
@@ -102,8 +99,6 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
public override void Release(bool completed)
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref _released, 1, 0) != 0)
|
||||
return;
|
||||
_dictionary.Release(_lockinfo, completed);
|
||||
}
|
||||
}
|
||||
@@ -117,28 +112,31 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ScopedWriteLock(this, scoped));
|
||||
}
|
||||
|
||||
private void EnsureLocked()
|
||||
{
|
||||
if (!Monitor.IsEntered(_wlocko))
|
||||
throw new InvalidOperationException("Write lock must be acquried.");
|
||||
}
|
||||
|
||||
private void Lock(WriteLockInfo lockInfo, bool forceGen = false)
|
||||
{
|
||||
if (Monitor.IsEntered(_wlocko))
|
||||
throw new InvalidOperationException("Recursive locks not allowed");
|
||||
|
||||
Monitor.Enter(_wlocko, ref lockInfo.Taken);
|
||||
|
||||
var rtaken = false;
|
||||
try
|
||||
lock(_rlocko)
|
||||
{
|
||||
Monitor.Enter(_rlocko, ref rtaken);
|
||||
|
||||
// assume everything in finally runs atomically
|
||||
// http://stackoverflow.com/questions/18501678/can-this-unexpected-behavior-of-prepareconstrainedregions-and-thread-abort-be-ex
|
||||
// http://joeduffyblog.com/2005/03/18/atomicity-and-asynchronous-exception-failures/
|
||||
// http://joeduffyblog.com/2007/02/07/introducing-the-new-readerwriterlockslim-in-orcas/
|
||||
// http://chabster.blogspot.fr/2013/12/readerwriterlockslim-fails-on-dual.html
|
||||
//RuntimeHelpers.PrepareConstrainedRegions();
|
||||
try { } finally
|
||||
try { }
|
||||
finally
|
||||
{
|
||||
// increment the lock count, and register that this lock is counting
|
||||
_wlocked++;
|
||||
lockInfo.Count = true;
|
||||
|
||||
if (_nextGen == false || (forceGen && _wlocked == 1))
|
||||
if (_nextGen == false || (forceGen))
|
||||
{
|
||||
// because we are changing things, a new generation
|
||||
// is created, which will trigger a new snapshot
|
||||
@@ -149,15 +147,6 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rtaken) Monitor.Exit(_rlocko);
|
||||
}
|
||||
}
|
||||
|
||||
private void Lock(ReadLockInfo lockInfo)
|
||||
{
|
||||
Monitor.Enter(_rlocko, ref lockInfo.Taken);
|
||||
}
|
||||
|
||||
private void Release(WriteLockInfo lockInfo, bool commit = true)
|
||||
@@ -168,22 +157,17 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
if (commit == false)
|
||||
{
|
||||
var rtaken = false;
|
||||
try
|
||||
lock(_rlocko)
|
||||
{
|
||||
Monitor.Enter(_rlocko, ref rtaken);
|
||||
try { } finally
|
||||
try { }
|
||||
finally
|
||||
{
|
||||
// forget about the temp. liveGen
|
||||
_nextGen = false;
|
||||
_liveGen -= 1;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rtaken) Monitor.Exit(_rlocko);
|
||||
}
|
||||
|
||||
|
||||
foreach (var item in _items)
|
||||
{
|
||||
var link = item.Value;
|
||||
@@ -197,16 +181,10 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
}
|
||||
}
|
||||
|
||||
// decrement the lock count, if counting, then exit the lock
|
||||
if (lockInfo.Count) _wlocked--;
|
||||
// TODO: Shouldn't this be in a finally block?
|
||||
Monitor.Exit(_wlocko);
|
||||
}
|
||||
|
||||
private void Release(ReadLockInfo lockInfo)
|
||||
{
|
||||
if (lockInfo.Taken) Monitor.Exit(_rlocko);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Set, Clear, Get, Has
|
||||
@@ -219,75 +197,59 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
return link;
|
||||
}
|
||||
|
||||
public void Set(TKey key, TValue value)
|
||||
public void SetLocked(TKey key, TValue value)
|
||||
{
|
||||
var lockInfo = new WriteLockInfo();
|
||||
try
|
||||
{
|
||||
Lock(lockInfo);
|
||||
EnsureLocked();
|
||||
|
||||
// this is safe only because we're write-locked
|
||||
var link = GetHead(key);
|
||||
if (link != null)
|
||||
// this is safe only because we're write-locked
|
||||
var link = GetHead(key);
|
||||
if (link != null)
|
||||
{
|
||||
// already in the dict
|
||||
if (link.Gen != _liveGen)
|
||||
{
|
||||
// already in the dict
|
||||
if (link.Gen != _liveGen)
|
||||
{
|
||||
// for an older gen - if value is different then insert a new
|
||||
// link for the new gen, with the new value
|
||||
if (link.Value != value)
|
||||
_items.TryUpdate(key, new LinkedNode<TValue>(value, _liveGen, link), link);
|
||||
}
|
||||
else
|
||||
{
|
||||
// for the live gen - we can fix the live gen - and remove it
|
||||
// if value is null and there's no next gen
|
||||
if (value == null && link.Next == null)
|
||||
_items.TryRemove(key, out link);
|
||||
else
|
||||
link.Value = value;
|
||||
}
|
||||
// for an older gen - if value is different then insert a new
|
||||
// link for the new gen, with the new value
|
||||
if (link.Value != value)
|
||||
_items.TryUpdate(key, new LinkedNode<TValue>(value, _liveGen, link), link);
|
||||
}
|
||||
else
|
||||
{
|
||||
_items.TryAdd(key, new LinkedNode<TValue>(value, _liveGen));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Release(lockInfo);
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear(TKey key)
|
||||
{
|
||||
Set(key, null);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
var lockInfo = new WriteLockInfo();
|
||||
try
|
||||
{
|
||||
Lock(lockInfo);
|
||||
|
||||
// this is safe only because we're write-locked
|
||||
foreach (var kvp in _items.Where(x => x.Value != null))
|
||||
{
|
||||
if (kvp.Value.Gen < _liveGen)
|
||||
{
|
||||
var link = new LinkedNode<TValue>(null, _liveGen, kvp.Value);
|
||||
_items.TryUpdate(kvp.Key, link, kvp.Value);
|
||||
}
|
||||
// for the live gen - we can fix the live gen - and remove it
|
||||
// if value is null and there's no next gen
|
||||
if (value == null && link.Next == null)
|
||||
_items.TryRemove(key, out link);
|
||||
else
|
||||
{
|
||||
kvp.Value.Value = null;
|
||||
}
|
||||
link.Value = value;
|
||||
}
|
||||
}
|
||||
finally
|
||||
else
|
||||
{
|
||||
Release(lockInfo);
|
||||
_items.TryAdd(key, new LinkedNode<TValue>(value, _liveGen));
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearLocked(TKey key)
|
||||
{
|
||||
SetLocked(key, null);
|
||||
}
|
||||
|
||||
public void ClearLocked()
|
||||
{
|
||||
EnsureLocked();
|
||||
|
||||
// this is safe only because we're write-locked
|
||||
foreach (var kvp in _items.Where(x => x.Value != null))
|
||||
{
|
||||
if (kvp.Value.Gen < _liveGen)
|
||||
{
|
||||
var link = new LinkedNode<TValue>(null, _liveGen, kvp.Value);
|
||||
_items.TryUpdate(kvp.Key, link, kvp.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
kvp.Value.Value = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,11 +309,8 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
public Snapshot CreateSnapshot()
|
||||
{
|
||||
var lockInfo = new ReadLockInfo();
|
||||
try
|
||||
lock(_rlocko)
|
||||
{
|
||||
Lock(lockInfo);
|
||||
|
||||
// if no next generation is required, and we already have a gen object,
|
||||
// use it to create a new snapshot
|
||||
if (_nextGen == false && _genObj != null)
|
||||
@@ -360,7 +319,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
// else we need to try to create a new gen object
|
||||
// whether we are wlocked or not, noone can rlock while we do,
|
||||
// so _liveGen and _nextGen are safe
|
||||
if (_wlocked > 0) // volatile, cannot ++ but could --
|
||||
if (Monitor.IsEntered(_wlocko))
|
||||
{
|
||||
// write-locked, cannot use latest gen (at least 1) so use previous
|
||||
var snapGen = _nextGen ? _liveGen - 1 : _liveGen;
|
||||
@@ -398,10 +357,6 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Release(lockInfo);
|
||||
}
|
||||
}
|
||||
|
||||
public Task CollectAsync()
|
||||
@@ -496,17 +451,18 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
}
|
||||
}
|
||||
|
||||
public /*async*/ Task PendingCollect()
|
||||
{
|
||||
Task task;
|
||||
lock (_rlocko)
|
||||
{
|
||||
task = _collectTask;
|
||||
}
|
||||
return task ?? Task.CompletedTask;
|
||||
//if (task != null)
|
||||
// await task;
|
||||
}
|
||||
// TODO: This is never used? Should it be? Maybe move to TestHelper below?
|
||||
//public /*async*/ Task PendingCollect()
|
||||
//{
|
||||
// Task task;
|
||||
// lock (_rlocko)
|
||||
// {
|
||||
// task = _collectTask;
|
||||
// }
|
||||
// return task ?? Task.CompletedTask;
|
||||
// //if (task != null)
|
||||
// // await task;
|
||||
//}
|
||||
|
||||
public long GenCount => _genObjs.Count;
|
||||
|
||||
@@ -531,7 +487,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
public long LiveGen => _dict._liveGen;
|
||||
public long FloorGen => _dict._floorGen;
|
||||
public bool NextGen => _dict._nextGen;
|
||||
public int WLocked => _dict._wlocked;
|
||||
public bool IsLocked => Monitor.IsEntered(_dict._wlocko);
|
||||
|
||||
public bool CollectAuto
|
||||
{
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
NuCache Documentation
|
||||
======================
|
||||
|
||||
NOTE - RENAMING
|
||||
Facade = PublishedSnapshot
|
||||
and everything needs to be renamed accordingly
|
||||
|
||||
HOW IT WORKS
|
||||
-------------
|
||||
|
||||
@@ -22,12 +18,12 @@ When reading the cache, we read views up the chain until we find a value (which
|
||||
null) for the given id, and finally we read the store itself.
|
||||
|
||||
|
||||
The FacadeService manages a ContentStore for content, and another for media.
|
||||
When a Facade is created, the FacadeService gets ContentView objects from the stores.
|
||||
The PublishedSnapshotService manages a ContentStore for content, and another for media.
|
||||
When a PublishedSnapshot is created, the PublishedSnapshotService gets ContentView objects from the stores.
|
||||
Views provide an immutable snapshot of the content and media.
|
||||
|
||||
When the FacadeService is notified of changes, it notifies the stores.
|
||||
Then it resync the current Facade, so that it requires new views, etc.
|
||||
When the PublishedSnapshotService is notified of changes, it notifies the stores.
|
||||
Then it resync the current PublishedSnapshot, so that it requires new views, etc.
|
||||
|
||||
Whenever a content, media or member is modified or removed, the cmsContentNu table
|
||||
is updated with a json dictionary of alias => property value, so that a content,
|
||||
@@ -50,32 +46,32 @@ Each ContentStore has a _freezeLock object used to protect the 'Frozen'
|
||||
state of the store. It's a disposable object that releases the lock when disposed,
|
||||
so usage would be: using (store.Frozen) { ... }.
|
||||
|
||||
The FacadeService has a _storesLock object used to guarantee atomic access to the
|
||||
The PublishedSnapshotService has a _storesLock object used to guarantee atomic access to the
|
||||
set of content, media stores.
|
||||
|
||||
|
||||
CACHE
|
||||
------
|
||||
|
||||
For each set of views, the FacadeService creates a SnapshotCache. So a SnapshotCache
|
||||
For each set of views, the PublishedSnapshotService creates a SnapshotCache. So a SnapshotCache
|
||||
is valid until anything changes in the content or media trees. In other words, things
|
||||
that go in the SnapshotCache stay until a content or media is modified.
|
||||
|
||||
For each Facade, the FacadeService creates a FacadeCache. So a FacadeCache is valid
|
||||
for the duration of the Facade (usually, the request). In other words, things that go
|
||||
in the FacadeCache stay (and are visible to) for the duration of the request only.
|
||||
For each PublishedSnapshot, the PublishedSnapshotService creates a PublishedSnapshotCache. So a PublishedSnapshotCache is valid
|
||||
for the duration of the PublishedSnapshot (usually, the request). In other words, things that go
|
||||
in the PublishedSnapshotCache stay (and are visible to) for the duration of the request only.
|
||||
|
||||
The FacadeService defines a static constant FullCacheWhenPreviewing, that defines
|
||||
The PublishedSnapshotService defines a static constant FullCacheWhenPreviewing, that defines
|
||||
how caches operate when previewing:
|
||||
- when true, the caches in preview mode work normally.
|
||||
- when false, everything that would go to the SnapshotCache goes to the FacadeCache.
|
||||
- when false, everything that would go to the SnapshotCache goes to the PublishedSnapshotCache.
|
||||
At the moment it is true in the code, which means that eg converted values for
|
||||
previewed content will go in the SnapshotCache. Makes for faster preview, but uses
|
||||
more memory on the long term... would need some benchmarking to figure out what is
|
||||
best.
|
||||
|
||||
Members only live for the duration of the Facade. So, for members SnapshotCache is
|
||||
never used, and everything goes to the FacadeCache.
|
||||
Members only live for the duration of the PublishedSnapshot. So, for members SnapshotCache is
|
||||
never used, and everything goes to the PublishedSnapshotCache.
|
||||
|
||||
All cache keys are computed in the CacheKeys static class.
|
||||
|
||||
@@ -85,15 +81,15 @@ TESTS
|
||||
|
||||
For testing purposes the following mechanisms exist:
|
||||
|
||||
The Facade type has a static Current property that is used to obtain the 'current'
|
||||
facade in many places, going through the PublishedCachesServiceResolver to get the
|
||||
current service, and asking the current service for the current facade, which by
|
||||
The PublishedSnapshot type has a static Current property that is used to obtain the 'current'
|
||||
PublishedSnapshot in many places, going through the PublishedCachesServiceResolver to get the
|
||||
current service, and asking the current service for the current PublishedSnapshot, which by
|
||||
default relies on UmbracoContext. For test purposes, it is possible to override the
|
||||
entire mechanism by defining Facade.GetCurrentFacadeFunc which should return a facade.
|
||||
entire mechanism by defining PublishedSnapshot.GetCurrentPublishedSnapshotFunc which should return a PublishedSnapshot.
|
||||
|
||||
A PublishedContent keeps only id-references to its parent and children, and needs a
|
||||
way to retrieve the actual objects from the cache - which depends on the current
|
||||
facade. It is possible to override the entire mechanism by defining PublishedContent.
|
||||
PublishedSnapshot. It is possible to override the entire mechanism by defining PublishedContent.
|
||||
GetContentByIdFunc or .GetMediaByIdFunc.
|
||||
|
||||
Setting these functions must be done before Resolution is frozen.
|
||||
@@ -110,7 +106,7 @@ possible to support detached contents & properties, even those that do not have
|
||||
int id, but again this should be refactored entirely anyway.
|
||||
|
||||
Not doing any row-version checks (see XmlStore) when reloading from database, though it
|
||||
is maintained in the database. Two FIXME in FacadeService. Should we do it?
|
||||
is maintained in the database. Two FIXME in PublishedSnapshotService. Should we do it?
|
||||
|
||||
There is no on-disk cache at all so everything is reloaded from the cmsContentNu table
|
||||
when the site restarts. This is pretty fast, but we should experiment with solutions to
|
||||
@@ -121,4 +117,4 @@ PublishedMember exposes properties that IPublishedContent does not, and that are
|
||||
to be lost soon as the member is wrapped (content set, model...) - so we probably need
|
||||
some sort of IPublishedMember.
|
||||
|
||||
/
|
||||
/
|
||||
Reference in New Issue
Block a user