Replies: 10 comments 1 reply
-
Hi @b-twis , sorry for the delay but the last couple of days have been quote busy, with the release of v0.9 and everything. |
Beta Was this translation helpful? Give feedback.
-
Hi @jodydonetti, I did some thinking and trials and create a small plugin which illustrates a possible solution using a ConcurrentDictionary to track currently cached values and KeyGroups to track the relations. I think this approach simulates the same process as the Token based expiry and allows for both of my above situations. My approach below requires calling .Remove on each related key, rather than expiring a single token for each CacheGroup. I am not sure if the expiry token option is viable within a plugin as I would need to intercept the Set of a cache value received by the backplane and inject in the relevant expiry token. using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using ZiggyCreatures.Caching.Fusion;
using ZiggyCreatures.Caching.Fusion.Events;
using ZiggyCreatures.Caching.Fusion.Plugins;
namespace Core.Caching
{
public class KeyGroup
{
public string GroupName { get; }
/// <summary>
/// List of known static keys that do not require pattern matching
/// </summary>
public List<string> StaticKeysNames { get; }
/// <summary>
/// List of patterns to match dynamic keys
/// </summary>
public List<string> KeyPatterns { get; }
private List<Regex> _regexList { get; } = new List<Regex>();
public KeyGroup(string groupName, List<string> staticKeysNames, List<string> keyPatterns)
{
GroupName = groupName;
StaticKeysNames = staticKeysNames;
KeyPatterns = keyPatterns;
foreach (string keyPattern in keyPatterns)
{
_regexList.Add(new Regex(keyPattern,
RegexOptions.Singleline | RegexOptions.Compiled));
}
}
/// <summary>
/// Check the supplied key against the StaticKeys or KeyPatterns
/// </summary>
/// <param name="key">key to check</param>
/// <returns>true: key matches one of the StaticKeys or KeyPatterns, false: key does not match any StaticKeys or KeyPatterns</returns>
public bool IsMatch(string key)
{
if (StaticKeysNames.Contains(key))
{
return true;
}
else
{
foreach (Regex regex in _regexList)
{
if (regex.IsMatch(key))
{
return true;
}
}
}
return false;
}
}
public class CacheGroupPlugin : IFusionCachePlugin
{
/// <summary>
/// List of Key Groups to support local tracking operations.
/// </summary>
private readonly List<KeyGroup> _keyGroups;
/// <summary>
/// List of keys that are locally tracked in the FusionCache.
/// </summary>
private readonly ConcurrentDictionary<string, int> _cachedKeys = new ConcurrentDictionary<string, int>(10,100);
private int OneSetCounter = 0;
private int OneRemoveCounter = 0;
private int OnBackplaneMessageReceivedCounter = 0;
private int OnBackplaneMessageReceivedSetCounter = 0;
private int OnBackplaneMessageReceivedRemoveCounter = 0;
public CacheGroupPlugin(List<KeyGroup> keyGroups)
{
_keyGroups = keyGroups;
}
public void Start(IFusionCache cache)
{
IsStarted = true;
cache.Events.Set += OnSet;
cache.Events.Remove += OnRemove;
cache.Events.Backplane.MessageReceived += OnBackplaneMessageReceived;
}
public void Stop(IFusionCache cache)
{
IsStopped = true;
cache.Events.Set -= OnSet;
cache.Events.Remove -= OnRemove;
cache.Events.Backplane.MessageReceived -= OnBackplaneMessageReceived;
}
/// <summary>
/// Every time a cache value is set, keep track of the key.
/// </summary>
/// <param name="sender">FusionCache instance handling the event</param>
/// <param name="e"></param>
private void OnSet(object? sender, FusionCacheEntryEventArgs e)
{
OneSetCounter++;
if (sender is IFusionCache cache)
{
_cachedKeys.TryAdd(e.Key, 0);
}
}
/// <summary>
/// On Cache.Remove, check for related keys in the group and also remove them
/// </summary>
/// <param name="sender">FusionCache instance handling the event</param>
/// <param name="e"></param>
private void OnRemove(object? sender, FusionCacheEntryEventArgs e)
{
OneRemoveCounter++;
if (sender is IFusionCache cache)
{
RemoveCacheEntry(cache, e.Key, true);
}
}
/// <summary>
/// On Backplane message recieve. Set: Add the new cache key to the local tracked keys, Remove: Remove self and related Keys without triggering backplane notification.
/// </summary>
/// <param name="sender">FusionCache instance handling the event</param>
/// <param name="e"></param>
private void OnBackplaneMessageReceived(object? sender, FusionCacheBackplaneMessageEventArgs e)
{
OnBackplaneMessageReceivedCounter++;
if (sender is IFusionCache cache)
{
if (e.Message.Action == ZiggyCreatures.Caching.Fusion.Backplane.BackplaneMessageAction.EntrySet)
{
OnBackplaneMessageReceivedSetCounter++;
// When new cache entry is created, add to local tracking dictionary
_cachedKeys.TryAdd(e.Message.CacheKey, 0);
}
else if (e.Message.Action == ZiggyCreatures.Caching.Fusion.Backplane.BackplaneMessageAction.EntryRemove)
{
OnBackplaneMessageReceivedRemoveCounter++;
RemoveCacheEntry(cache, e.Message.CacheKey, false);
}
}
}
private void RemoveCacheEntry(IFusionCache cache,string key, bool triggerBackplaneNotification)
{
if (_cachedKeys.TryRemove(key, out _))
{
// Remove the current key from the cache
cache.Remove(key, new FusionCacheEntryOptions() { EnableBackplaneNotifications = triggerBackplaneNotification });
// Find all related keys, across all groups
var relatedKeys = GetRelatedKeys(key);
foreach (var relatedKey in relatedKeys)
{
// Remove the key from the local tracking dictionary as to avoid recursive events
if (_cachedKeys.TryRemove(relatedKey, out _))
{
// Remove the cache value, this triggers backplane notifications
cache.Remove(relatedKey, new FusionCacheEntryOptions() { EnableBackplaneNotifications = triggerBackplaneNotification });
}
}
}
}
public bool IsStarted { get; private set; }
public bool IsStopped { get; private set; }
/// <summary>
/// Locate keys related to the current supplied value.
/// </summary>
/// <param name="key">key name</param>
/// <returns>IEnumerable<string> containing keys in the same group</returns>
private IEnumerable<string> GetRelatedKeys(string key)
{
List<string> keyList = new List<string>();
// Find groups this key belongs to
var keyGroups = _keyGroups.Where(x => x.IsMatch(key));
// find all keys in the groups
foreach (var group in keyGroups)
{
keyList.AddRange(_cachedKeys.Keys.Where(x => group.IsMatch(x)));
}
return keyList.Distinct();
}
}
} |
Beta Was this translation helpful? Give feedback.
-
@jodydonetti do you have some time to evaluate adding support for including ExpirationTokens in the MemoryCache layer? I would really like Cache dependencies in FusionCache. I know it is part fo the standard IMemoryCache interface and would be fairly easy to expose an additional property on the FusionCacheEntryOptions. My only hesitation on this is the impact on things like backplane and distributed cache. If my understanding of the concepts are correct, then the ExpirationTokens are maintained outside of the FusionCache, And these would be unique instance per node. I could see the following issues/concerns
I would expect that we could bind into the RegisterPostEvictionCallback just like we do for the Events in FusionCache. If this is not the case, and simple adding the ability for the ExpirationToken in teh cache entry to be passed through, I would be happy to do a PR for this. Thanks, Basil |
Beta Was this translation helpful? Give feedback.
-
Hi @jodydonetti I would love to make some progress on this request. So I did some further investigation of the code and found that this could be fairly straight forward to actually implement. Adding a new property to FusionCacheEntryOptions to hold the ExpirationTokens public IList<IChangeToken> ExpirationTokens { get; } = new List<IChangeToken>(); and update the ToMemoryCacheEntryOptions method adding // This will add the Expiry Tokens assigned to FusionCacheEntryOptions onto the MemoryCacheEntryOptions
foreach(var token in ExpirationTokens)
{
res.AddExpirationToken(token);
} I checked out the backplane setup and can see that when either a SET or REMOVE notification is received, it just triggers an Evict of local cache. So my previous comments about "Create/Set via the backplane" were not relevant. Still, the issue of distributed cache eviction and backplane notification still stands. Does it seem viable to add an additional RegisterPostEvictionCallback which would call FusionCache.RemoveAsync to handle the distribued cache and backplane? Only real concern is crossing some boundries, ie passing down the FuctionCache (similar to FusionCacheMemoryEventsHub) into the MemoryCacheAccessor class in order to register a correct callback. An alternative is to just use a plugin that listens to the Eviction events and calls Remove on FusionCache. public class CacheExpiryTokenPlugin : IFusionCachePlugin
{
public CacheExpiryTokenPlugin()
{
}
public void Start(IFusionCache cache)
{
cache.Events.Memory.Eviction += OnEvict;
}
public void Stop(IFusionCache cache)
{
cache.Events.Memory.Eviction -= OnEvict;
}
private void OnEvict(object? sender, FusionCacheEntryEvictionEventArgs e)
{
if (e.Reason == Microsoft.Extensions.Caching.Memory.EvictionReason.TokenExpired)
{
if (sender is IFusionCache cache)
{
cache.Remove(e.Key);
}
}
}
} What are your thoughts here? |
Beta Was this translation helpful? Give feedback.
-
@jodydonetti I have opened up PR #112 with the first option of adding an event to Memory.Eviction for handling the manual cancellation of the token. Could you please take a look and let me know what you think. |
Beta Was this translation helpful? Give feedback.
-
Hi @b-twis , sorry if I haven't answered you before, I somehow lost myself in other issues and stuff and time passed. Anyway, straight to the cache entries cleanup issue: it simply cannot work, or at least not in a reasonable way, I think. Region Cleanup (or similar)Let me explain: if you only consider the case of one node with a local memory cache then yes, it may work. But as soon as you consider any other common scenarios for FusionCache, like:
this approach unfortunately breaks. A Practical ExampleA practical example: imagine a node that is running from sometime and have populated the cache with some entries. What happens when it restarts, and soon after it receives a call to Or let's say that on a node there are 3 cache entries with prefix You may think you've expired the data, but in reality it will still be there. All of this because expiration tokens are only a local, in-memory concept: there's no shared state between multiple nodes and/or with the distributed cache. AlternativesOn the other hand the approach I've highlighted on this answer (key-based cache expiration) may be a valid alternative: as I've said there, in some cases I had similar needs, and I've solved them with that approach, even with quite a big amount of data and a ton of requests (just don't set your expirations to something very very high, otherwise it would take a lot of time for them to expire from the cache(s)). On the other hand maybe I'm missing something from your reasonings: in that case please let me know! I'd be happy to know more and have a discussion. Oh, and againI'm repeating myself, but really: sorry again for the long time without a proper answer. I haven't ignored you on purpose, it's just that in the last year I worked on some planned big features for FusionCache, and from design to development, from testing to performance tuning they took a lot of time. Hope this helps, let me know what you think! |
Beta Was this translation helpful? Give feedback.
-
UPDATE: it is happening 🥳 Any help would be appreciated! |
Beta Was this translation helpful? Give feedback.
-
Hi all, v2.0.0-preview-1 is out 🥳 🙏 Please, if you can try it out and let me know what you think, how it feels to use it or anything else really: your contribution is essential, thanks! |
Beta Was this translation helpful? Give feedback.
-
Hi all, I just published a dedicated issue for the Clear() feature. It contains some details about the mechanics behind it, the design of it, performance considerations and more. Hope this helps. |
Beta Was this translation helpful? Give feedback.
-
First off, the library looks great, especially with the introduction of the backplane!
This may be an extension to the ongoing cache region discussion, but I think deems it's own topic.
I am working on a PoC utilising this library and came across a gap compared to our current implementation on top of LazyCache.
It is the equivalent of a Clear mechanism affecting all or part of the cache.
MemoryCacheEntryOptions of IMemoryCache supports entries with attached ExpiryTokens. Each entry can have multiple tokens assigned to it and when the token is cancelled, all associated cache values are expired.
Currently we track the issued tokens in a ConcurrentDictionary keyed on a string.
This allows for us to do 2 things
The primary reason for this approach was to solve cache invalidation across services.
ServiceA caches
ServiceB caches
Now if I update data in ServiceB, I raise an event (Rest API, or Message Queue) that ServiceA listens to and thus calls .Clear("ServiceB"). (note only one listener responds on ServiceA due to load balancing).
The second part of this implementation is segmentation/relations inside of ServiceA
Where CacheKey2 & CacheKey3 data is built on top of CacheKey1 data. (I understand this may not be the 'right' way but it addresses our situation, where cacheKey1 is computationally intensive as is CacheKey2 & CaheKey3)
cacheKey1 = "baseCacheData" eg "template"
cacheKey2 = "UserID:baseCacheData+UserSpecificEnhancements" eg "123:template:nhanced"
cacheKey3 = "UserID:baseCacheData+UserSpecificEnhancements" eg "234:templateEnhanced"
Now if I change cacheKey1 it should invalidate cacheKey2 and cacheKey3.
Currently I would use a cacheGroup token to relate these cache entries together, and expire cacheKey1 before setting a new value for it.
This approach also supports nested relations like
"{GroupID}:{UserID}:Data"
Each entry has 2 change tokens, 1 for the group and 1 for the user.
With this I can invalidate a variety of data subsets, either on user change to invalidate across all groups, or on group change to invalidate across all users.
I am not sure of the side effects of this in FusionCache, especially with Background and Backplane support.
I would happily keep the the .Clear() functionality and tracking of ExpiryTokens outside of FuctionCache, but would be great if I could easily add the token when creating an entry.
Would be great to discuss this possibility.
Beta Was this translation helpful? Give feedback.
All reactions