Skip to Main Content
Customer Feedback

We love feedback from you on our products and the problems in your daily work that you would like us to solve. Please describe the challenge you're encountering and your desired outcome. Be as detailed as possible.

For technical issues or bugs please head to Support or our Developer Community. You can assign up to 20 votes in total. Thank you for your feedback.

Status explanation: 'Future Consideration' = Continuing to collect further feedback, not planned at this time. 'Investigating' = Prioritized for deeper customer and feasibility investigations ahead of planning development.

Status Already exists
Categories Technical
Created by Guest
Created on May 13, 2022

CacheEvictionPolicy with masterkey dependency to content version makes edit mode broken for that IContent

To make a simple showcase, imagine the following code for caching a friendly URL for a page.

CacheManager.Insert
($"GetFriendlyUrl({contentLink.ID})",
contentLink.GetFriendlyURL(),
new CacheEvictionPolicy
(TimeSpan.FromDays(1),
CacheTimeoutType.Absolute,
null,
new string[] { GetVersionKey(contentLink)})
);

Where we have a helper method:

public static string GetVersionKey(this ContentReference contentLink)
{
var _contentVersionRepository = ServiceLocator.Current.GetInstance<IContentVersionRepository>();
var _cacheKeyCreator = ServiceLocator.Current.GetInstance<IContentCacheKeyCreator>();

var versionCacheKey = string.Empty;
var latestPublishedVersion = _contentVersionRepository
.List(contentLink)
.ToList()
.FirstOrDefault(v => v.Status == VersionStatus.Published);

if (latestPublishedVersion != null)
{
versionCacheKey = _cacheKeyCreator.CreateVersionCacheKey(latestPublishedVersion.ContentLink);
}

return versionCacheKey;
}

If this VersionKey is not in the cache, the code above will store a null object for that masterkey.

The side effect is when trying to edit this page/IContent the frontend hangs because of expected json is 500 error html page, instead of expected json result.

epi.js:2 SyntaxError: Unexpected token '<'
at Object.dojo.fromJson (dojo.js:15:82234)
at Object.json (dojo.js:15:120242)
at _497 (dojo.js:15:123066)
at _2cd (dojo.js:15:73012)
at _2cb (dojo.js:15:72704)
at dojo.Deferred.resolve.callback (dojo.js:15:73525)
at dojo.js:15:125114
at _2f2 (dojo.js:15:75368)
at _2ec (dojo.js:15:75232)
at _2f1.resolve (dojo.js:15:76693)

Which in turn is because of CmsContentContextResolver.GetCommonDraft => ContentProvider.LoadContentFromCacheOrRepository => ... => ObjectInstanceCacheExtensions.TryGetWithWait is expecting an cached item of key EPContentVersion:29154_797799 being IContent or not existing in cache at all. But the null object is considered an System.Object.

