2

I have a method which generates our cache key which is a string like below:

private static readonly HashSet<string> CacheKeys = new HashSet<string>();

public string Generate(int clientId, int processId)
{
    StringBuilder sb = new StringBuilder();
    sb.Append("data:").Append("c:").Append(clientId).Append("::");
    sb.Append("pi::").Append(processId);

    // do I need to store everything in my CacheKeys?
    // or this can be improved so that I can fullfill below 3 requirements
    // CacheKeys.Add(sb.ToString());
    
    return sb.ToString();
}

And then this string gets cached in MemoryCache as a key along with their value (not shown here as it is not relevant).

_memoryCache.Set<T>(cacheKey, value, memoryCacheOptions);

I have a requirement where I need to delete a specific key or keys from the memory cache depending on below requirement-

  • I need to delete specific processId + clientId key from the memory cache.
  • I need to delete all keys matching specific clientId key from the memory cache regardless of processId.
  • I also need to delete all the keys matching a particular processId key from the memory cache regardless of clientId.

Now after reading on SO and MemoryCache tutorial, it looks like there is no way to iterate over all the keys (or find keys matching particular regex pattern) and remove it from the cache.

Problem Statement

After going through various link it looks like I need to store my cache keys in separate data structure and then use that to remove it.

  • How should I store my cacheKey in such a way in the above method so that I can remove a key basis on above 3 requirements easily by iterating this data structure or any thread safe data structure?
  • Also the input structure which specifies what keys to remove (following above 3 pattern) is coming from some other service and that is not defined as well and I am pretty much open to represent in any format which can help me to do this task efficiently.

Basically I am trying to come up with an example for all my above three patterns by which I can remove the keys efficiently from MemoryCache which uses RemoveByPattern or Remove method. As of now I am not able to figure out on how to prepare an example or test cases for all those three cases which can use my RemoveByPattern or Remove method and clear the keys efficiently.

public void RemoveByPattern(MemoryCache memoryCache, string pattern)
{
    var keysToRemove = CacheKeys
        .Where(k => Regex.IsMatch(k, pattern, RegexOptions.IgnoreCase))
        .ToArray();

    foreach (var ktr in keysToRemove)
        Remove(memoryCache, ktr);
}

public void Remove(MemoryCache memoryCache, string key)
{
    memoryCache.Remove(key);
    CacheKeys.Remove(key);
}

How can I represent my input structure in such a way which can understand my above three requirements and then feed it to RemoveByPattern or Remove method easily?

user1950349
  • 4,738
  • 19
  • 67
  • 119

1 Answers1

1

I think you are on the right track. The goal is to make keys in CacheKeys consistent with keys in MemoryCache.

Case 1: When adding a new key

You need to add newly created key to both MemoryCache and CacheKeys.

_memoryCache.Set<T>(cacheKey, value, memoryCacheOptions);
CacheKeys.Add(cacheKey);

Case 2: When removing an existing key

Your implementation is good enough.

Case 3: When a key is expired

When a key is expired from MemoryCache, the key set is inconsistent between MemoryCache and CacheKeys. You need to implement a scheduled task running periodically in background to check and clean up CacheKeys (MemoryCache is the source of truth).

private void ConsolidateKeys()
{
    var invalidKeys = new List<string>();
    foreach (var key in CacheKeys)
    {
        if (!_memoryCache.TryGetValue(key, out var _))
        {
            invalidKeys.Add(key);
        }
    }
    foreach (var key in invalidKeys)
    {
        CacheKeys.Remove(key);
    }
}

Note

You need to consider thread safety of accessing CacheKeys since this is a multithreading scenario. Regarding data structure of CacheKeys, ConcurrentDictionary<TKey, TValue> is recommended over HashSet<T>.


Update

To remove keys efficiently (assuming you mean speed), I will trade space for speed.

It is a bit complex, but you can define a set of processId named ProcessIdMap. The key is processId. The value is a set of clientId with the same processId when generating cache keys. ClientIdMap is similar. (pseudo code)

