Updates the caching layer to handle GUID keys for content types while preserving backwards compat, fixes unit tests, removes the strongly typed lists for the block editor value since it's unecessary

This commit is contained in:
Shannon
2020-06-10 16:12:00 +10:00
parent aa447bc704
commit a5adb322f1
44 changed files with 392 additions and 224 deletions

View File

@@ -7,6 +7,16 @@ using Umbraco.Core.Xml;
namespace Umbraco.Web.PublishedCache
{
public interface IPublishedCache2 : IPublishedCache
{
/// <summary>
/// Gets a content type identified by its alias.
/// </summary>
/// <param name="key">The content type key.</param>
/// <returns>The content type, or null.</returns>
IPublishedContentType GetContentType(Guid key);
}
/// <summary>
/// Provides access to cached contents.
/// </summary>

View File

@@ -4,6 +4,11 @@ using Umbraco.Core.Models.PublishedContent;
namespace Umbraco.Web.PublishedCache
{
public interface IPublishedContentCache2 : IPublishedContentCache, IPublishedCache2
{
// NOTE: this is here purely to avoid API breaking changes
}
public interface IPublishedContentCache : IPublishedCache
{
/// <summary>

View File

@@ -1,5 +1,10 @@
namespace Umbraco.Web.PublishedCache
{
public interface IPublishedMediaCache2 : IPublishedMediaCache, IPublishedCache2
{
// NOTE: this is here purely to avoid API breaking changes
}
public interface IPublishedMediaCache : IPublishedCache
{ }
}

View File

@@ -13,7 +13,7 @@ using Umbraco.Web.PublishedCache.NuCache.Navigable;
namespace Umbraco.Web.PublishedCache.NuCache
{
internal class ContentCache : PublishedCacheBase, IPublishedContentCache, INavigableData, IDisposable
internal class ContentCache : PublishedCacheBase, IPublishedContentCache2, INavigableData, IDisposable
{
private readonly ContentStore.Snapshot _snapshot;
private readonly IAppCache _snapshotCache;
@@ -384,15 +384,11 @@ namespace Umbraco.Web.PublishedCache.NuCache
#region Content types
public override IPublishedContentType GetContentType(int id)
{
return _snapshot.GetContentType(id);
}
public override IPublishedContentType GetContentType(int id) => _snapshot.GetContentType(id);
public override IPublishedContentType GetContentType(string alias)
{
return _snapshot.GetContentType(alias);
}
public override IPublishedContentType GetContentType(string alias) => _snapshot.GetContentType(alias);
public override IPublishedContentType GetContentType(Guid key) => _snapshot.GetContentType(key);
#endregion

View File

@@ -37,9 +37,14 @@ namespace Umbraco.Web.PublishedCache.NuCache
private readonly IVariationContextAccessor _variationContextAccessor;
private readonly ConcurrentDictionary<int, LinkedNode<ContentNode>> _contentNodes;
private LinkedNode<ContentNode> _root;
private readonly ConcurrentDictionary<int, LinkedNode<IPublishedContentType>> _contentTypesById;
// We must keep separate dictionaries for by id and by alias because we track these in snapshot/layers
// and it is possible that the alias of a content type can be different for the same id in another layer
// whereas the GUID -> INT cross reference can never be different
private readonly ConcurrentDictionary<int, LinkedNode<IPublishedContentType>> _contentTypesById;
private readonly ConcurrentDictionary<string, LinkedNode<IPublishedContentType>> _contentTypesByAlias;
private readonly ConcurrentDictionary<Guid, int> _xmap;
private readonly ConcurrentDictionary<Guid, int> _contentTypeKeyToIdMap;
private readonly ConcurrentDictionary<Guid, int> _contentKeyToIdMap;
private readonly ILogger _logger;
private BPlusTree<int, ContentNodeKit> _localDb;
@@ -73,7 +78,8 @@ namespace Umbraco.Web.PublishedCache.NuCache
_root = new LinkedNode<ContentNode>(new ContentNode(), 0);
_contentTypesById = new ConcurrentDictionary<int, LinkedNode<IPublishedContentType>>();
_contentTypesByAlias = new ConcurrentDictionary<string, LinkedNode<IPublishedContentType>>(StringComparer.InvariantCultureIgnoreCase);
_xmap = new ConcurrentDictionary<Guid, int>();
_contentTypeKeyToIdMap = new ConcurrentDictionary<Guid, int>();
_contentKeyToIdMap = new ConcurrentDictionary<Guid, int>();
_genObjs = new ConcurrentQueue<GenObj>();
_genObj = null; // no initial gen exists
@@ -136,7 +142,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
Monitor.Enter(_wlocko, ref lockInfo.Taken);
lock(_rlocko)
lock (_rlocko)
{
// see SnapDictionary
try { }
@@ -152,7 +158,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
_nextGen = true;
}
}
}
}
}
private void Release(WriteLockInfo lockInfo, bool commit = true)
@@ -291,8 +297,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
foreach (var type in types)
{
SetValueLocked(_contentTypesById, type.Id, type);
SetValueLocked(_contentTypesByAlias, type.Alias, type);
SetContentTypeLocked(type);
}
}
@@ -318,8 +323,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
foreach (var type in index.Values)
{
SetValueLocked(_contentTypesById, type.Id, type);
SetValueLocked(_contentTypesByAlias, type.Alias, type);
SetContentTypeLocked(type);
}
foreach (var link in _contentNodes.Values)
@@ -354,8 +358,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
// set all new content types
foreach (var type in types)
{
SetValueLocked(_contentTypesById, type.Id, type);
SetValueLocked(_contentTypesByAlias, type.Alias, type);
SetContentTypeLocked(type);
}
// beware! at that point the cache is inconsistent,
@@ -419,8 +422,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
// perform update of refreshed content types
foreach (var type in refreshedTypesA)
{
SetValueLocked(_contentTypesById, type.Id, type);
SetValueLocked(_contentTypesByAlias, type.Alias, type);
SetContentTypeLocked(type);
}
// perform update of content with refreshed content type - from the kits
@@ -638,7 +640,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
kit.Node.PreviousSiblingContentId = existing.PreviousSiblingContentId;
}
_xmap[kit.Node.Uid] = kit.Node.Id;
_contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id;
return true;
}
@@ -734,7 +736,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
// this node becomes the previous node
previousNode = thisNode;
_xmap[kit.Node.Uid] = kit.Node.Id;
_contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id;
}
return ok;
@@ -757,7 +759,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
EnsureLocked();
var ok = true;
ClearLocked(_contentNodes);
ClearRootLocked();
@@ -778,7 +780,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
if (_localDb != null) RegisterChange(kit.Node.Id, kit);
AddTreeNodeLocked(kit.Node, parent);
_xmap[kit.Node.Uid] = kit.Node.Id;
_contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id;
}
return ok;
@@ -807,7 +809,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
EnsureLocked();
var ok = true;
// get existing
_contentNodes.TryGetValue(rootContentId, out var link);
var existing = link?.Value;
@@ -833,7 +835,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
if (_localDb != null) RegisterChange(kit.Node.Id, kit);
AddTreeNodeLocked(kit.Node, parent);
_xmap[kit.Node.Uid] = kit.Node.Id;
_contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id;
}
return ok;
@@ -885,11 +887,11 @@ namespace Umbraco.Web.PublishedCache.NuCache
// This should never be null, all code that calls this method is null checking but we've seen
// issues of null ref exceptions in issue reports so we'll double check here
if (content == null) throw new ArgumentNullException(nameof(content));
SetValueLocked(_contentNodes, content.Id, null);
if (_localDb != null) RegisterChange(content.Id, ContentNodeKit.Null);
_xmap.TryRemove(content.Uid, out _);
_contentKeyToIdMap.TryRemove(content.Uid, out _);
var id = content.FirstChildContentId;
while (id > 0)
@@ -913,10 +915,10 @@ namespace Umbraco.Web.PublishedCache.NuCache
{
if (_contentNodes.TryGetValue(id, out var link))
{
link = GetLinkedNodeGen(link, gen);
link = GetLinkedNodeGen(link, gen);
if (link != null && link.Value != null)
return link;
}
}
throw new PanicException($"failed to get {description} with id={id}");
}
@@ -929,13 +931,13 @@ namespace Umbraco.Web.PublishedCache.NuCache
{
if (content.ParentContentId < 0)
{
var root = GetLinkedNodeGen(_root, gen);
var root = GetLinkedNodeGen(_root, gen);
return root;
}
if (_contentNodes.TryGetValue(content.ParentContentId, out var link))
link = GetLinkedNodeGen(link, gen);
return link;
}
@@ -1154,6 +1156,15 @@ namespace Umbraco.Web.PublishedCache.NuCache
}
}
private void SetContentTypeLocked(IPublishedContentType type)
{
SetValueLocked(_contentTypesById, type.Id, type);
SetValueLocked(_contentTypesByAlias, type.Alias, type);
// ensure the key/id map is accurate
if (type.TryGetKey(out var key))
_contentTypeKeyToIdMap[key] = type.Id;
}
// set a node (just the node, not the tree)
private void SetValueLocked<TKey, TValue>(ConcurrentDictionary<TKey, LinkedNode<TValue>> dict, TKey key, TValue value)
where TValue : class
@@ -1211,14 +1222,14 @@ namespace Umbraco.Web.PublishedCache.NuCache
public ContentNode Get(Guid uid, long gen)
{
return _xmap.TryGetValue(uid, out var id)
return _contentKeyToIdMap.TryGetValue(uid, out var id)
? GetValue(_contentNodes, id, gen)
: null;
}
public IEnumerable<ContentNode> GetAtRoot(long gen)
{
var root = GetLinkedNodeGen(_root, gen);
var root = GetLinkedNodeGen(_root, gen);
if (root == null)
yield break;
@@ -1274,13 +1285,20 @@ namespace Umbraco.Web.PublishedCache.NuCache
return GetValue(_contentTypesByAlias, alias, gen);
}
public IPublishedContentType GetContentType(Guid key, long gen)
{
if (!_contentTypeKeyToIdMap.TryGetValue(key, out var id))
return null;
return GetContentType(id, gen);
}
#endregion
#region Snapshots
public Snapshot CreateSnapshot()
{
lock(_rlocko)
lock (_rlocko)
{
// if no next generation is required, and we already have one,
// use it and create a new snapshot
@@ -1606,6 +1624,13 @@ namespace Umbraco.Web.PublishedCache.NuCache
return _store.GetContentType(alias, _gen);
}
public IPublishedContentType GetContentType(Guid key)
{
if (_gen < 0)
throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/);
return _store.GetContentType(key, _gen);
}
// this code is here just so you don't try to implement it
// the only way we can iterate over "all" without locking the entire cache forever
// is by shallow cloning the cache, which is quite expensive, so we should probably not do it,

