diff --git a/src/Umbraco.Core/Scoping/ScopeContextualBase.cs b/src/Umbraco.Core/Scoping/ScopeContextualBase.cs
index 7461142234..25f176d471 100644
--- a/src/Umbraco.Core/Scoping/ScopeContextualBase.cs
+++ b/src/Umbraco.Core/Scoping/ScopeContextualBase.cs
@@ -2,39 +2,61 @@
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
+ ///
+ /// Provides a base class for scope contextual objects.
+ ///
+ ///
+ /// A scope contextual object is enlisted in the current scope context,
+ /// if any, and released when the context exists. It must be used in a 'using'
+ /// block, and will be released when disposed, if not part of a scope.
+ ///
public abstract class ScopeContextualBase : IDisposable
{
- private bool _using, _scoped;
+ private bool _scoped;
+ ///
+ /// Gets a contextual object.
+ ///
+ /// The type of the object.
+ /// A scope provider.
+ /// A context key for the object.
+ /// A function producing the contextual object.
+ /// The contextual object.
+ ///
+ ///
+ ///
public static T Get(IScopeProvider scopeProvider, string key, Func ctor)
where T : ScopeContextualBase
{
+ // no scope context = create a non-scoped object
var scopeContext = scopeProvider.Context;
if (scopeContext == null)
return ctor(false);
+ // create & enlist the scoped object
var w = scopeContext.Enlist("ScopeContextualBase_" + key,
() => ctor(true),
(completed, item) => { item.Release(completed); });
- if (w._using) throw new InvalidOperationException("panic: used.");
- w._using = true;
w._scoped = true;
return w;
}
+ ///
+ ///
+ /// If not scoped, then this releases the contextual object.
+ ///
public void Dispose()
{
- _using = false;
-
if (_scoped == false)
Release(true);
}
+ ///
+ /// Releases the contextual object.
+ ///
+ /// A value indicating whether the scoped operation completed.
public abstract void Release(bool completed);
}
}
diff --git a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs
index 013dbadbb8..b435af9e77 100644
--- a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs
+++ b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs
@@ -5,6 +5,7 @@ using Moq;
using NUnit.Framework;
using Umbraco.Core.Scoping;
using Umbraco.Web.PublishedCache.NuCache;
+using Umbraco.Web.PublishedCache.NuCache.Snap;
namespace Umbraco.Tests.Cache
{
@@ -388,8 +389,7 @@ namespace Umbraco.Tests.Cache
// collect liveGen
GC.Collect();
- SnapDictionary.GenerationObject genObj;
- Assert.IsTrue(d.Test.GenerationObjects.TryPeek(out genObj));
+ Assert.IsTrue(d.Test.GenObjs.TryPeek(out var genObj));
genObj = null;
// in Release mode, it works, but in Debug mode, the weak reference is still alive
@@ -399,14 +399,14 @@ namespace Umbraco.Tests.Cache
GC.Collect();
#endif
- Assert.IsTrue(d.Test.GenerationObjects.TryPeek(out genObj));
- Assert.IsFalse(genObj.WeakReference.IsAlive); // snapshot is gone, along with its reference
+ Assert.IsTrue(d.Test.GenObjs.TryPeek(out genObj));
+ Assert.IsFalse(genObj.WeakGenRef.IsAlive); // snapshot is gone, along with its reference
await d.CollectAsync();
Assert.AreEqual(0, d.Test.GetValues(1).Length); // null value is gone
Assert.AreEqual(0, d.Count); // item is gone
- Assert.AreEqual(0, d.Test.GenerationObjects.Count);
+ Assert.AreEqual(0, d.Test.GenObjs.Count);
Assert.AreEqual(0, d.SnapCount); // snapshot is gone
Assert.AreEqual(0, d.GenCount); // and generation has been dequeued
}
@@ -632,7 +632,7 @@ namespace Umbraco.Tests.Cache
Assert.AreEqual(1, d.Test.LiveGen);
Assert.IsTrue(d.Test.NextGen);
- using (d.GetWriter(GetScopeProvider()))
+ using (d.GetScopedWriteLock(GetScopeProvider()))
{
var s1 = d.CreateSnapshot();
@@ -685,7 +685,7 @@ namespace Umbraco.Tests.Cache
Assert.IsFalse(d.Test.NextGen);
Assert.AreEqual("uno", s2.Get(1));
- using (d.GetWriter(GetScopeProvider()))
+ using (d.GetScopedWriteLock(GetScopeProvider()))
{
// gen 3
Assert.AreEqual(2, d.Test.GetValues(1).Length);
@@ -712,16 +712,102 @@ namespace Umbraco.Tests.Cache
}
[Test]
- public void NestedWriteLocking()
+ public void NestedWriteLocking1()
+ {
+ var d = new SnapDictionary();
+ var t = d.Test;
+ t.CollectAuto = false;
+
+ Assert.AreEqual(0, d.CreateSnapshot().Gen);
+
+ // no scope context: writers nest, last one to be disposed commits
+
+ var scopeProvider = GetScopeProvider();
+
+ using (var w1 = d.GetScopedWriteLock(scopeProvider))
+ {
+ Assert.AreEqual(1, t.LiveGen);
+ Assert.AreEqual(1, t.WLocked);
+ Assert.IsTrue(t.NextGen);
+
+ using (var w2 = d.GetScopedWriteLock(scopeProvider))
+ {
+ Assert.AreEqual(1, t.LiveGen);
+ Assert.AreEqual(2, t.WLocked);
+ Assert.IsTrue(t.NextGen);
+
+ Assert.AreNotSame(w1, w2); // get a new writer each time
+
+ d.Set(1, "one");
+
+ Assert.AreEqual(0, d.CreateSnapshot().Gen);
+ }
+
+ Assert.AreEqual(1, t.LiveGen);
+ Assert.AreEqual(1, t.WLocked);
+ Assert.IsTrue(t.NextGen);
+
+ Assert.AreEqual(0, d.CreateSnapshot().Gen);
+ }
+
+ Assert.AreEqual(1, t.LiveGen);
+ Assert.AreEqual(0, t.WLocked);
+ Assert.IsTrue(t.NextGen);
+
+ Assert.AreEqual(1, d.CreateSnapshot().Gen);
+ }
+
+ [Test]
+ public void NestedWriteLocking2()
{
var d = new SnapDictionary();
d.Test.CollectAuto = false;
- var scopeProvider = GetScopeProvider();
- using (d.GetWriter(scopeProvider))
+ Assert.AreEqual(0, d.CreateSnapshot().Gen);
+
+ // scope context: writers enlist
+
+ var scopeContext = new ScopeContext();
+ var scopeProvider = GetScopeProvider(scopeContext);
+
+ using (var w1 = d.GetScopedWriteLock(scopeProvider))
{
- using (d.GetWriter(scopeProvider))
+ using (var w2 = d.GetScopedWriteLock(scopeProvider))
{
+ Assert.AreSame(w1, w2);
+
+ d.Set(1, "one");
+ }
+ }
+ }
+
+ [Test]
+ public void NestedWriteLocking3()
+ {
+ var d = new SnapDictionary();
+ var t = d.Test;
+ t.CollectAuto = false;
+
+ Assert.AreEqual(0, d.CreateSnapshot().Gen);
+
+ var scopeContext = new ScopeContext();
+ var scopeProvider1 = GetScopeProvider();
+ var scopeProvider2 = GetScopeProvider(scopeContext);
+
+ using (var w1 = d.GetScopedWriteLock(scopeProvider1))
+ {
+ Assert.AreEqual(1, t.LiveGen);
+ Assert.AreEqual(1, t.WLocked);
+ Assert.IsTrue(t.NextGen);
+
+ using (var w2 = d.GetScopedWriteLock(scopeProvider2))
+ {
+ Assert.AreEqual(1, t.LiveGen);
+ Assert.AreEqual(2, t.WLocked);
+ Assert.IsTrue(t.NextGen);
+
+ Assert.AreNotSame(w1, w2);
+
d.Set(1, "one");
}
}
@@ -764,7 +850,7 @@ namespace Umbraco.Tests.Cache
var scopeProvider = GetScopeProvider();
- using (d.GetWriter(scopeProvider))
+ using (d.GetScopedWriteLock(scopeProvider))
{
// gen 3
Assert.AreEqual(2, d.Test.GetValues(1).Length);
@@ -809,7 +895,7 @@ namespace Umbraco.Tests.Cache
var scopeProvider = GetScopeProvider();
- using (d.GetWriter(scopeProvider))
+ using (d.GetScopedWriteLock(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
@@ -846,9 +932,10 @@ namespace Umbraco.Tests.Cache
Assert.AreEqual(2, s2.Gen);
Assert.AreEqual("uno", s2.Get(1));
- var scopeProvider = GetScopeProvider(true);
+ var scopeContext = new ScopeContext();
+ var scopeProvider = GetScopeProvider(scopeContext);
- using (d.GetWriter(scopeProvider))
+ using (d.GetScopedWriteLock(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
@@ -867,7 +954,7 @@ namespace Umbraco.Tests.Cache
Assert.AreEqual(2, s4.Gen);
Assert.AreEqual("uno", s4.Get(1));
- ((ScopeContext) scopeProvider.Context).ScopeExit(true);
+ scopeContext.ScopeExit(true);
var s5 = d.CreateSnapshot();
Assert.AreEqual(3, s5.Gen);
@@ -878,7 +965,8 @@ namespace Umbraco.Tests.Cache
public void ScopeLocking2()
{
var d = new SnapDictionary();
- d.Test.CollectAuto = false;
+ var t = d.Test;
+ t.CollectAuto = false;
// gen 1
d.Set(1, "one");
@@ -891,12 +979,13 @@ namespace Umbraco.Tests.Cache
Assert.AreEqual(2, s2.Gen);
Assert.AreEqual("uno", s2.Get(1));
- var scopeProviderMock = new Mock();
- var scopeContext = new ScopeContext();
- scopeProviderMock.Setup(x => x.Context).Returns(scopeContext);
- var scopeProvider = scopeProviderMock.Object;
+ Assert.AreEqual(2, t.LiveGen);
+ Assert.IsFalse(t.NextGen);
- using (d.GetWriter(scopeProvider))
+ var scopeContext = new ScopeContext();
+ var scopeProvider = GetScopeProvider(scopeContext);
+
+ using (d.GetScopedWriteLock(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
@@ -905,18 +994,35 @@ namespace Umbraco.Tests.Cache
Assert.AreEqual(2, s3.Gen);
Assert.AreEqual("uno", s3.Get(1));
+ // we made some changes, so a next gen is required
+ Assert.AreEqual(3, t.LiveGen);
+ Assert.IsTrue(t.NextGen);
+ Assert.AreEqual(1, t.WLocked);
+
// but live snapshot contains changes
- var ls = d.Test.LiveSnapshot;
+ var ls = t.LiveSnapshot;
Assert.AreEqual("ein", ls.Get(1));
Assert.AreEqual(3, ls.Gen);
}
+ // nothing is committed until scope exits
+ Assert.AreEqual(3, t.LiveGen);
+ Assert.IsTrue(t.NextGen);
+ Assert.AreEqual(1, t.WLocked);
+
+ // no changes until exit
var s4 = d.CreateSnapshot();
Assert.AreEqual(2, s4.Gen);
Assert.AreEqual("uno", s4.Get(1));
scopeContext.ScopeExit(false);
+ // now things have changed
+ Assert.AreEqual(2, t.LiveGen);
+ Assert.IsFalse(t.NextGen);
+ Assert.AreEqual(0, t.WLocked);
+
+ // no changes since not completed
var s5 = d.CreateSnapshot();
Assert.AreEqual(2, s5.Gen);
Assert.AreEqual("uno", s5.Get(1));
@@ -955,12 +1061,92 @@ namespace Umbraco.Tests.Cache
Assert.AreEqual("four", all[3]);
}
- private IScopeProvider GetScopeProvider(bool withContext = false)
+ [Test]
+ public void DontPanic()
{
- var scopeProviderMock = new Mock();
- var scopeContext = withContext ? new ScopeContext() : null;
- scopeProviderMock.Setup(x => x.Context).Returns(scopeContext);
- var scopeProvider = scopeProviderMock.Object;
+ var d = new SnapDictionary();
+ d.Test.CollectAuto = false;
+
+ Assert.IsNull(d.Test.GenObj);
+
+ // gen 1
+ d.Set(1, "one");
+ Assert.IsTrue(d.Test.NextGen);
+ Assert.AreEqual(1, d.Test.LiveGen);
+ Assert.IsNull(d.Test.GenObj);
+
+ var s1 = d.CreateSnapshot();
+ Assert.IsFalse(d.Test.NextGen);
+ Assert.AreEqual(1, d.Test.LiveGen);
+ Assert.IsNotNull(d.Test.GenObj);
+ Assert.AreEqual(1, d.Test.GenObj.Gen);
+
+ Assert.AreEqual(1, s1.Gen);
+ Assert.AreEqual("one", s1.Get(1));
+
+ d.Set(1, "uno");
+ Assert.IsTrue(d.Test.NextGen);
+ Assert.AreEqual(2, d.Test.LiveGen);
+ Assert.IsNotNull(d.Test.GenObj);
+ Assert.AreEqual(1, d.Test.GenObj.Gen);
+
+ var scopeContext = new ScopeContext();
+ var scopeProvider = GetScopeProvider(scopeContext);
+
+ // scopeProvider.Context == scopeContext -> writer is scoped
+ // writer is scope contextual and scoped
+ // when disposed, nothing happens
+ // when the context exists, the writer is released
+ using (d.GetScopedWriteLock(scopeProvider))
+ {
+ d.Set(1, "ein");
+ Assert.IsTrue(d.Test.NextGen);
+ Assert.AreEqual(3, d.Test.LiveGen);
+ Assert.IsNotNull(d.Test.GenObj);
+ Assert.AreEqual(2, d.Test.GenObj.Gen);
+ }
+
+ // writer has not released
+ Assert.AreEqual(1, d.Test.WLocked);
+ Assert.IsNotNull(d.Test.GenObj);
+ Assert.AreEqual(2, d.Test.GenObj.Gen);
+
+ // nothing changed
+ Assert.IsTrue(d.Test.NextGen);
+ Assert.AreEqual(3, d.Test.LiveGen);
+
+ // panic!
+ var s2 = d.CreateSnapshot();
+
+ Assert.AreEqual(1, d.Test.WLocked);
+ Assert.IsNotNull(d.Test.GenObj);
+ Assert.AreEqual(2, d.Test.GenObj.Gen);
+ Assert.AreEqual(3, d.Test.LiveGen);
+ Assert.IsTrue(d.Test.NextGen);
+
+ // release writer
+ scopeContext.ScopeExit(true);
+
+ Assert.AreEqual(0, d.Test.WLocked);
+ Assert.IsNotNull(d.Test.GenObj);
+ Assert.AreEqual(2, d.Test.GenObj.Gen);
+ Assert.AreEqual(3, d.Test.LiveGen);
+ Assert.IsTrue(d.Test.NextGen);
+
+ var s3 = d.CreateSnapshot();
+
+ Assert.AreEqual(0, d.Test.WLocked);
+ Assert.IsNotNull(d.Test.GenObj);
+ Assert.AreEqual(3, d.Test.GenObj.Gen);
+ Assert.AreEqual(3, d.Test.LiveGen);
+ Assert.IsFalse(d.Test.NextGen);
+ }
+
+ private IScopeProvider GetScopeProvider(ScopeContext scopeContext = null)
+ {
+ var scopeProvider = Mock.Of();
+ Mock.Get(scopeProvider)
+ .Setup(x => x.Context).Returns(scopeContext);
return scopeProvider;
}
}
diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs
index b3996050a6..7ab4a64f31 100644
--- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs
+++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs
@@ -8,6 +8,7 @@ using CSharpTest.Net.Collections;
using Umbraco.Core.Logging;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Scoping;
+using Umbraco.Web.PublishedCache.NuCache.Snap;
namespace Umbraco.Web.PublishedCache.NuCache
{
@@ -29,8 +30,8 @@ namespace Umbraco.Web.PublishedCache.NuCache
private readonly ILogger _logger;
private BPlusTree _localDb;
- private readonly ConcurrentQueue _genRefRefs;
- private GenRefRef _genRefRef;
+ private readonly ConcurrentQueue _genObjs;
+ private GenObj _genObj;
private readonly object _wlocko = new object();
private readonly object _rlocko = new object();
private long _liveGen, _floorGen;
@@ -64,8 +65,8 @@ namespace Umbraco.Web.PublishedCache.NuCache
_contentTypesByAlias = new ConcurrentDictionary>(StringComparer.InvariantCultureIgnoreCase);
_xmap = new ConcurrentDictionary();
- _genRefRefs = new ConcurrentQueue();
- _genRefRef = null; // no initial gen exists
+ _genObjs = new ConcurrentQueue();
+ _genObj = null; // no initial gen exists
_liveGen = _floorGen = 0;
_nextGen = false; // first time, must create a snapshot
_collectAuto = true; // collect automatically by default
@@ -91,12 +92,13 @@ namespace Umbraco.Web.PublishedCache.NuCache
}
// a scope contextual that represents a locked writer to the dictionary
- private class ContentStoreWriter : ScopeContextualBase
+ private class ScopedWriteLock : ScopeContextualBase
{
private readonly WriteLockInfo _lockinfo = new WriteLockInfo();
- private ContentStore _store;
+ private readonly ContentStore _store;
+ private int _released;
- public ContentStoreWriter(ContentStore store, bool scoped)
+ public ScopedWriteLock(ContentStore store, bool scoped)
{
_store = store;
store.Lock(_lockinfo, scoped);
@@ -104,17 +106,17 @@ namespace Umbraco.Web.PublishedCache.NuCache
public override void Release(bool completed)
{
- if (_store== null) return;
+ if (Interlocked.CompareExchange(ref _released, 1, 0) != 0)
+ return;
_store.Release(_lockinfo, completed);
- _store = null;
}
}
// gets a scope contextual representing a locked writer to the dictionary
// TODO: GetScopedWriter? should the dict have a ref onto the scope provider?
- public IDisposable GetWriter(IScopeProvider scopeProvider)
+ public IDisposable GetScopedWriteLock(IScopeProvider scopeProvider)
{
- return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ContentStoreWriter(this, scoped));
+ return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ScopedWriteLock(this, scoped));
}
private void Lock(WriteLockInfo lockInfo, bool forceGen = false)
@@ -131,12 +133,14 @@ namespace Umbraco.Web.PublishedCache.NuCache
{
_wlocked++;
lockInfo.Count = true;
- if (_nextGen == false || (forceGen && _wlocked == 1)) // if true already... ok to have "holes" in generation objects
+ if (_nextGen == false || (forceGen && _wlocked == 1))
{
// because we are changing things, a new generation
// is created, which will trigger a new snapshot
- _nextGen = true;
+ if (_nextGen)
+ _genObjs.Enqueue(_genObj = new GenObj(_liveGen));
_liveGen += 1;
+ _nextGen = true;
}
}
}
@@ -212,7 +216,6 @@ namespace Umbraco.Web.PublishedCache.NuCache
else
dictionary.TryUpdate(key, link.Next, link);
}
-
}
#endregion
@@ -836,8 +839,8 @@ namespace Umbraco.Web.PublishedCache.NuCache
// if no next generation is required, and we already have one,
// use it and create a new snapshot
- if (_nextGen == false && _genRefRef != null)
- return new Snapshot(this, _genRefRef.GetGenRef()
+ if (_nextGen == false && _genObj != null)
+ return new Snapshot(this, _genObj.GetGenRef()
#if DEBUG
, _logger
#endif
@@ -852,15 +855,15 @@ namespace Umbraco.Web.PublishedCache.NuCache
var snapGen = _nextGen ? _liveGen - 1 : _liveGen;
// create a new gen ref unless we already have it
- if (_genRefRef == null)
- _genRefRefs.Enqueue(_genRefRef = new GenRefRef(snapGen));
- else if (_genRefRef.Gen != snapGen)
+ if (_genObj == null)
+ _genObjs.Enqueue(_genObj = new GenObj(snapGen));
+ else if (_genObj.Gen != snapGen)
throw new Exception("panic");
}
else
{
// not write-locked, can use latest gen, create a new gen ref
- _genRefRefs.Enqueue(_genRefRef = new GenRefRef(_liveGen));
+ _genObjs.Enqueue(_genObj = new GenObj(_liveGen));
_nextGen = false; // this is the ONLY thing that triggers a _liveGen++
}
@@ -873,7 +876,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
// - the genRefRef weak ref is dead because all snapshots have been collected
// in both cases, we will dequeue and collect
- var snapshot = new Snapshot(this, _genRefRef.GetGenRef()
+ var snapshot = new Snapshot(this, _genObj.GetGenRef()
#if DEBUG
, _logger
#endif
@@ -930,10 +933,10 @@ namespace Umbraco.Web.PublishedCache.NuCache
#if DEBUG
_logger.Debug("Collect.");
#endif
- while (_genRefRefs.TryPeek(out GenRefRef genRefRef) && (genRefRef.Count == 0 || genRefRef.WGenRef.IsAlive == false))
+ while (_genObjs.TryPeek(out var genObj) && (genObj.Count == 0 || genObj.WeakGenRef.IsAlive == false))
{
- _genRefRefs.TryDequeue(out genRefRef); // cannot fail since TryPeek has succeeded
- _floorGen = genRefRef.Gen;
+ _genObjs.TryDequeue(out genObj); // cannot fail since TryPeek has succeeded
+ _floorGen = genObj.Gen;
#if DEBUG
//_logger.Debug("_floorGen=" + _floorGen + ", _liveGen=" + _liveGen);
#endif
@@ -1009,9 +1012,9 @@ namespace Umbraco.Web.PublishedCache.NuCache
await task;
}
- public long GenCount => _genRefRefs.Count;
+ public long GenCount => _genObjs.Count;
- public long SnapCount => _genRefRefs.Sum(x => x.Count);
+ public long SnapCount => _genObjs.Sum(x => x.Count);
#endregion
@@ -1061,24 +1064,6 @@ namespace Umbraco.Web.PublishedCache.NuCache
#region Classes
- private class LinkedNode
- where TValue: class
- {
- public LinkedNode(TValue value, long gen, LinkedNode next = null)
- {
- Value = value;
- Gen = gen;
- Next = next;
- }
-
- internal readonly long Gen;
-
- // reading & writing references is thread-safe on all .NET platforms
- // mark as volatile to ensure we always read the correct value
- internal volatile TValue Value;
- internal volatile LinkedNode Next;
- }
-
public class Snapshot : IDisposable
{
private readonly ContentStore _store;
@@ -1100,7 +1085,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
_store = store;
_genRef = genRef;
_gen = genRef.Gen;
- Interlocked.Increment(ref genRef.GenRefRef.Count);
+ Interlocked.Increment(ref genRef.GenObj.Count);
//_thisCount = _count++;
#if DEBUG
@@ -1201,49 +1186,15 @@ namespace Umbraco.Web.PublishedCache.NuCache
{
if (_gen < 0) return;
#if DEBUG
- _logger.Debug("Dispose snapshot ({Snapshot})", _genRef?.GenRefRef.Count.ToString() ?? "live");
+ _logger.Debug("Dispose snapshot ({Snapshot})", _genRef?.GenObj.Count.ToString() ?? "live");
#endif
_gen = -1;
if (_genRef != null)
- Interlocked.Decrement(ref _genRef.GenRefRef.Count);
+ Interlocked.Decrement(ref _genRef.GenObj.Count);
GC.SuppressFinalize(this);
}
}
- internal class GenRefRef
- {
- public GenRefRef(long gen)
- {
- Gen = gen;
- WGenRef = new WeakReference(null);
- }
-
- public GenRef GetGenRef()
- {
- // not thread-safe but always invoked from within a lock
- var genRef = (GenRef) WGenRef.Target;
- if (genRef == null)
- WGenRef.Target = genRef = new GenRef(this, Gen);
- return genRef;
- }
-
- public readonly long Gen;
- public readonly WeakReference WGenRef;
- public int Count;
- }
-
- internal class GenRef
- {
- public GenRef(GenRefRef genRefRef, long gen)
- {
- GenRefRef = genRefRef;
- Gen = gen;
- }
-
- public readonly GenRefRef GenRefRef;
- public readonly long Gen;
- }
-
#endregion
}
}
diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs
index e19531a25b..9c5587fbd5 100755
--- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs
+++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs
@@ -330,14 +330,15 @@ namespace Umbraco.Web.PublishedCache.NuCache
private void LockAndLoadContent(Action action)
{
- using (_contentStore.GetWriter(_scopeProvider))
+ // first get a writer, then a scope
+ // if there already is a scope, the writer will attach to it
+ // otherwise, it will only exist here - cheap
+ using (_contentStore.GetScopedWriteLock(_scopeProvider))
+ using (var scope = _scopeProvider.CreateScope())
{
- using (var scope = _scopeProvider.CreateScope())
- {
- scope.ReadLock(Constants.Locks.ContentTree);
- action(scope);
- scope.Complete();
- }
+ scope.ReadLock(Constants.Locks.ContentTree);
+ action(scope);
+ scope.Complete();
}
}
@@ -399,14 +400,13 @@ namespace Umbraco.Web.PublishedCache.NuCache
private void LockAndLoadMedia(Action action)
{
- using (_mediaStore.GetWriter(_scopeProvider))
+ // see note in LockAndLoadContent
+ using (_mediaStore.GetScopedWriteLock(_scopeProvider))
+ using (var scope = _scopeProvider.CreateScope())
{
- using (var scope = _scopeProvider.CreateScope())
- {
- scope.ReadLock(Constants.Locks.MediaTree);
- action(scope);
- scope.Complete();
- }
+ scope.ReadLock(Constants.Locks.MediaTree);
+ action(scope);
+ scope.Complete();
}
}
@@ -528,14 +528,13 @@ namespace Umbraco.Web.PublishedCache.NuCache
private void LockAndLoadDomains()
{
- using (_domainStore.GetWriter(_scopeProvider))
+ // see note in LockAndLoadContent
+ using (_domainStore.GetScopedWriteLock(_scopeProvider))
+ using (var scope = _scopeProvider.CreateScope())
{
- using (var scope = _scopeProvider.CreateScope())
- {
- scope.ReadLock(Constants.Locks.Domains);
- LoadDomainsLocked();
- scope.Complete();
- }
+ scope.ReadLock(Constants.Locks.Domains);
+ LoadDomainsLocked();
+ scope.Complete();
}
}
@@ -587,7 +586,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
return;
}
- using (_contentStore.GetWriter(_scopeProvider))
+ using (_contentStore.GetScopedWriteLock(_scopeProvider))
{
NotifyLocked(payloads, out bool draftChanged2, out bool publishedChanged2);
draftChanged = draftChanged2;
@@ -683,7 +682,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
return;
}
- using (_mediaStore.GetWriter(_scopeProvider))
+ using (_mediaStore.GetScopedWriteLock(_scopeProvider))
{
NotifyLocked(payloads, out bool anythingChanged2);
anythingChanged = anythingChanged2;
@@ -804,7 +803,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
if (removedIds.Count == 0 && refreshedIds.Count == 0 && otherIds.Count == 0 && newIds.Count == 0) return;
- using (store.GetWriter(_scopeProvider))
+ using (store.GetScopedWriteLock(_scopeProvider))
{
// ReSharper disable AccessToModifiedClosure
action(removedIds, refreshedIds, otherIds, newIds);
@@ -825,8 +824,8 @@ namespace Umbraco.Web.PublishedCache.NuCache
payload.Removed ? "Removed" : "Refreshed",
payload.Id);
- using (_contentStore.GetWriter(_scopeProvider))
- using (_mediaStore.GetWriter(_scopeProvider))
+ using (_contentStore.GetScopedWriteLock(_scopeProvider))
+ using (_mediaStore.GetScopedWriteLock(_scopeProvider))
{
// TODO: need to add a datatype lock
// this is triggering datatypes reload in the factory, and right after we create some
@@ -858,7 +857,8 @@ namespace Umbraco.Web.PublishedCache.NuCache
if (_isReady == false)
return;
- using (_domainStore.GetWriter(_scopeProvider))
+ // see note in LockAndLoadContent
+ using (_domainStore.GetScopedWriteLock(_scopeProvider))
{
foreach (var payload in payloads)
{
diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Snap/GenObj.cs b/src/Umbraco.Web/PublishedCache/NuCache/Snap/GenObj.cs
new file mode 100644
index 0000000000..b69dab7dac
--- /dev/null
+++ b/src/Umbraco.Web/PublishedCache/NuCache/Snap/GenObj.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Threading;
+
+namespace Umbraco.Web.PublishedCache.NuCache.Snap
+{
+ internal class GenObj
+ {
+ public GenObj(long gen)
+ {
+ Gen = gen;
+ WeakGenRef = new WeakReference(null);
+ }
+
+ public GenRef GetGenRef()
+ {
+ // not thread-safe but always invoked from within a lock
+ var genRef = (GenRef)WeakGenRef.Target;
+ if (genRef == null)
+ WeakGenRef.Target = genRef = new GenRef(this);
+ return genRef;
+ }
+
+ public readonly long Gen;
+ public readonly WeakReference WeakGenRef;
+ public int Count;
+
+ public void Reference()
+ {
+ Interlocked.Increment(ref Count);
+ }
+
+ public void Release()
+ {
+ Interlocked.Decrement(ref Count);
+ }
+ }
+}
diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Snap/GenRef.cs b/src/Umbraco.Web/PublishedCache/NuCache/Snap/GenRef.cs
new file mode 100644
index 0000000000..ade0251b8d
--- /dev/null
+++ b/src/Umbraco.Web/PublishedCache/NuCache/Snap/GenRef.cs
@@ -0,0 +1,13 @@
+namespace Umbraco.Web.PublishedCache.NuCache.Snap
+{
+ internal class GenRef
+ {
+ public GenRef(GenObj genObj)
+ {
+ GenObj = genObj;
+ }
+
+ public readonly GenObj GenObj;
+ public long Gen => GenObj.Gen;
+ }
+}
diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs b/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs
new file mode 100644
index 0000000000..20d7e7ddcd
--- /dev/null
+++ b/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs
@@ -0,0 +1,20 @@
+namespace Umbraco.Web.PublishedCache.NuCache.Snap
+{
+ internal class LinkedNode
+ where TValue : class
+ {
+ public LinkedNode(TValue value, long gen, LinkedNode next = null)
+ {
+ Value = value;
+ Gen = gen;
+ Next = next;
+ }
+
+ public readonly long Gen;
+
+ // reading & writing references is thread-safe on all .NET platforms
+ // mark as volatile to ensure we always read the correct value
+ public volatile TValue Value;
+ public volatile LinkedNode Next;
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs
index 30f6e7e638..c5b1df1206 100644
--- a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs
+++ b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs
@@ -5,6 +5,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Umbraco.Core.Scoping;
+using Umbraco.Web.PublishedCache.NuCache.Snap;
namespace Umbraco.Web.PublishedCache.NuCache
{
@@ -20,9 +21,9 @@ namespace Umbraco.Web.PublishedCache.NuCache
// This class is optimized for many readers, few writers
// Readers are lock-free
- private readonly ConcurrentDictionary _items;
- private readonly ConcurrentQueue _generationObjects;
- private GenerationObject _generationObject;
+ private readonly ConcurrentDictionary> _items;
+ private readonly ConcurrentQueue _genObjs;
+ private GenObj _genObj;
private readonly object _wlocko = new object();
private readonly object _rlocko = new object();
private long _liveGen, _floorGen;
@@ -40,9 +41,9 @@ namespace Umbraco.Web.PublishedCache.NuCache
public SnapDictionary()
{
- _items = new ConcurrentDictionary();
- _generationObjects = new ConcurrentQueue();
- _generationObject = null; // no initial gen exists
+ _items = new ConcurrentDictionary>();
+ _genObjs = new ConcurrentQueue();
+ _genObj = null; // no initial gen exists
_liveGen = _floorGen = 0;
_nextGen = false; // first time, must create a snapshot
_collectAuto = true; // collect automatically by default
@@ -86,12 +87,13 @@ namespace Umbraco.Web.PublishedCache.NuCache
}
// a scope contextual that represents a locked writer to the dictionary
- private class SnapDictionaryWriter : ScopeContextualBase
+ private class ScopedWriteLock : ScopeContextualBase
{
private readonly WriteLockInfo _lockinfo = new WriteLockInfo();
- private SnapDictionary _dictionary;
+ private readonly SnapDictionary _dictionary;
+ private int _released;
- public SnapDictionaryWriter(SnapDictionary dictionary, bool scoped)
+ public ScopedWriteLock(SnapDictionary dictionary, bool scoped)
{
_dictionary = dictionary;
dictionary.Lock(_lockinfo, scoped);
@@ -99,17 +101,19 @@ namespace Umbraco.Web.PublishedCache.NuCache
public override void Release(bool completed)
{
- if (_dictionary == null) return;
+ if (Interlocked.CompareExchange(ref _released, 1, 0) != 0)
+ return;
_dictionary.Release(_lockinfo, completed);
- _dictionary = null;
}
}
// gets a scope contextual representing a locked writer to the dictionary
- // GetScopedWriter? should the dict have a ref onto the scope provider?
- public IDisposable GetWriter(IScopeProvider scopeProvider)
+ // the dict is write-locked until the write-lock is released
+ // which happens when it is disposed (non-scoped)
+ // or when the scope context exits (scoped)
+ public IDisposable GetScopedWriteLock(IScopeProvider scopeProvider)
{
- return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new SnapDictionaryWriter(this, scoped));
+ return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ScopedWriteLock(this, scoped));
}
private void Lock(WriteLockInfo lockInfo, bool forceGen = false)
@@ -129,14 +133,18 @@ namespace Umbraco.Web.PublishedCache.NuCache
//RuntimeHelpers.PrepareConstrainedRegions();
try { } finally
{
+ // increment the lock count, and register that this lock is counting
_wlocked++;
lockInfo.Count = true;
- if (_nextGen == false || (forceGen && _wlocked == 1)) // if true already... ok to have "holes" in generation objects
+
+ if (_nextGen == false || (forceGen && _wlocked == 1))
{
// because we are changing things, a new generation
// is created, which will trigger a new snapshot
- _nextGen = true;
+ if (_nextGen)
+ _genObjs.Enqueue(_genObj = new GenObj(_liveGen));
_liveGen += 1;
+ _nextGen = true; // this is the ONLY place where _nextGen becomes true
}
}
}
@@ -153,6 +161,10 @@ namespace Umbraco.Web.PublishedCache.NuCache
private void Release(WriteLockInfo lockInfo, bool commit = true)
{
+ // if the lock wasn't taken in the first place, do nothing
+ if (!lockInfo.Taken)
+ return;
+
if (commit == false)
{
var rtaken = false;
@@ -161,6 +173,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
Monitor.Enter(_rlocko, ref rtaken);
try { } finally
{
+ // forget about the temp. liveGen
_nextGen = false;
_liveGen -= 1;
}
@@ -183,8 +196,9 @@ namespace Umbraco.Web.PublishedCache.NuCache
}
}
+ // decrement the lock count, if counting, then exit the lock
if (lockInfo.Count) _wlocked--;
- if (lockInfo.Taken) Monitor.Exit(_wlocko);
+ Monitor.Exit(_wlocko);
}
private void Release(ReadLockInfo lockInfo)
@@ -198,9 +212,9 @@ namespace Umbraco.Web.PublishedCache.NuCache
public int Count => _items.Count;
- private LinkedNode GetHead(TKey key)
+ private LinkedNode GetHead(TKey key)
{
- _items.TryGetValue(key, out LinkedNode link); // else null
+ _items.TryGetValue(key, out var link); // else null
return link;
}
@@ -221,7 +235,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
// 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(value, _liveGen, link), link);
+ _items.TryUpdate(key, new LinkedNode(value, _liveGen, link), link);
}
else
{
@@ -235,7 +249,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
}
else
{
- _items.TryAdd(key, new LinkedNode(value, _liveGen));
+ _items.TryAdd(key, new LinkedNode(value, _liveGen));
}
}
finally
@@ -261,7 +275,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
{
if (kvp.Value.Gen < _liveGen)
{
- var link = new LinkedNode(null, _liveGen, kvp.Value);
+ var link = new LinkedNode(null, _liveGen, kvp.Value);
_items.TryUpdate(kvp.Key, link, kvp.Value);
}
else
@@ -337,12 +351,12 @@ namespace Umbraco.Web.PublishedCache.NuCache
{
Lock(lockInfo);
- // if no next generation is required, and we already have one,
- // use it and create a new snapshot
- if (_nextGen == false && _generationObject != null)
- return new Snapshot(this, _generationObject.GetReference());
+ // 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)
+ return new Snapshot(this, _genObj.GetGenRef());
- // else we need to try to create a new gen ref
+ // 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 --
@@ -350,29 +364,32 @@ namespace Umbraco.Web.PublishedCache.NuCache
// write-locked, cannot use latest gen (at least 1) so use previous
var snapGen = _nextGen ? _liveGen - 1 : _liveGen;
- // create a new gen ref unless we already have it
- if (_generationObject == null)
- _generationObjects.Enqueue(_generationObject = new GenerationObject(snapGen));
- else if (_generationObject.Gen != snapGen)
+ // create a new gen object if we don't already have one
+ // (happens the first time a snapshot is created)
+ if (_genObj == null)
+ _genObjs.Enqueue(_genObj = new GenObj(snapGen));
+
+ // if we have one already, ensure it's consistent
+ else if (_genObj.Gen != snapGen)
throw new Exception("panic");
}
else
{
- // not write-locked, can use latest gen, create a new gen ref
- _generationObjects.Enqueue(_generationObject = new GenerationObject(_liveGen));
+ // not write-locked, can use latest gen (_liveGen), create a corresponding new gen object
+ _genObjs.Enqueue(_genObj = new GenObj(_liveGen));
_nextGen = false; // this is the ONLY thing that triggers a _liveGen++
}
// so...
- // the genRefRef has a weak ref to the genRef, and is queued
- // the snapshot has a ref to the genRef, which has a ref to the genRefRef
- // when the snapshot is disposed, it decreases genRefRef counter
+ // the genObj has a weak ref to the genRef, and is queued
+ // the snapshot has a ref to the genRef, which has a ref to the genObj
+ // when the snapshot is disposed, it decreases genObj counter
// so after a while, one of these conditions is going to be true:
- // - the genRefRef counter is zero because all snapshots have properly been disposed
- // - the genRefRef weak ref is dead because all snapshots have been collected
+ // - genObj.Count is zero because all snapshots have properly been disposed
+ // - genObj.WeakGenRef is dead because all snapshots have been collected
// in both cases, we will dequeue and collect
- var snapshot = new Snapshot(this, _generationObject.GetReference());
+ var snapshot = new Snapshot(this, _genObj.GetGenRef());
// reading _floorGen is safe if _collectTask is null
if (_collectTask == null && _collectAuto && _liveGen - _floorGen > CollectMinGenDelta)
@@ -416,16 +433,16 @@ namespace Umbraco.Web.PublishedCache.NuCache
private void Collect()
{
// see notes in CreateSnapshot
- while (_generationObjects.TryPeek(out GenerationObject generationObject) && (generationObject.Count == 0 || generationObject.WeakReference.IsAlive == false))
+ while (_genObjs.TryPeek(out var genObj) && (genObj.Count == 0 || genObj.WeakGenRef.IsAlive == false))
{
- _generationObjects.TryDequeue(out generationObject); // cannot fail since TryPeek has succeeded
- _floorGen = generationObject.Gen;
+ _genObjs.TryDequeue(out genObj); // cannot fail since TryPeek has succeeded
+ _floorGen = genObj.Gen;
}
Collect(_items);
}
- private void Collect(ConcurrentDictionary dict)
+ private void Collect(ConcurrentDictionary> dict)
{
// it is OK to enumerate a concurrent dictionary and it does not lock
// it - and here it's not an issue if we skip some items, they will be
@@ -460,7 +477,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
// not live, null value, no next link = remove that one -- but only if
// the dict has not been updated, have to do it via ICollection<> (thanks
// Mr Toub) -- and if the dict has been updated there is nothing to collect
- var idict = dict as ICollection>;
+ var idict = dict as ICollection>>;
/*var removed =*/ idict.Remove(kvp);
//Console.WriteLine("remove (" + (removed ? "true" : "false") + ")");
continue;
@@ -485,14 +502,14 @@ namespace Umbraco.Web.PublishedCache.NuCache
{
task = _collectTask;
}
- return task ?? Task.FromResult(0);
+ return task ?? Task.CompletedTask;
//if (task != null)
// await task;
}
- public long GenCount => _generationObjects.Count;
+ public long GenCount => _genObjs.Count;
- public long SnapCount => _generationObjects.Sum(x => x.Count);
+ public long SnapCount => _genObjs.Sum(x => x.Count);
#endregion
@@ -513,6 +530,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 CollectAuto
{
@@ -520,13 +538,15 @@ namespace Umbraco.Web.PublishedCache.NuCache
set => _dict._collectAuto = value;
}
- public ConcurrentQueue GenerationObjects => _dict._generationObjects;
+ public GenObj GenObj => _dict._genObj;
+
+ public ConcurrentQueue GenObjs => _dict._genObjs;
public Snapshot LiveSnapshot => new Snapshot(_dict, _dict._liveGen);
public GenVal[] GetValues(TKey key)
{
- _dict._items.TryGetValue(key, out LinkedNode link); // else null
+ _dict._items.TryGetValue(key, out var link); // else null
if (link == null)
return new GenVal[0];
@@ -559,35 +579,23 @@ namespace Umbraco.Web.PublishedCache.NuCache
#region Classes
- private class LinkedNode
- {
- public LinkedNode(TValue value, long gen, LinkedNode next = null)
- {
- Value = value;
- Gen = gen;
- Next = next;
- }
-
- internal readonly long Gen;
-
- // reading & writing references is thread-safe on all .NET platforms
- // mark as volatile to ensure we always read the correct value
- internal volatile TValue Value;
- internal volatile LinkedNode Next;
- }
-
public class Snapshot : IDisposable
{
private readonly SnapDictionary _store;
- private readonly GenerationReference _generationReference;
- private long _gen; // copied for perfs
+ private readonly GenRef _genRef;
+ private readonly long _gen; // copied for perfs
+ private int _disposed;
- internal Snapshot(SnapDictionary store, GenerationReference generationReference)
+ //private static int _count;
+ //private readonly int _thisCount;
+
+ internal Snapshot(SnapDictionary store, GenRef genRef)
{
_store = store;
- _generationReference = generationReference;
- _gen = generationReference.GenerationObject.Gen;
- _generationReference.GenerationObject.Reference();
+ _genRef = genRef;
+ _gen = genRef.GenObj.Gen;
+ _genRef.GenObj.Reference();
+ //_thisCount = _count++;
}
internal Snapshot(SnapDictionary store, long gen)
@@ -596,17 +604,21 @@ namespace Umbraco.Web.PublishedCache.NuCache
_gen = gen;
}
+ private void EnsureNotDisposed()
+ {
+ if (_disposed > 0)
+ throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/);
+ }
+
public TValue Get(TKey key)
{
- if (_gen < 0)
- throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/);
+ EnsureNotDisposed();
return _store.Get(key, _gen);
}
public IEnumerable GetAll()
{
- if (_gen < 0)
- throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/);
+ EnsureNotDisposed();
return _store.GetAll(_gen);
}
@@ -614,8 +626,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
{
get
{
- if (_gen < 0)
- throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/);
+ EnsureNotDisposed();
return _store.IsEmpty(_gen);
}
}
@@ -624,63 +635,20 @@ namespace Umbraco.Web.PublishedCache.NuCache
{
get
{
- if (_gen < 0)
- throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/);
+ EnsureNotDisposed();
return _gen;
}
}
public void Dispose()
{
- if (_gen < 0) return;
- _gen = -1;
- _generationReference?.GenerationObject.Release();
+ if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0)
+ return;
+ _genRef?.GenObj.Release();
GC.SuppressFinalize(this);
}
}
- internal class GenerationObject
- {
- public GenerationObject(long gen)
- {
- Gen = gen;
- WeakReference = new WeakReference(null);
- }
-
- public GenerationReference GetReference()
- {
- // not thread-safe but always invoked from within a lock
- var generationReference = (GenerationReference) WeakReference.Target;
- if (generationReference == null)
- WeakReference.Target = generationReference = new GenerationReference(this);
- return generationReference;
- }
-
- public readonly long Gen;
- public readonly WeakReference WeakReference;
- public int Count;
-
- public void Reference()
- {
- Interlocked.Increment(ref Count);
- }
-
- public void Release()
- {
- Interlocked.Decrement(ref Count);
- }
- }
-
- internal class GenerationReference
- {
- public GenerationReference(GenerationObject generationObject)
- {
- GenerationObject = generationObject;
- }
-
- public readonly GenerationObject GenerationObject;
- }
-
#endregion
}
}
diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj
index 09cc7d856a..c6cbc7cbaa 100755
--- a/src/Umbraco.Web/Umbraco.Web.csproj
+++ b/src/Umbraco.Web/Umbraco.Web.csproj
@@ -205,6 +205,9 @@
+
+
+