NuCache+Scope - add scope lock to SnapDictionary

This commit is contained in:
Stephan
2017-07-12 20:40:09 +02:00
parent ab42f8d6e0
commit b0303ca753
5 changed files with 385 additions and 70 deletions

View File

@@ -0,0 +1,40 @@
using System;
namespace Umbraco.Core.Scoping
{
// base class for an object that will be enlisted in scope context, if any. it
// must be used in a 'using' block, and if not scoped, released when disposed,
// else when scope context runs enlisted actions
public abstract class ScopeContextualBase : IDisposable
{
private bool _using, _scoped;
public static T Get<T>(IScopeProvider scopeProvider, string key, Func<T> ctor)
where T : ScopeContextualBase
{
var scopeContext = scopeProvider.Context;
if (scopeContext == null)
return ctor();
var w = scopeContext.Enlist("ScopeContextualBase_" + key,
ctor,
(completed, item) => { item.Release(completed); });
if (w._using) throw new InvalidOperationException("panic: used.");
w._using = true;
w._scoped = true;
return w;
}
public void Dispose()
{
_using = false;
if (_scoped == false)
Release(true);
}
public abstract void Release(bool completed);
}
}

View File

@@ -1256,6 +1256,7 @@
<Compile Include="Scoping\RepositoryCacheMode.cs" />
<Compile Include="Scoping\Scope.cs" />
<Compile Include="Scoping\ScopeContext.cs" />
<Compile Include="Scoping\ScopeContextualBase.cs" />
<Compile Include="Scoping\ScopeProvider.cs" />
<Compile Include="Scoping\ScopeReference.cs" />
<Compile Include="Security\ActiveDirectoryBackOfficeUserPasswordChecker.cs" />

View File