View File

@@ -11,7 +11,7 @@ using Umbraco.Web.PublishedCache.NuCache.Navigable;
namespace Umbraco.Web.PublishedCache.NuCache
{
internal class MediaCache : PublishedCacheBase, IPublishedMediaCache, INavigableData, IDisposable
internal class MediaCache : PublishedCacheBase, IPublishedMediaCache2, INavigableData, IDisposable
{
private readonly ContentStore.Snapshot _snapshot;
private readonly IVariationContextAccessor _variationContextAccessor;
@@ -155,15 +155,11 @@ namespace Umbraco.Web.PublishedCache.NuCache
#region Content types
public override IPublishedContentType GetContentType(int id)
{
return _snapshot.GetContentType(id);
}
public override IPublishedContentType GetContentType(int id) => _snapshot.GetContentType(id);
public override IPublishedContentType GetContentType(string alias)
{
return _snapshot.GetContentType(alias);
}
public override IPublishedContentType GetContentType(string alias) => _snapshot.GetContentType(alias);
public override IPublishedContentType GetContentType(Guid key) => _snapshot.GetContentType(key);
#endregion

View File

@@ -8,7 +8,7 @@ using Umbraco.Core.Xml;
namespace Umbraco.Web.PublishedCache
{
abstract class PublishedCacheBase : IPublishedCache
internal abstract class PublishedCacheBase : IPublishedCache2
{
public bool PreviewDefault { get; }
@@ -89,8 +89,8 @@ namespace Umbraco.Web.PublishedCache
}
public abstract IPublishedContentType GetContentType(int id);
public abstract IPublishedContentType GetContentType(string alias);
public abstract IPublishedContentType GetContentType(Guid key);
public virtual IEnumerable<IPublishedContent> GetByContentType(IPublishedContentType contentType)
{

View File

@@ -15,8 +15,10 @@ namespace Umbraco.Web.PublishedCache
/// <remarks>This cache is not snapshotted, so it refreshes any time things change.</remarks>
public class PublishedContentTypeCache
{
// NOTE: These are not concurrent dictionaries because all access is done within a lock
private readonly Dictionary<string, IPublishedContentType> _typesByAlias = new Dictionary<string, IPublishedContentType>();
private readonly Dictionary<int, IPublishedContentType> _typesById = new Dictionary<int, IPublishedContentType>();
private readonly Dictionary<Guid, int> _keyToIdMap = new Dictionary<Guid, int>();
private readonly IContentTypeService _contentTypeService;
private readonly IMediaTypeService _mediaTypeService;
private readonly IMemberTypeService _memberTypeService;
@@ -130,6 +132,42 @@ namespace Umbraco.Web.PublishedCache
}
}
/// <summary>
/// Gets a published content type.
/// </summary>
/// <param name="itemType">An item type.</param>
/// <param name="key">An key.</param>
/// <returns>The published content type corresponding to the item key.</returns>
public IPublishedContentType Get(PublishedItemType itemType, Guid key)
{
try
{
_lock.EnterUpgradeableReadLock();
if (_keyToIdMap.TryGetValue(key, out var id))
return Get(itemType, id);
var type = CreatePublishedContentType(itemType, key);
try
{
_lock.EnterWriteLock();
_keyToIdMap[key] = type.Id;
return _typesByAlias[GetAliasKey(type)] = _typesById[type.Id] = type;
}
finally
{
if (_lock.IsWriteLockHeld)
_lock.ExitWriteLock();
}
}
finally
{
if (_lock.IsUpgradeableReadLockHeld)
_lock.ExitUpgradeableReadLock();
}
}
/// <summary>
/// Gets a published content type.
/// </summary>
@@ -152,7 +190,8 @@ namespace Umbraco.Web.PublishedCache
try
{
_lock.EnterWriteLock();
if (type.TryGetKey(out var key))
_keyToIdMap[key] = type.Id;
return _typesByAlias[aliasKey] = _typesById[type.Id] = type;
}
finally
@@ -188,7 +227,8 @@ namespace Umbraco.Web.PublishedCache
try
{
_lock.EnterWriteLock();
if (type.TryGetKey(out var key))
_keyToIdMap[key] = type.Id;
return _typesByAlias[GetAliasKey(type)] = _typesById[type.Id] = type;
}
finally
@@ -204,27 +244,32 @@ namespace Umbraco.Web.PublishedCache
}
}
private IPublishedContentType CreatePublishedContentType(PublishedItemType itemType, Guid key)
{
IContentTypeComposition contentType = itemType switch
{
PublishedItemType.Content => _contentTypeService.Get(key),
PublishedItemType.Media => _mediaTypeService.Get(key),
PublishedItemType.Member => _memberTypeService.Get(key),
_ => throw new ArgumentOutOfRangeException(nameof(itemType)),
};
if (contentType == null)
throw new Exception($"ContentTypeService failed to find a {itemType.ToString().ToLower()} type with key \"{key}\".");
return _publishedContentTypeFactory.CreateContentType(contentType);
}
private IPublishedContentType CreatePublishedContentType(PublishedItemType itemType, string alias)
{
if (GetPublishedContentTypeByAlias != null)
return GetPublishedContentTypeByAlias(alias);
IContentTypeComposition contentType;
switch (itemType)
IContentTypeComposition contentType = itemType switch
{
case PublishedItemType.Content:
contentType = _contentTypeService.Get(alias);
break;
case PublishedItemType.Media:
contentType = _mediaTypeService.Get(alias);
break;
case PublishedItemType.Member:
contentType = _memberTypeService.Get(alias);
break;
default:
throw new ArgumentOutOfRangeException(nameof(itemType));
}
PublishedItemType.Content => _contentTypeService.Get(alias),
PublishedItemType.Media => _mediaTypeService.Get(alias),
PublishedItemType.Member => _memberTypeService.Get(alias),
_ => throw new ArgumentOutOfRangeException(nameof(itemType)),
};
if (contentType == null)
throw new Exception($"ContentTypeService failed to find a {itemType.ToString().ToLower()} type with alias \"{alias}\".");
@@ -235,23 +280,13 @@ namespace Umbraco.Web.PublishedCache
{
if (GetPublishedContentTypeById != null)
return GetPublishedContentTypeById(id);
IContentTypeComposition contentType;
switch (itemType)
IContentTypeComposition contentType = itemType switch
{
case PublishedItemType.Content:
contentType = _contentTypeService.Get(id);
break;
case PublishedItemType.Media:
contentType = _mediaTypeService.Get(id);
break;
case PublishedItemType.Member:
contentType = _memberTypeService.Get(id);
break;
default:
throw new ArgumentOutOfRangeException(nameof(itemType));
}
PublishedItemType.Content => _contentTypeService.Get(id),
PublishedItemType.Media => _mediaTypeService.Get(id),
PublishedItemType.Member => _memberTypeService.Get(id),
_ => throw new ArgumentOutOfRangeException(nameof(itemType)),
};
if (contentType == null)
throw new Exception($"ContentTypeService failed to find a {itemType.ToString().ToLower()} type with id {id}.");
@@ -259,6 +294,7 @@ namespace Umbraco.Web.PublishedCache
}
// for unit tests - changing the callback must reset the cache obviously
// TODO: Why does this even exist? For testing you'd pass in a mocked service to get by id
private Func<string, IPublishedContentType> _getPublishedContentTypeByAlias;
internal Func<string, IPublishedContentType> GetPublishedContentTypeByAlias
{
@@ -282,6 +318,7 @@ namespace Umbraco.Web.PublishedCache
}
// for unit tests - changing the callback must reset the cache obviously
// TODO: Why does this even exist? For testing you'd pass in a mocked service to get by id
private Func<int, IPublishedContentType> _getPublishedContentTypeById;
internal Func<int, IPublishedContentType> GetPublishedContentTypeById
{