ProcessIdMap = Map<processId, Set<clientId>>
ClientIdMap = Map<clientId, Set<cacheKey>>

Case 1: when adding a new key

private static readonly HashSet<string> CacheKeys = new HashSet<string>();
private static readonly Dictionary<string, HashSet<string>> ProcessIdMap = new Dictionary<string, HashSet<string>>();
private static readonly Dictionary<string, HashSet<string>> ClientIdMap = new Dictionary<string, HashSet<string>>();

private string BuildCacheKey(int clientId, int processId)
{
    var sb = new StringBuilder();
    sb.Append("data:").Append("c:").Append(clientId).Append("::");
    sb.Append("pi::").Append(processId);
    return sb.ToString();
}

public string AddToCache(int clientId, int processId)
{
    // Populate processId map
    if (!ProcessIdMap.TryGetValue(processId, out var clientIdSet))
    {
        clientIdSet = new HashSet<string>();
    }
    clientIdSet.Add(clientId);

    // Populate clientId map
    if (!ClientIdMap.TryGetValue(clientId, out var processIdSet))
    {
        processIdSet = new HashSet<string>();
    }
    processIdSet.Add(processId);

    var cacheKey = BuildCacheKey(clientId, processId);
    
    // Populate cache key store
    CacheKeys.Add(cacheKey);

    // Populate memory cache
    _memoryCache.Set<T>(cacheKey, value, memoryCacheOptions);

    return cacheKey;
}

Case 2: when removing an existing key (RemoveClientId method is similar)

public void RemoveProcessId(string processId)
{
    if (ProcessIdMap.TryGetValue(processId, out var clientIds))
    {
        foreach (var clientId in clientIds)
        {
            var cacheKey = BuildCacheKey(clientId, processId);

            // Remove from cache key store
            CacheKeys.Remove(cacheKey);

            // Remove from memory cache
            _memoryCache.Remove(cacheKey);

            // Remove from clientId map
            if (ClientIdMap.TryGetValue(clientId, out var processIdSet))
            {
                processIdSet.Remove(processId);
            }
        }

        // Remove from processId map
        ProcessIdMap.Remove(processId);
    }
}
Han Zhao
  • 1,932
  • 7
  • 10
  • It's a good suggestion on using `ConcurrentDictionary`. I will look into that. – user1950349 Oct 11 '20 at 06:41
  • Thanks for your suggestion but my confusion is how I am gonna use my `RemoveByPattern` or `Remove` method to achieve those 3 cases I have with `CacheKeys` set data structure or `ConcurrentDictionary` data structure? Do you think you can provide an example which can demonstrate on how to use those methods efficiently to achieve those 3 scenarios? As of now I am not able to figure out on how to prepare an example or test cases for all those 3 cases which can use my `RemoveByPattern` or `Remove` method. – user1950349 Oct 11 '20 at 06:43
  • For example: How can I represent my input structure in such a way which can understand my above three requirements and then feed it to `RemoveByPattern` or `Remove` method easily. – user1950349 Oct 11 '20 at 06:56
  • You still around? Any thoughts on how to setup an example for those three patterns which can demonstrate on how to remove the keys efficiently? – user1950349 Oct 11 '20 at 16:54
  • @user1950349 - Post is updated. Basically, the performance (speed-wise) will improve with two additional maps. Hope it helps. – Han Zhao Oct 11 '20 at 17:34
  • Thanks a lot! Still trying to understand your suggestion. And yeah it is kinda complicated than I thought of earlier. In your example you suggested to use `ProcessIdMap` and `ClientIdMap` but I don't see they are being populated in your code or I missed something? – user1950349 Oct 11 '20 at 17:50
  • @user1950349 - I updated my post to make it clear. Populating happens when you call `AddToCache` method. The core idea is the same: no matter adding or removing cached item (along with cache key), `_memoryCache`, `CacheKeys`, `ProcessIdMap`, and `ClientIdMap` should be consistent. – Han Zhao Oct 11 '20 at 18:21