Merge remote-tracking branch 'origin/v8/8.0' into temp8-4011
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
|
||||
<metadata minClientVersion="4.6.0">
|
||||
<metadata minClientVersion="4.1.0">
|
||||
<id>UmbracoCms.Core</id>
|
||||
<version>8.0.0</version>
|
||||
<title>Umbraco Cms Core Binaries</title>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
|
||||
<metadata minClientVersion="4.6.0">
|
||||
<metadata minClientVersion="4.1.0">
|
||||
<id>UmbracoCms.Web</id>
|
||||
<version>8.0.0</version>
|
||||
<title>Umbraco Cms Core Binaries</title>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
|
||||
<metadata minClientVersion="4.6.0">
|
||||
<metadata minClientVersion="4.1.0">
|
||||
<id>UmbracoCms</id>
|
||||
<version>8.0.0</version>
|
||||
<title>Umbraco Cms</title>
|
||||
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// Provides a base class for scope contextual objects.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>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.</para>
|
||||
/// </remarks>
|
||||
public abstract class ScopeContextualBase : IDisposable
|
||||
{
|
||||
private bool _using, _scoped;
|
||||
private bool _scoped;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a contextual object.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the object.</typeparam>
|
||||
/// <param name="scopeProvider">A scope provider.</param>
|
||||
/// <param name="key">A context key for the object.</param>
|
||||
/// <param name="ctor">A function producing the contextual object.</param>
|
||||
/// <returns>The contextual object.</returns>
|
||||
/// <remarks>
|
||||
/// <para></para>
|
||||
/// </remarks>
|
||||
public static T Get<T>(IScopeProvider scopeProvider, string key, Func<bool, T> 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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// <para>If not scoped, then this releases the contextual object.</para>
|
||||
/// </remarks>
|
||||
public void Dispose()
|
||||
{
|
||||
_using = false;
|
||||
|
||||
if (_scoped == false)
|
||||
Release(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the contextual object.
|
||||
/// </summary>
|
||||
/// <param name="completed">A value indicating whether the scoped operation completed.</param>
|
||||
public abstract void Release(bool completed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2875,7 +2875,14 @@ namespace Umbraco.Core.Services.Implement
|
||||
{
|
||||
foreach (var property in blueprint.Properties)
|
||||
{
|
||||
content.SetValue(property.Alias, property.GetValue(culture), culture);
|
||||
if (property.PropertyType.VariesByCulture())
|
||||
{
|
||||
content.SetValue(property.Alias, property.GetValue(culture), culture);
|
||||
}
|
||||
else
|
||||
{
|
||||
content.SetValue(property.Alias, property.GetValue());
|
||||
}
|
||||
}
|
||||
|
||||
content.Name = blueprint.Name;
|
||||
|
||||
@@ -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<int, string>.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<int, string>();
|
||||
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<int, string>();
|
||||
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<int, string>();
|
||||
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<int, string>();
|
||||
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<IScopeProvider>();
|
||||
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<IScopeProvider>();
|
||||
var scopeContext = withContext ? new ScopeContext() : null;
|
||||
scopeProviderMock.Setup(x => x.Context).Returns(scopeContext);
|
||||
var scopeProvider = scopeProviderMock.Object;
|
||||
var d = new SnapDictionary<int, string>();
|
||||
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<IScopeProvider>();
|
||||
Mock.Get(scopeProvider)
|
||||
.Setup(x => x.Context).Returns(scopeContext);
|
||||
return scopeProvider;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,8 +62,8 @@ function valPropertyMsg(serverValidationManager) {
|
||||
if (!watcher) {
|
||||
watcher = scope.$watch("currentProperty.value",
|
||||
function (newValue, oldValue) {
|
||||
|
||||
if (!newValue || angular.equals(newValue, oldValue)) {
|
||||
|
||||
if (angular.equals(newValue, oldValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -78,10 +78,12 @@ function valPropertyMsg(serverValidationManager) {
|
||||
// based on other errors. We'll also check if there's no other validation errors apart from valPropertyMsg, if valPropertyMsg
|
||||
// is the only one, then we'll clear.
|
||||
|
||||
if ((errCount === 1 && angular.isArray(formCtrl.$error.valPropertyMsg)) || (formCtrl.$invalid && angular.isArray(formCtrl.$error.valServer))) {
|
||||
if (errCount === 0 || (errCount === 1 && angular.isArray(formCtrl.$error.valPropertyMsg)) || (formCtrl.$invalid && angular.isArray(formCtrl.$error.valServer))) {
|
||||
scope.errorMsg = "";
|
||||
formCtrl.$setValidity('valPropertyMsg', true);
|
||||
stopWatch();
|
||||
} else if (showValidation && scope.errorMsg === "") {
|
||||
formCtrl.$setValidity('valPropertyMsg', false);
|
||||
scope.errorMsg = getErrorMsg();
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
@@ -152,6 +154,7 @@ function valPropertyMsg(serverValidationManager) {
|
||||
showValidation = true;
|
||||
if (hasError && scope.errorMsg === "") {
|
||||
scope.errorMsg = getErrorMsg();
|
||||
startWatch();
|
||||
}
|
||||
else if (!hasError) {
|
||||
scope.errorMsg = "";
|
||||
|
||||
@@ -113,7 +113,12 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.servi
|
||||
$scope.exitPreview = function () {
|
||||
|
||||
var culture = $location.search().culture || getParameterByName("culture");
|
||||
var relativeUrl = "/" + $scope.pageId +'?culture='+ culture;
|
||||
var relativeUrl = "/" + $scope.pageId;
|
||||
|
||||
if(culture){
|
||||
relativeUrl +='?culture='+ culture;
|
||||
}
|
||||
|
||||
window.top.location.href = "../preview/end?redir=" + encodeURIComponent(relativeUrl);
|
||||
};
|
||||
|
||||
|
||||
@@ -46,8 +46,7 @@
|
||||
|
||||
if (data.language !== "undefined") {
|
||||
var lang = vm.languages.filter(function (l) {
|
||||
return matchLanguageById(l, data.language.Id);
|
||||
|
||||
return matchLanguageById(l, data.language);
|
||||
});
|
||||
if (lang.length > 0) {
|
||||
vm.language = lang[0];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Umbraco.Core;
|
||||
@@ -14,11 +15,18 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
[DefaultPropertyValueConverter]
|
||||
public class MediaPickerValueConverter : PropertyValueConverterBase
|
||||
{
|
||||
// hard-coding "image" here but that's how it works at UI level too
|
||||
private const string ImageTypeAlias = "image";
|
||||
|
||||
private readonly IPublishedModelFactory _publishedModelFactory;
|
||||
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
|
||||
|
||||
public MediaPickerValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor)
|
||||
public MediaPickerValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IPublishedModelFactory publishedModelFactory)
|
||||
{
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor ?? throw new ArgumentNullException(nameof(publishedSnapshotAccessor));
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor ??
|
||||
throw new ArgumentNullException(nameof(publishedSnapshotAccessor));
|
||||
_publishedModelFactory = publishedModelFactory;
|
||||
}
|
||||
|
||||
public override bool IsConverter(PublishedPropertyType propertyType)
|
||||
@@ -31,15 +39,19 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
var isMultiple = IsMultipleDataType(propertyType.DataType);
|
||||
var isOnlyImages = IsOnlyImagesDataType(propertyType.DataType);
|
||||
|
||||
// hard-coding "image" here but that's how it works at UI level too
|
||||
|
||||
return isMultiple
|
||||
? (isOnlyImages ? typeof(IEnumerable<>).MakeGenericType(ModelType.For("image")) : typeof(IEnumerable<IPublishedContent>))
|
||||
: (isOnlyImages ? ModelType.For("image") : typeof(IPublishedContent));
|
||||
? isOnlyImages
|
||||
? typeof(IEnumerable<>).MakeGenericType(ModelType.For(ImageTypeAlias))
|
||||
: typeof(IEnumerable<IPublishedContent>)
|
||||
: isOnlyImages
|
||||
? ModelType.For(ImageTypeAlias)
|
||||
: typeof(IPublishedContent);
|
||||
}
|
||||
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Snapshot;
|
||||
{
|
||||
return PropertyCacheLevel.Snapshot;
|
||||
}
|
||||
|
||||
private bool IsMultipleDataType(PublishedDataType dataType)
|
||||
{
|
||||
@@ -53,26 +65,31 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
return config.OnlyImages;
|
||||
}
|
||||
|
||||
public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview)
|
||||
public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType,
|
||||
object source, bool preview)
|
||||
{
|
||||
if (source == null) return null;
|
||||
|
||||
var nodeIds = source.ToString()
|
||||
.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Split(new[] {","}, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(Udi.Parse)
|
||||
.ToArray();
|
||||
return nodeIds;
|
||||
}
|
||||
|
||||
public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview)
|
||||
public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType,
|
||||
PropertyCacheLevel cacheLevel, object source, bool preview)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var isMultiple = IsMultipleDataType(propertyType.DataType);
|
||||
var isOnlyImages = IsOnlyImagesDataType(propertyType.DataType);
|
||||
|
||||
var udis = (Udi[]) source;
|
||||
var mediaItems = isOnlyImages
|
||||
? _publishedModelFactory.CreateModelList(ImageTypeAlias)
|
||||
: new List<IPublishedContent>();
|
||||
|
||||
if (source == null) return isMultiple ? mediaItems : null;
|
||||
|
||||
var udis = (Udi[])source;
|
||||
var mediaItems = new List<IPublishedContent>();
|
||||
if (udis.Any())
|
||||
{
|
||||
foreach (var udi in udis)
|
||||
@@ -84,12 +101,15 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
mediaItems.Add(item);
|
||||
}
|
||||
|
||||
if (IsMultipleDataType(propertyType.DataType))
|
||||
return mediaItems;
|
||||
return mediaItems.FirstOrDefault();
|
||||
return isMultiple ? mediaItems : FirstOrDefault(mediaItems);
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
private object FirstOrDefault(IList mediaItems)
|
||||
{
|
||||
return mediaItems.Count == 0 ? null : mediaItems[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<int, ContentNodeKit> _localDb;
|
||||
private readonly ConcurrentQueue<GenRefRef> _genRefRefs;
|
||||
private GenRefRef _genRefRef;
|
||||
private readonly ConcurrentQueue<GenObj> _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<string, LinkedNode<PublishedContentType>>(StringComparer.InvariantCultureIgnoreCase);
|
||||
_xmap = new ConcurrentDictionary<Guid, int>();
|
||||
|
||||
_genRefRefs = new ConcurrentQueue<GenRefRef>();
|
||||
_genRefRef = null; // no initial gen exists
|
||||
_genObjs = new ConcurrentQueue<GenObj>();
|
||||
_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<ContentStore>("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<ContentStore>("_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<TValue>
|
||||
where TValue: class
|
||||
{
|
||||
public LinkedNode(TValue value, long gen, LinkedNode<TValue> 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<TValue> 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<Snapshot>("Dispose snapshot ({Snapshot})", _genRef?.GenRefRef.Count.ToString() ?? "live");
|
||||
_logger.Debug<Snapshot>("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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,14 +330,15 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
|
||||
private void LockAndLoadContent(Action<IScope> 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<IScope> 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)
|
||||
{
|
||||
|
||||
37
src/Umbraco.Web/PublishedCache/NuCache/Snap/GenObj.cs
Normal file
37
src/Umbraco.Web/PublishedCache/NuCache/Snap/GenObj.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Umbraco.Web/PublishedCache/NuCache/Snap/GenRef.cs
Normal file
13
src/Umbraco.Web/PublishedCache/NuCache/Snap/GenRef.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
20
src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs
Normal file
20
src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace Umbraco.Web.PublishedCache.NuCache.Snap
|
||||
{
|
||||
internal class LinkedNode<TValue>
|
||||
where TValue : class
|
||||
{
|
||||
public LinkedNode(TValue value, long gen, LinkedNode<TValue> 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<TValue> Next;
|
||||
}
|
||||
}
|
||||
@@ -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<TKey, LinkedNode> _items;
|
||||
private readonly ConcurrentQueue<GenerationObject> _generationObjects;
|
||||
private GenerationObject _generationObject;
|
||||
private readonly ConcurrentDictionary<TKey, LinkedNode<TValue>> _items;
|
||||
private readonly ConcurrentQueue<GenObj> _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<TKey, LinkedNode>();
|
||||
_generationObjects = new ConcurrentQueue<GenerationObject>();
|
||||
_generationObject = null; // no initial gen exists
|
||||
_items = new ConcurrentDictionary<TKey, LinkedNode<TValue>>();
|
||||
_genObjs = new ConcurrentQueue<GenObj>();
|
||||
_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<TKey, TValue> _dictionary;
|
||||
private readonly SnapDictionary<TKey, TValue> _dictionary;
|
||||
private int _released;
|
||||
|
||||
public SnapDictionaryWriter(SnapDictionary<TKey, TValue> dictionary, bool scoped)
|
||||
public ScopedWriteLock(SnapDictionary<TKey, TValue> 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<TValue> 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<TValue>(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<TValue>(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<TValue>(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<TKey, LinkedNode> dict)
|
||||
private void Collect(ConcurrentDictionary<TKey, LinkedNode<TValue>> 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<KeyValuePair<TKey, LinkedNode>>;
|
||||
var idict = dict as ICollection<KeyValuePair<TKey, LinkedNode<TValue>>>;
|
||||
/*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<GenerationObject> GenerationObjects => _dict._generationObjects;
|
||||
public GenObj GenObj => _dict._genObj;
|
||||
|
||||
public ConcurrentQueue<GenObj> 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<TKey, TValue> _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<TKey, TValue> store, GenerationReference generationReference)
|
||||
//private static int _count;
|
||||
//private readonly int _thisCount;
|
||||
|
||||
internal Snapshot(SnapDictionary<TKey, TValue> 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<TKey, TValue> 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<TValue> 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,6 +205,9 @@
|
||||
<Compile Include="Models\PublishedContent\HybridVariationContextAccessor.cs" />
|
||||
<Compile Include="Models\TemplateQuery\QueryConditionExtensions.cs" />
|
||||
<Compile Include="Mvc\SurfaceControllerTypeCollectionBuilder.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\Snap\GenObj.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\Snap\GenRef.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\Snap\LinkedNode.cs" />
|
||||
<Compile Include="Routing\IPublishedRouter.cs" />
|
||||
<Compile Include="Services\DashboardService.cs" />
|
||||
<Compile Include="Services\IDashboardService.cs" />
|
||||
|
||||
Reference in New Issue
Block a user