System.InvalidOperationException: Unknown object of type System.Object, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 found with cache key EPContentVersion:29154_797799. Expected type EPiServer.Core.IContent, EPiServer, Version=11.20.11.0, Culture=neutral, PublicKeyToken=8fe83dea738b45b7
at EPiServer.Framework.Cache.ObjectInstanceCacheExtensions.TryGetWithWait[T](IObjectInstanceCache cache, String cacheKey, T& instance)
at EPiServer.Framework.Cache.ObjectInstanceCacheExtensions.ReadThroughWithWait[T](IObjectInstanceCache cache, String cacheKey, Func`1 readValue, Func`2 evictionPolicy)
at EPiServer.Core.Internal.ContentInstanceCache.ReadThrough(ContentReference contentLink, String selectedLanguage, Func`1 readValue)
at EPiServer.Core.ContentProvider.LoadContentFromCacheOrRepository(ContentReference contentreference, ILanguageSelector selector)
at EPiServer.Core.Internal.ProviderPipelineImplementation.GetItem(ContentProvider provider, ContentReference contentLink, LoaderOptions loaderOptions)
at EPiServer.Core.Internal.DefaultContentLoader.TryGet[T](ContentReference contentLink, LoaderOptions loaderOptions, T& content)
at EPiServer.Core.Internal.DefaultContentLoader.Get[T](ContentReference contentLink, LoaderOptions loaderOptions)
at EPiServer.Cms.Shell.UI.Rest.Internal.CmsContentContextResolver.GetCommonDraft(ContentReference contentReference, String language, ContentLanguageInformation& languageInformation)
at EPiServer.Cms.Shell.UI.Rest.Internal.CmsContentContextResolver.TryResolveUri(Uri uri, ClientContextBase& instance)
at EPiServer.Shell.UI.Rest.ContextStore.ResolveUri(Uri uri)
at lambda_method(Closure , ControllerBase , Object[] )

One would think that a TryGetWithWait wouldn't throw an exception like this, the try-part kind of indicates that it's error handled internally, this leads me to think there is an bug in the implementation, that some cases aren't handled correctly.

But, one could also argue that adding a masterkey dependency would mean that there would be a cascade-delete when the masterkey is removed from cache, regaradless if the masterkey was in cache or not. One would think of this as something that is called when changing content version, like publishing a new version etc., this code would indicate that this cache item should be deleted. But that's maybe a bad assumption?

We have found workarounds for now, like not adding masterkey dependency if there is no cached item for that masterkey, and instead caching a shorter time without masterkey dependency.


CMS 11.20.14.0 and .NET 4.7.2

  • Guest
    Reply
    |
    Aug 5, 2022

    Hi Cindy, and Henrik!

    Thanks for the response.

    I feel that there is missing some clarification about this in the documentation, at https://docs.developers.optimizely.com/content-cloud/v12.0.0-content-cloud/docs/object-caching there could be a complementary section, although the last section of Master keys indicates this explanation a bit, it would be nice with some section with a new header, describing how to cache custom stuff with content link dependency, and what to absolutely not do. ;)

  • Guest
    Reply
    |
    Jul 29, 2022

    Hi Jonas,
    I asked one of CMS engineers to look into this and received the following response (they can't seem to post public comments, hence me passing it down).

    "Hi Jonas,

    I think that there has been a slight misunderstanding on what the master keys in the CacheEvictionPolicy are and how to use the IContentCacheKeyCreator to depend on a content cache entry. Obviously it's never good when something results in an unhandled exception, but I think that we should try to solve your problem first.

    First of all we should establish that when something is inserted into the cache with cache dependency keys, it will be removed when any of the keys does not exist in the cache. This means that it will removed with the dependent item is removed, but it also means that it will be removed immediately if it doesn't exist at the time of the insert.

    Another thing we should note is that the underlying HttpRuntime Cache (in CMS 11) only has one type of cache dependency keys, while the CMS Cache has two types of dependency keys. CacheKeys works just as the normal ones, while MasterKeys has a slightly different behavior. When inserting something with a master key dependency, the cache will make sure that this key exists in the cache, and if it doesn't a dummy object will be inserted with the master key with no expiration. This feature was introduced to support clearing the cache of a certain class of items, e.g. "clear the content type cache". Cache keys that represents actual items should never be used as MasterKeys.

    So in your case when you are inserting your friendly URL with a master key that matches a content cache key, and that item does not already exist in the cache, you will be inserting a dummy object in the cache that will essentially prevent that content item from loading properly.

    So I would suggest that you instead use the version key as a normal dependency key. And if you want to have the ability to clear all the friendly urls from the cache you can provide a static master key dependency.

    There should also not be any need for your GetVersionKey helper method. You can just use the IContentCacheKeyCreator.CreateCommonCacheKey to get the shared key for the published version, or in your case since the friendly URL will be language specific, you could probably use IContentCacheKeyCreator.CreateLanguageCacheKey.

    Your code should therefore look something like:

    CacheManager.Insert
    ($"GetFriendlyUrl({contentLink.ID})",
    contentLink
    .GetFriendlyURL(),
    new CacheEvictionPolicy
    (TimeSpan.FromDays(1),
    CacheTimeoutType
    .Absolute,
    new string[] { _cacheKeyCreator.CreateLanguageCacheKey(contentLink, "en")},
    new string[] { "FriendlyUrlMasterKey" })
    );

    "

    He also mentioned that if you want to discuss this further, you can reach him on Slack. It is Henrik Nyström. :)

  • Guest
    Reply
    |
    Jul 15, 2022

    Complementary info

    This example is pretty real world. It was on article pages, and the 5 latest article siblings where presented in a side menu.

    When edited pages that where in the menu on some sibling, like visit some article, click on another and the enter edit mode, these article pages didn’t enter edit mode, the whole edit pane was broken.

    If I remember correctly pages not in menu was able to edit, no broken edit mode.