@@ -1,7 +1,9 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Moq;
using NUnit.Framework;
using Umbraco.Core.Scoping;
using Umbraco.Web.PublishedCache.NuCache;
namespace Umbraco.Tests.Cache
@@ -709,6 +711,223 @@ namespace Umbraco.Tests.Cache
Assert.AreEqual("ein", s4.Get(1));
}
[Test]
public void NestedWriteLocking()
{
var d = new SnapDictionary<int, string>();
d.Test.CollectAuto = false;
d.WriteLocked(() =>
{
d.WriteLocked(() =>
{
d.Set(1, "one");
});
});
}
[Test]
public void WriteLocking2()
{
var d = new SnapDictionary<int, string>();
d.Test.CollectAuto = false;
// gen 1
d.Set(1, "one");
Assert.AreEqual(1, d.Test.GetValues(1).Length);
Assert.AreEqual(1, d.Test.LiveGen);
Assert.IsTrue(d.Test.NextGen);
var s1 = d.CreateSnapshot();
Assert.AreEqual(1, s1.Gen);
Assert.AreEqual(1, d.Test.LiveGen);
Assert.IsFalse(d.Test.NextGen);
Assert.AreEqual("one", s1.Get(1));
// gen 2
Assert.AreEqual(1, d.Test.GetValues(1).Length);
d.Set(1, "uno");
Assert.AreEqual(2, d.Test.GetValues(1).Length);
Assert.AreEqual(2, d.Test.LiveGen);
Assert.IsTrue(d.Test.NextGen);
var s2 = d.CreateSnapshot();
Assert.AreEqual(2, s2.Gen);
Assert.AreEqual(2, d.Test.LiveGen);
Assert.IsFalse(d.Test.NextGen);
Assert.AreEqual("uno", s2.Get(1));
var scopeProviderMock = new Mock<IScopeProvider>();
scopeProviderMock.Setup(x => x.Context).Returns<ScopeContext>(null);
var scopeProvider = scopeProviderMock.Object;
using (d.GetWriter(scopeProvider))
{
// gen 3
Assert.AreEqual(2, d.Test.GetValues(1).Length);
d.Set(1, "ein");
Assert.AreEqual(3, d.Test.GetValues(1).Length);
Assert.AreEqual(3, d.Test.LiveGen);
Assert.IsTrue(d.Test.NextGen);
var s3 = d.CreateSnapshot();
Assert.AreEqual(2, s3.Gen);
Assert.AreEqual(3, d.Test.LiveGen);
Assert.IsTrue(d.Test.NextGen); // has NOT changed when (non) creating snapshot
Assert.AreEqual("uno", s3.Get(1));
}
var s4 = d.CreateSnapshot();
Assert.AreEqual(3, s4.Gen);
Assert.AreEqual(3, d.Test.LiveGen);
Assert.IsFalse(d.Test.NextGen);
Assert.AreEqual("ein", s4.Get(1));
}
[Test]
public void WriteLocking3()
{
var d = new SnapDictionary<int, string>();
d.Test.CollectAuto = false;
// gen 1
d.Set(1, "one");
var s1 = d.CreateSnapshot();
Assert.AreEqual(1, s1.Gen);
Assert.AreEqual("one", s1.Get(1));
d.Set(1, "uno");
var s2 = d.CreateSnapshot();
Assert.AreEqual(2, s2.Gen);
Assert.AreEqual("uno", s2.Get(1));
var scopeProviderMock = new Mock<IScopeProvider>();
scopeProviderMock.Setup(x => x.Context).Returns<ScopeContext>(null);
var scopeProvider = scopeProviderMock.Object;
using (d.GetWriter(scopeProvider))
{
// creating a snapshot in a write-lock does NOT return the "current" content
// it uses the previous snapshot, so new snapshot created only on release
d.Set(1, "ein");
var s3 = d.CreateSnapshot();
Assert.AreEqual(2, s3.Gen);
Assert.AreEqual("uno", s3.Get(1));
// but live snapshot contains changes
var ls = d.Test.LiveSnapshot;
Assert.AreEqual("ein", ls.Get(1));
Assert.AreEqual(3, ls.Gen);
}
var s4 = d.CreateSnapshot();
Assert.AreEqual(3, s4.Gen);
Assert.AreEqual("ein", s4.Get(1));
}
[Test]
public void ScopeLocking1()
{
var d = new SnapDictionary<int, string>();
d.Test.CollectAuto = false;
// gen 1
d.Set(1, "one");
var s1 = d.CreateSnapshot();
Assert.AreEqual(1, s1.Gen);
Assert.AreEqual("one", s1.Get(1));
d.Set(1, "uno");
var s2 = d.CreateSnapshot();
Assert.AreEqual(2, s2.Gen);
Assert.AreEqual("uno", s2.Get(1));
var scopeProviderMock = new Mock<IScopeProvider>();
var scopeContext = new ScopeContext();
scopeProviderMock.Setup(x => x.Context).Returns(scopeContext);
var scopeProvider = scopeProviderMock.Object;
using (d.GetWriter(scopeProvider))
{
// creating a snapshot in a write-lock does NOT return the "current" content
// it uses the previous snapshot, so new snapshot created only on release
d.Set(1, "ein");
var s3 = d.CreateSnapshot();
Assert.AreEqual(2, s3.Gen);
Assert.AreEqual("uno", s3.Get(1));
// but live snapshot contains changes
var ls = d.Test.LiveSnapshot;
Assert.AreEqual("ein", ls.Get(1));
Assert.AreEqual(3, ls.Gen);
}
var s4 = d.CreateSnapshot();
Assert.AreEqual(2, s4.Gen);
Assert.AreEqual("uno", s4.Get(1));
scopeContext.ScopeExit(true);
var s5 = d.CreateSnapshot();
Assert.AreEqual(3, s5.Gen);
Assert.AreEqual("ein", s5.Get(1));
}
[Test]
public void ScopeLocking2()
{
var d = new SnapDictionary<int, string>();
d.Test.CollectAuto = false;
// gen 1
d.Set(1, "one");
var s1 = d.CreateSnapshot();
Assert.AreEqual(1, s1.Gen);
Assert.AreEqual("one", s1.Get(1));
d.Set(1, "uno");
var s2 = d.CreateSnapshot();
Assert.AreEqual(2, s2.Gen);
Assert.AreEqual("uno", s2.Get(1));
var scopeProviderMock = new Mock<IScopeProvider>();
var scopeContext = new ScopeContext();
scopeProviderMock.Setup(x => x.Context).Returns(scopeContext);
var scopeProvider = scopeProviderMock.Object;
using (d.GetWriter(scopeProvider))
{
// creating a snapshot in a write-lock does NOT return the "current" content
// it uses the previous snapshot, so new snapshot created only on release
d.Set(1, "ein");
var s3 = d.CreateSnapshot();
Assert.AreEqual(2, s3.Gen);
Assert.AreEqual("uno", s3.Get(1));
// but live snapshot contains changes
var ls = d.Test.LiveSnapshot;
Assert.AreEqual("ein", ls.Get(1));
Assert.AreEqual(3, ls.Gen);
}
var s4 = d.CreateSnapshot();
Assert.AreEqual(2, s4.Gen);
Assert.AreEqual("uno", s4.Get(1));
scopeContext.ScopeExit(false);
var s5 = d.CreateSnapshot();
Assert.AreEqual(2, s5.Gen);
Assert.AreEqual("uno", s5.Get(1));
}
[Test]
public void GetAll()
{

View File

@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Umbraco.Core.Scoping;
namespace Umbraco.Web.PublishedCache.NuCache
{
@@ -52,93 +53,141 @@ namespace Umbraco.Web.PublishedCache.NuCache
#region Locking
public void WriteLocked(Action action)
// Lock has a 'forceGen' parameter:
// used to start a set of changes that may not commit, to isolate the set from any pending
// changes that would not have been snapshotted yet, so they cannot be rolled back by accident
//
// Release has a 'commit' parameter:
// if false, the live gen is scrapped and changes that have been applied as part of the lock
// 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
private readonly string _instanceId = Guid.NewGuid().ToString("N");
// a scope contextual that represents a locked writer to the dictionary
private class SnapDictionaryWriter : ScopeContextualBase
{
var wtaken = false;
var wcount = false;
private SnapDictionary<TKey, TValue> _dictionary;
private WriteLock _wl;
public SnapDictionaryWriter(SnapDictionary<TKey, TValue> dictionary)
{
_dictionary = dictionary;
_wl = new WriteLock();
dictionary.Lock(_wl, true);
}
public override void Release(bool completed)
{
if (_wl == null) return;
_dictionary.Release(_wl, completed);
_wl = null;
_dictionary = null;
}
}
// gets a scope contextual representing a locked writer to the dictionary
public IDisposable GetWriter(IScopeProvider scopeProvider)
{
return ScopeContextualBase.Get(scopeProvider, _instanceId, () => new SnapDictionaryWriter(this));
}
private class WriteLock
{
public bool Taken;
public bool Count;
}
private void Lock(WriteLock wl, bool forceGen)
{
Monitor.Enter(_wlocko, ref wl.Taken);
var rtaken = false;
try
{
Monitor.Enter(_wlocko, ref wtaken);
Monitor.Enter(_rlocko, ref rtaken);
var rtaken = false;
// 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
{
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
{
_wlocked++;
wcount = true;
if (_nextGen == false)
{
// because we are changing things, a new generation
// is created, which will trigger a new snapshot
_nextGen = true;
_liveGen += 1;
}
}
}
{ }
finally
{
if (rtaken) Monitor.Exit(_rlocko);
_wlocked++;
wl.Count = true;
if (_nextGen == false || (forceGen && _wlocked == 1)) // if true already... ok to have "holes" in generation objects
{
// because we are changing things, a new generation
// is created, which will trigger a new snapshot
_nextGen = true;
_liveGen += 1;
}
}
}
finally
{
if (rtaken) Monitor.Exit(_rlocko);
}
}
private void Release(WriteLock wl, bool commit = true)
{
if (commit == false)
{
_nextGen = false;
_liveGen -= 1;
foreach (var item in _items)
{
var link = item.Value;
if (link.Gen <= _liveGen) continue;
var key = item.Key;
if (link.Next == null)
_items.TryRemove(key, out link);
else
_items.TryUpdate(key, link.Next, link);
}
}
if (wl.Count) _wlocked--;
if (wl.Taken) Monitor.Exit(_wlocko);
}
public void WriteLocked(Action action)
{
var wl = new WriteLock();
try
{
// lock (again) - don't force a next gen if there's already one
Lock(wl, false);
action();
}
finally
{
if (wcount) _wlocked--;
if (wtaken) Monitor.Exit(_wlocko);
Release(wl);
}
}
public T WriteLocked<T>(Func<T> func)
{
var wtaken = false;
var wcount = false;
var wl = new WriteLock();
try
{
Monitor.Enter(_wlocko, ref wtaken);
var rtaken = false;
try
{
Monitor.Enter(_rlocko, ref rtaken);
try
{ }
finally
{
_wlocked++;
wcount = true;
if (_nextGen == false)
{
// because we are changing things, a new generation
// is created, which will trigger a new snapshot
_nextGen = true;
_liveGen += 1;
}
}
}
finally
{
if (rtaken) Monitor.Exit(_rlocko);
}
// lock (again) - don't force a next gen if there's already one
Lock(wl, false);
return func();
}
finally
{
if (wcount) _wlocked--;
if (wtaken) Monitor.Exit(_wlocko);
Release(wl);
}
}
@@ -168,8 +217,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
private LinkedNode GetHead(TKey key)
{
LinkedNode link;
_items.TryGetValue(key, out link); // else null
_items.TryGetValue(key, out LinkedNode link); // else null
return link;
}
@@ -364,8 +412,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
private void Collect()
{
// see notes in CreateSnapshot
GenerationObject generationObject;
while (_generationObjects.TryPeek(out generationObject) && (generationObject.Count == 0 || generationObject.WeakReference.IsAlive == false))
while (_generationObjects.TryPeek(out GenerationObject generationObject) && (generationObject.Count == 0 || generationObject.WeakReference.IsAlive == false))
{
_generationObjects.TryDequeue(out generationObject); // cannot fail since TryPeek has succeeded
_floorGen = generationObject.Gen;
@@ -471,10 +518,11 @@ namespace Umbraco.Web.PublishedCache.NuCache
public ConcurrentQueue<GenerationObject> GenerationObjects => _dict._generationObjects;
public Snapshot LiveSnapshot => new Snapshot(_dict, _dict._liveGen);
public GenVal[] GetValues(TKey key)
{
LinkedNode link;
_dict._items.TryGetValue(key, out link); // else null
_dict._items.TryGetValue(key, out LinkedNode link); // else null
if (link == null)
return new GenVal[0];
@@ -538,6 +586,12 @@ namespace Umbraco.Web.PublishedCache.NuCache
_generationReference.GenerationObject.Reference();
}
internal Snapshot(SnapDictionary<TKey, TValue> store, long gen)
{
_store = store;
_gen = gen;
}
public TValue Get(TKey key)
{
if (_gen < 0)
@@ -576,7 +630,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
{
if (_gen < 0) return;
_gen = -1;
_generationReference.GenerationObject.Release();
_generationReference?.GenerationObject.Release();
GC.SuppressFinalize(this);
}
}

View File

@@ -5,6 +5,7 @@ using Umbraco.Core.Scoping;
namespace Umbraco.Web.PublishedCache.XmlPublishedCache
{
// fixme should be a ScopeContextualBase
internal class SafeXmlReaderWriter : IDisposable
{
private readonly bool _scoped;