diff --git a/HzCache.Benchmarks/HzCache.Benchmarks.csproj b/HzCache.Benchmarks/HzCache.Benchmarks.csproj
index 02a5af4..e2d1630 100644
--- a/HzCache.Benchmarks/HzCache.Benchmarks.csproj
+++ b/HzCache.Benchmarks/HzCache.Benchmarks.csproj
@@ -15,7 +15,7 @@
-
+
diff --git a/HzCache.Benchmarks/Program.cs b/HzCache.Benchmarks/Program.cs
index a361ab6..bc696b4 100644
--- a/HzCache.Benchmarks/Program.cs
+++ b/HzCache.Benchmarks/Program.cs
@@ -2,7 +2,7 @@
using System.Runtime.Caching;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
-using hzcache;
+using HzCache;
using HzCache.Benchmarks;
BenchmarkRunner.Run();
diff --git a/HzCache.Benchmarks/WithRedisInvalidation.cs b/HzCache.Benchmarks/WithRedisInvalidation.cs
index f3e9afb..6492c16 100644
--- a/HzCache.Benchmarks/WithRedisInvalidation.cs
+++ b/HzCache.Benchmarks/WithRedisInvalidation.cs
@@ -1,8 +1,6 @@
using System.Collections.Concurrent;
using System.Runtime.Caching;
using BenchmarkDotNet.Attributes;
-using hzcache;
-using RedisBackplaneMemoryCache;
namespace HzCache.Benchmarks
{
@@ -10,7 +8,7 @@ namespace HzCache.Benchmarks
[MemoryDiagnoser]
public class WithRedisInvalidation
{
- private static readonly IDetailedHzCache _hzCache = new RedisBackplaneHzCache(new RedisBackplaneMemoryMemoryCacheOptions
+ private static readonly IDetailedHzCache _hzCache = new RedisBackedHzCache(new RedisBackedHzCacheOptions
{
applicationCachePrefix = "benchmark",
redisConnectionString = "localhost",
diff --git a/HzMemoryCache/HzCacheMemoryLocker.cs b/HzMemoryCache/HzCacheMemoryLocker.cs
index 4d43265..982ddb9 100644
--- a/HzMemoryCache/HzCacheMemoryLocker.cs
+++ b/HzMemoryCache/HzCacheMemoryLocker.cs
@@ -4,7 +4,7 @@
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
-namespace hzcache
+namespace HzCache
{
public class HzCacheMemoryLockerOptions
{
diff --git a/HzMemoryCache/HzMemoryCache.cs b/HzMemoryCache/HzMemoryCache.cs
index f0c4129..32a2eb6 100644
--- a/HzMemoryCache/HzMemoryCache.cs
+++ b/HzMemoryCache/HzMemoryCache.cs
@@ -12,15 +12,15 @@
using System.Threading.Tasks.Dataflow;
using Microsoft.Extensions.Logging;
-namespace hzcache
+namespace HzCache
{
///
/// Simple MemoryCache alternative. Basically a concurrent dictionary with expiration and cache value change
/// notifications.
///
- public class HzMemoryCache : IEnumerable>, IDisposable, IDetailedHzCache
+ public partial class HzMemoryCache : IEnumerable>, IDisposable, IDetailedHzCache
{
- private static readonly IPropagatorBlock> updateChecksumAndSerializeQueue = CreateBuffer(TimeSpan.FromMilliseconds(10), 100);
+ private static readonly IPropagatorBlock> updateChecksumAndSerializeQueue = CreateBuffer(TimeSpan.FromMilliseconds(35), 100);
private static readonly SemaphoreSlim globalStaticLock = new(1);
private readonly Timer cleanUpTimer;
private readonly ConcurrentDictionary dictionary = new();
@@ -42,6 +42,7 @@ public HzMemoryCache(HzCacheOptions? options = null)
}
public int Count => dictionary.Count;
+ public long SizeInBytes => dictionary.Values.Sum(ttlv => ttlv.sizeInBytes);
public void RemoveByPattern(string pattern, bool sendNotification = true)
@@ -135,7 +136,6 @@ public void Set(string key, T? value)
///
public void Set(string key, T? value, TimeSpan ttl)
{
- // Console.WriteLine($"[{options.instanceId}] Set {key} with TTL {ttl} and value {value}");
var v = new TTLValue(key, value, ttl, updateChecksumAndSerializeQueue, options.notificationType,
(tv, objectData) => NotifyItemChange(key, CacheItemChangeType.AddOrUpdate, tv, objectData));
dictionary[key] = v;
@@ -171,12 +171,13 @@ public void Set(string key, T? value, TimeSpan ttl)
ReleaseLock(factoryLock, "GET", key);
return value;
}
-
+
public IList GetOrSetBatch(IList keys, Func, List>> valueFactory)
{
return GetOrSetBatch(keys, valueFactory, options.defaultTTL);
}
- public IList GetOrSetBatch(IList keys, Func, List>> valueFactory, TimeSpan ttl)
+
+ public IList GetOrSetBatch(IList keys, Func, List>> valueFactory, TimeSpan ttl)
{
var cachedItems = keys.Select(key => new KeyValuePair(key, Get(key)));
var missingKeys = cachedItems.Where(kvp => IsNullOrDefault(kvp.Value)).Select(kvp => kvp.Key).ToList();
@@ -189,6 +190,7 @@ public IList GetOrSetBatch(IList keys, Func, List GetOrSetBatch(IList keys, Func, List v.Value is not null).Select(kv => kv.Value).ToList();
}
+
///
/// @see
///
@@ -211,14 +214,14 @@ public bool Remove(string key)
///
/// @see
///
- public bool Remove(string key, bool sendBackplaneNotification = true, Func? skipRemoveIfEqualFunc = null)
+ public bool Remove(string key, bool sendBackplaneNotification, Func? skipRemoveIfEqualFunc = null)
{
return RemoveItem(key, CacheItemChangeType.Remove, sendBackplaneNotification, skipRemoveIfEqualFunc);
}
public CacheStatistics GetStatistics()
{
- return new CacheStatistics {Counts = Count, SizeInBytes = 0};
+ return new CacheStatistics {Counts = Count, SizeInBytes = SizeInBytes};
}
///
@@ -301,7 +304,6 @@ private async Task ProcessExpiredEviction()
private bool RemoveItem(string key, CacheItemChangeType changeType, bool sendNotification, Func? areEqualFunc = null)
{
- // Console.WriteLine($"[{options.instanceId}] Delete {key}");
var result = !(!dictionary.TryGetValue(key, out TTLValue ttlValue) || (areEqualFunc != null && areEqualFunc.Invoke(ttlValue.checksum)));
if (result)
@@ -323,8 +325,7 @@ private bool RemoveItem(string key, CacheItemChangeType changeType, bool sendNot
private void NotifyItemChange(string key, CacheItemChangeType changeType, TTLValue ttlValue, byte[]? objectData = null, bool isPattern = false)
{
- // Console.WriteLine($"Publishing {changeType} for {key} and pattern {isPattern}");
- options.valueChangeListener.Invoke(key, changeType, ttlValue, objectData, isPattern);
+ options.valueChangeListener(key, changeType, ttlValue, objectData, isPattern);
}
public void SetRaw(string key, TTLValue value)
diff --git a/HzMemoryCache/HzMemoryCache.csproj b/HzMemoryCache/HzMemoryCache.csproj
index 4dc3266..ef635ab 100644
--- a/HzMemoryCache/HzMemoryCache.csproj
+++ b/HzMemoryCache/HzMemoryCache.csproj
@@ -16,7 +16,7 @@
True
true
latestmajor
- hzcache
+ HzCache
diff --git a/HzMemoryCache/HzMemoryCacheAsync.cs b/HzMemoryCache/HzMemoryCacheAsync.cs
new file mode 100644
index 0000000..564aa19
--- /dev/null
+++ b/HzMemoryCache/HzMemoryCacheAsync.cs
@@ -0,0 +1,170 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace HzCache
+{
+ public partial class HzMemoryCache
+ {
+ public Task SetAsync(string key, T? value)
+ {
+ return SetAsync(key, value, options.defaultTTL);
+ }
+
+ public Task SetAsync(string key, T? value, TimeSpan ttl)
+ {
+ Set(key, value, ttl);
+ return Task.CompletedTask;
+ }
+
+ public async Task GetOrSetAsync(string key, Func> valueFactory, TimeSpan ttl, long maxMsToWaitForFactory = 10000)
+ {
+ var value = Get(key);
+ if (!IsNullOrDefault(value))
+ {
+ return value;
+ }
+
+ options.logger?.LogDebug("Cache miss for key {Key}, calling value factory", key);
+
+ var factoryLock = memoryLocker.AcquireLock(options.applicationCachePrefix, options.instanceId, "GET", key, TimeSpan.FromMilliseconds(maxMsToWaitForFactory),
+ options.logger, CancellationToken.None);
+ if (factoryLock is null)
+ {
+ options.logger?.LogDebug("Could not acquire lock for key {Key}, returning default value", key);
+ return value;
+ }
+
+ value = await valueFactory(key);
+ var ttlValue = new TTLValue(key, value, ttl, updateChecksumAndSerializeQueue, options.notificationType, (tv, objectData) =>
+ {
+ NotifyItemChange(key, CacheItemChangeType.AddOrUpdate, tv, objectData);
+ });
+ dictionary[key] = ttlValue;
+ ReleaseLock(factoryLock, "GET", key);
+ return value;
+ }
+
+
+ public async Task> GetOrSetBatchAsync(IList keys, Func, Task>>> valueFactory)
+ {
+ return await GetOrSetBatchAsync(keys, valueFactory, options.defaultTTL);
+ }
+
+ public async Task> GetOrSetBatchAsync(IList keys, Func, Task>>> valueFactory, TimeSpan ttl)
+ {
+ var cachedItems = keys.Select(key => new KeyValuePair(key, Get(key)));
+ var missingKeys = cachedItems.Where(kvp => IsNullOrDefault(kvp.Value)).Select(kvp => kvp.Key).ToList();
+ var factoryRetrievedItems = (await valueFactory(missingKeys)).ToDictionary(kv => kv.Key, kv => kv.Value);
+
+ return cachedItems.Select(kv =>
+ {
+ T? value = default;
+ if (kv.Value != null)
+ {
+ value = kv.Value;
+ }
+
+ if (kv.Value == null)
+ {
+ factoryRetrievedItems.TryGetValue(kv.Key, out value);
+ Set(kv.Key, value, ttl);
+ }
+
+ return new KeyValuePair(kv.Key, value);
+ }).Where(v => v.Value is not null).Select(kv => kv.Value).ToList();
+ }
+
+ public async Task ClearAsync()
+ {
+ var kvps = dictionary.ToArray();
+ dictionary.Clear();
+ foreach (var kv in kvps)
+ {
+ NotifyItemChange("*", CacheItemChangeType.Remove, null, null, true);
+ }
+ }
+
+ public async Task RemoveAsync(string key)
+ {
+ return await RemoveAsync(key, options.notificationType != NotificationType.None);
+ }
+
+ public async Task RemoveByPatternAsync(string pattern, bool sendNotification = true)
+ {
+ var myPattern = pattern;
+ if (pattern[0] != '*')
+ {
+ myPattern = "^" + pattern;
+ }
+
+ var re = new Regex(myPattern.Replace("*", ".*"));
+ var victims = dictionary.Keys.Where(k => re.IsMatch(k)).ToList();
+ victims.AsParallel().ForAll(async key =>
+ {
+ await RemoveItemAsync(key, CacheItemChangeType.Remove, false);
+ });
+ if (sendNotification)
+ {
+ NotifyItemChange(pattern, CacheItemChangeType.Remove, null, null, true);
+ }
+ }
+
+ public async Task GetAsync(string key)
+ {
+ var defaultValue = default(T);
+
+ if (!dictionary.TryGetValue(key, out var ttlValue))
+ {
+ return defaultValue;
+ }
+
+ if (ttlValue.IsExpired()) //found but expired
+ {
+ return defaultValue;
+ }
+
+ if (options.evictionPolicy == EvictionPolicy.LRU)
+ {
+ ttlValue.UpdateTimeToKill();
+ }
+
+ if (ttlValue.value is T o)
+ {
+ return o;
+ }
+
+ return default;
+ }
+
+ public async Task RemoveAsync(string key, bool sendBackplaneNotification = true, Func? skipRemoveIfEqualFunc = null)
+ {
+ return await RemoveItemAsync(key, CacheItemChangeType.Remove, sendBackplaneNotification, skipRemoveIfEqualFunc);
+ }
+
+ private async Task RemoveItemAsync(string key, CacheItemChangeType changeType, bool sendNotification, Func? areEqualFunc = null)
+ {
+ var result = !(!dictionary.TryGetValue(key, out var ttlValue) || (areEqualFunc != null && areEqualFunc.Invoke(ttlValue.checksum)));
+
+ if (result)
+ {
+ result = dictionary.TryRemove(key, out ttlValue);
+ if (result)
+ {
+ result = !ttlValue.IsExpired();
+ }
+ }
+
+ if (sendNotification)
+ {
+ NotifyItemChange(key, changeType, ttlValue);
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/HzMemoryCache/IHzCache.cs b/HzMemoryCache/IHzCache.cs
index 10e5c9e..26901bc 100644
--- a/HzMemoryCache/IHzCache.cs
+++ b/HzMemoryCache/IHzCache.cs
@@ -1,9 +1,10 @@
#nullable enable
using System;
using System.Collections.Generic;
+using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
-namespace hzcache
+namespace HzCache
{
public enum CacheItemChangeType
{
@@ -84,7 +85,7 @@ public class HzCacheOptions
///
public EvictionPolicy evictionPolicy { get; set; } = EvictionPolicy.LRU;
- public ILogger? logger { get; set; }
+ public ILogger? logger { get; set; }
}
public interface IHzCache
@@ -96,6 +97,8 @@ public interface IHzCache
///
void RemoveByPattern(string pattern, bool sendNotification = true);
+ Task RemoveByPatternAsync(string pattern, bool sendNotification = true);
+
///
/// Attempts to get a value by key
///
@@ -103,6 +106,8 @@ public interface IHzCache
/// True if value exists, otherwise false
T? Get(string key);
+ Task GetAsync(string key);
+
///
/// Attempts to add a key/value item
///
@@ -111,6 +116,8 @@ public interface IHzCache
/// True if value was added, otherwise false (already exists)
void Set(string key, T? value);
+ Task SetAsync(string key, T? value);
+
///
/// Adds a key/value pair. This method could potentially be optimized, but not sure as of now.
/// The initial "TryGetValue" likely costs a bit, which adds time and makes it slower than
@@ -123,6 +130,8 @@ public interface IHzCache
/// True if value was added, otherwise false (already exists)
void Set(string key, T? value, TimeSpan ttl);
+ Task SetAsync(string key, T? value, TimeSpan ttl);
+
///
/// Adds a key/value pair by using the specified function if the key does not already exist, or returns the existing
/// value if the key exists.
@@ -133,6 +142,8 @@ public interface IHzCache
/// The maximum amount of time (in ms) to wait for backend. Default is 10.000ms
T? GetOrSet(string key, Func valueFactory, TimeSpan ttl, long maxMsToWaitForFactory = 10000);
+ Task GetOrSetAsync(string key, Func> valueFactory, TimeSpan ttl, long maxMsToWaitForFactory = 10000);
+
///
/// Get a list of cache items by key list. If the key doesn't exist, it will be added by the valueFactory which
/// must return a list of KeyValuePairs where the Key is the cache key and the value is the value to add.
@@ -143,6 +154,8 @@ public interface IHzCache
/// A list of items matching the keys.
public IList GetOrSetBatch(IList keys, Func, List>> valueFactory);
+ public Task> GetOrSetBatchAsync(IList keys, Func, Task>>> valueFactory);
+
///
/// Get a list of cache items by key list. If the key doesn't exist, it will be added by the valueFactory which
/// must return a list of KeyValuePairs where the Key is the cache key and the value is the value to add.
@@ -154,11 +167,16 @@ public interface IHzCache
/// A list of items matching the keys.
public IList GetOrSetBatch(IList keys, Func, List>> valueFactory, TimeSpan ttl);
+ public Task> GetOrSetBatchAsync(IList keys, Func, Task>>> valueFactory, TimeSpan ttl);
+
///
/// Tries to remove item with the specified key, also returns the object removed in an "out" var
///
/// The key of the element to remove
bool Remove(string key);
+
+ Task ClearAsync();
+ Task RemoveAsync(string key);
}
public interface IDetailedHzCache : IHzCache
@@ -181,7 +199,7 @@ public interface IDetailedHzCache : IHzCache
/// The key of the element to remove
/// Send backplane notification or not
/// If function returns true, skip removing the entry
- bool Remove(string key, bool sendBackplaneNotification = true, Func? checksumEqualsFunc = null);
+ bool Remove(string key, bool sendBackplaneNotification, Func? checksumEqualsFunc = null);
CacheStatistics GetStatistics();
}
@@ -190,5 +208,10 @@ public class CacheStatistics
{
public long Counts { get; set; }
public long SizeInBytes { get; set; }
+
+ public override string ToString()
+ {
+ return $"Number of keys: {Counts}, SizeInBytes: {SizeInBytes}";
+ }
}
}
diff --git a/HzMemoryCache/TTLValue.cs b/HzMemoryCache/TTLValue.cs
index 20f6070..955b17d 100644
--- a/HzMemoryCache/TTLValue.cs
+++ b/HzMemoryCache/TTLValue.cs
@@ -1,11 +1,14 @@
#nullable enable
using System;
using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
using System.Security.Cryptography;
+using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using Utf8Json;
-namespace hzcache
+namespace HzCache
{
public class TTLValue
{
@@ -46,20 +49,59 @@ public TTLValue(string key, object? value, TimeSpan ttl, IPropagatorBlock(byte[] compressedData)
{
- // var target = new byte[LZ4Codec.MaximumOutputSize(compressedData.Length)];
- // LZ4Codec.Decode(
- // compressedData, 0, compressedData.Length,
- // target, 0, target.Length);
var redisValue = JsonSerializer.Deserialize(compressedData);
+ using Stream valueStream = new MemoryStream(false ? Decompress(redisValue.valueJson) : redisValue.valueJson);
+ return new TTLValue
+ {
+ checksum = redisValue.checksum,
+ key = redisValue.key,
+ ttlInMs = redisValue.ttlInMs,
+ value = JsonSerializer.Deserialize(valueStream),
+ sizeInBytes = redisValue.valueJson.Length,
+ timestampCreated = redisValue.timestampCreated,
+ tickCountWhenToKill = redisValue.tickCountWhenToKill,
+ absoluteExpireTime = redisValue.absoluteExpireTime
+ };
+ }
+
+ public static async Task FromRedisValueAsync(byte[] compressedData)
+ {
+ using Stream stream = new MemoryStream(compressedData);
+ var redisValue = await JsonSerializer.DeserializeAsync(stream);
+ using Stream valueStream = new MemoryStream(false ? Decompress(redisValue.valueJson) : redisValue.valueJson);
return new TTLValue
{
checksum = redisValue.checksum,
key = redisValue.key,
ttlInMs = redisValue.ttlInMs,
- value = JsonSerializer.Deserialize(redisValue.valueJson),
+ value = await JsonSerializer.DeserializeAsync(valueStream),
+ sizeInBytes = redisValue.valueJson.Length,
timestampCreated = redisValue.timestampCreated,
tickCountWhenToKill = redisValue.tickCountWhenToKill,
absoluteExpireTime = redisValue.absoluteExpireTime
@@ -71,9 +113,10 @@ public void UpdateChecksum()
using var md5 = MD5.Create();
var valueJson = JsonSerializer.Serialize(value);
checksum = BitConverter.ToString(md5.ComputeHash(valueJson));
+ sizeInBytes = valueJson.Length;
var redisValue = new TTLRedisValue
{
- valueJson = valueJson,
+ valueJson = false ? Compress(valueJson) : valueJson,
key = key,
timestampCreated = timestampCreated,
absoluteExpireTime = absoluteExpireTime,
diff --git a/RedisBackplaneHzCache/README.md b/RedisBackedHzCache/README.md
similarity index 100%
rename from RedisBackplaneHzCache/README.md
rename to RedisBackedHzCache/README.md
diff --git a/RedisBackplaneHzCache/RedisBackplaneHzCache.cs b/RedisBackedHzCache/RedisBackedHzCache.cs
similarity index 87%
rename from RedisBackplaneHzCache/RedisBackplaneHzCache.cs
rename to RedisBackedHzCache/RedisBackedHzCache.cs
index 0f8c51f..b262326 100644
--- a/RedisBackplaneHzCache/RedisBackplaneHzCache.cs
+++ b/RedisBackedHzCache/RedisBackedHzCache.cs
@@ -1,30 +1,28 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Text;
-using hzcache;
using Microsoft.Extensions.Logging;
using StackExchange.Redis;
using Utf8Json;
-namespace RedisBackplaneMemoryCache
+namespace HzCache
{
- public class RedisBackplaneMemoryMemoryCacheOptions : HzCacheOptions
+ public class RedisBackedHzCacheOptions : HzCacheOptions
{
public string redisConnectionString { get; set; }
- public string instanceId { get; set; }
- public bool useRedisAs2ndLevelCache { get; set; } = false;
+ public bool useRedisAs2ndLevelCache { get; set; }
}
- public class RedisBackplaneHzCache : IDetailedHzCache
+ public partial class RedisBackedHzCache : IDetailedHzCache
{
private readonly HzMemoryCache hzCache;
private readonly string instanceId = Guid.NewGuid().ToString();
- private readonly RedisBackplaneMemoryMemoryCacheOptions options;
- private readonly ConnectionMultiplexer redis;
+ private readonly RedisBackedHzCacheOptions options;
private readonly IDatabase redisDb;
- public RedisBackplaneHzCache(RedisBackplaneMemoryMemoryCacheOptions options)
+ public RedisBackedHzCache(RedisBackedHzCacheOptions options)
{
this.options = options;
if (this.options.redisConnectionString != null)
@@ -37,7 +35,7 @@ public RedisBackplaneHzCache(RedisBackplaneMemoryMemoryCacheOptions options)
instanceId = options.instanceId;
}
- redis = ConnectionMultiplexer.Connect(options.redisConnectionString);
+ var redis = ConnectionMultiplexer.Connect(options.redisConnectionString);
redisDb = redis.GetDatabase();
hzCache = new HzMemoryCache(new HzCacheOptions
{
@@ -49,20 +47,20 @@ public RedisBackplaneHzCache(RedisBackplaneMemoryMemoryCacheOptions options)
{
options.valueChangeListener?.Invoke(key, changeType, ttlValue, objectData, isPattern);
var redisChannel = new RedisChannel(options.applicationCachePrefix, RedisChannel.PatternMode.Auto);
- // Console.WriteLine($"Publishing message {changeType} {this.options.applicationCachePrefix} {instanceId} {key} {ttlValue?.checksum} {ttlValue?.timestampCreated} {isPattern}");
var messageObject = new RedisInvalidationMessage(this.options.applicationCachePrefix, instanceId, key, ttlValue?.checksum, ttlValue?.timestampCreated,
isPattern);
redis.GetSubscriber().PublishAsync(redisChannel, new RedisValue(JsonSerializer.ToJsonString(messageObject)));
var redisKey = GetRedisKey(key);
if (changeType == CacheItemChangeType.AddOrUpdate)
{
- if (options.useRedisAs2ndLevelCache && objectData != null)
+ if (options.useRedisAs2ndLevelCache && objectData != null && ttlValue != null)
{
try
{
- options.logger?.LogTrace("Setting value for key {Key} in redis", key);
+ var stopwatch = Stopwatch.StartNew();
redisDb.StringSet(redisKey, objectData,
TimeSpan.FromMilliseconds(ttlValue.absoluteExpireTime - DateTimeOffset.Now.ToUnixTimeMilliseconds()));
+ options.logger?.LogTrace("Writing value for key {Key} in redis took {Elapsed} ms", key, stopwatch.ElapsedMilliseconds);
}
catch (Exception e)
{
@@ -122,8 +120,6 @@ public RedisBackplaneHzCache(RedisBackplaneMemoryMemoryCacheOptions options)
if (invalidationMessage.instanceId != instanceId)
{
- // Console.WriteLine(
- // $"[{instanceId}] Received invalidation for key {invalidationMessage.key} from {invalidationMessage.instanceId}, isPattern: {invalidationMessage.isPattern}");
if (invalidationMessage.isPattern.HasValue && invalidationMessage.isPattern.Value)
{
hzCache.RemoveByPattern(invalidationMessage.key, false);
@@ -151,14 +147,14 @@ public void Clear()
hzCache.Clear();
}
- public bool Remove(string key, bool sendBackplaneNotification = true, Func skipRemoveIfEqualFunc = null)
+ public bool Remove(string key, bool sendBackplaneNotification, Func skipRemoveIfEqualFunc = null)
{
return hzCache.Remove(key, sendBackplaneNotification, skipRemoveIfEqualFunc);
}
public CacheStatistics GetStatistics()
{
- throw new NotImplementedException();
+ return hzCache.GetStatistics();
}
public T Get(string key)
@@ -166,7 +162,9 @@ public T Get(string key)
var value = hzCache.Get(key);
if (value == null && options.useRedisAs2ndLevelCache)
{
+ var stopwatch = Stopwatch.StartNew();
var redisValue = redisDb.StringGet(GetRedisKey(key));
+ options.logger?.LogTrace("Reading value for key {Key} in redis took {Elapsed} ms", key, stopwatch.ElapsedMilliseconds);
if (!redisValue.IsNull)
{
var ttlValue = TTLValue.FromRedisValue(Encoding.ASCII.GetBytes(redisValue.ToString()));
@@ -190,7 +188,6 @@ public void Set(string key, T value, TimeSpan ttl)
public T GetOrSet(string key, Func valueFactory, TimeSpan ttl, long maxMsToWaitForFactory = 10000)
{
- // TODO: Fix this HUGE HOLE!
return hzCache.GetOrSet(key, valueFactory, ttl, maxMsToWaitForFactory);
}
@@ -198,25 +195,26 @@ public IList GetOrSetBatch(IList keys, Func, List GetOrSetBatch(IList keys, Func, List>> valueFactory, TimeSpan ttl)
{
- Func, List>> redisFactory = (idList) =>
+ Func, List>> redisFactory = idList =>
{
// Create a list of redis keys from the list of cache keys
var redisKeyList = idList.Select(GetRedisKey).Select(k => new RedisKey(k)).ToArray();
-
+
// Get all values from redis, non-existing values are returned as RedisValue where HasValue == false;
var redisBatchResult = redisDb.StringGet(redisKeyList);
-
+
// Create a list of key-value pairs from the redis key list and the redis batch result. Values not found will still have HasValue == false
var redisKeyValueBatchResult = redisKeyList.Select((id, i) => new KeyValuePair(id, redisBatchResult[i])).ToList();
-
+
// Create a list of cache keys for which the value factory should be called
var idsForFactoryCall = redisKeyValueBatchResult.Where(rb => !rb.Value.HasValue).Select(rb => rb.Key.ToString()).ToList();
-
+
// Call the value factory with the list of cache keys missing in redis and create a Dictionary for lookup.
var factoryRetrievedValues = valueFactory.Invoke(idsForFactoryCall.Select(CacheKeyFromRedisKey).ToList()).ToDictionary(pair => pair.Key, pair => pair.Value);
-
+
// Merge factory-retrieved values with the redis values
return redisKeyValueBatchResult.Select(kv =>
{
@@ -226,7 +224,7 @@ public IList GetOrSetBatch(IList keys, Func, List(Encoding.UTF8.GetBytes(kv.Value));
hzCache.SetRaw(cacheKey, ttlValue);
- value = (T) ttlValue.value;
+ value = (T)ttlValue.value;
}
else if (factoryRetrievedValues.TryGetValue(cacheKey, out var factoryValue))
{
@@ -236,9 +234,9 @@ public IList GetOrSetBatch(IList keys, Func, List(cacheKey, value);
}).ToList();
-
};
return hzCache.GetOrSetBatch(keys, redisFactory, ttl);
}
@@ -252,6 +250,7 @@ private string GetRedisKey(string cacheKey)
{
return $"{options.applicationCachePrefix}:{cacheKey}";
}
+
private string CacheKeyFromRedisKey(string redisKey)
{
return redisKey.Substring(options.applicationCachePrefix.Length + 1);
diff --git a/RedisBackplaneHzCache/RedisBackplaneHzCache.csproj b/RedisBackedHzCache/RedisBackedHzCache.csproj
similarity index 89%
rename from RedisBackplaneHzCache/RedisBackplaneHzCache.csproj
rename to RedisBackedHzCache/RedisBackedHzCache.csproj
index 306f70c..cab87f4 100644
--- a/RedisBackplaneHzCache/RedisBackplaneHzCache.csproj
+++ b/RedisBackedHzCache/RedisBackedHzCache.csproj
@@ -2,7 +2,7 @@
netstandard2.0
- RedisBackplaneHzCache
+ RedisBackedHzCache
RedisBackplaneHzCache
Anders Heintz
HzCache
@@ -13,7 +13,7 @@
Library
True
true
- RedisBackplaneMemoryCache
+ HzCache
diff --git a/RedisBackedHzCache/RedisBackedHzCacheAsync.cs b/RedisBackedHzCache/RedisBackedHzCacheAsync.cs
new file mode 100644
index 0000000..6bc4945
--- /dev/null
+++ b/RedisBackedHzCache/RedisBackedHzCacheAsync.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using StackExchange.Redis;
+
+namespace HzCache
+{
+ public partial class RedisBackedHzCache
+ {
+ public async Task RemoveByPatternAsync(string pattern, bool sendNotification = true)
+ {
+ await hzCache.RemoveByPatternAsync(pattern, sendNotification);
+ }
+
+ public async Task GetAsync(string key)
+ {
+ var value = await hzCache.GetAsync(key);
+ if (value == null && options.useRedisAs2ndLevelCache)
+ {
+ var stopwatch = Stopwatch.StartNew();
+ var redisValue = await redisDb.StringGetAsync(GetRedisKey(key));
+ options.logger?.LogTrace("Redis get for key {Key} took {Elapsed} ms", key, stopwatch.ElapsedMilliseconds);
+ stopwatch.Restart();
+ if (!redisValue.IsNull)
+ {
+ var ttlValue = await TTLValue.FromRedisValueAsync(Encoding.ASCII.GetBytes(redisValue.ToString()));
+ options.logger?.LogTrace("Deerialize {Key} took {Elapsed} ms", key, stopwatch.ElapsedMilliseconds);
+ stopwatch.Restart();
+ hzCache.SetRaw(key, ttlValue);
+ return (T)ttlValue.value;
+ }
+ }
+
+ return value;
+ }
+
+ public async Task SetAsync(string key, T value)
+ {
+ await hzCache.SetAsync(key, value);
+ }
+
+ public async Task SetAsync(string key, T value, TimeSpan ttl)
+ {
+ await hzCache.SetAsync(key, value, ttl);
+ }
+
+ public async Task GetOrSetAsync(string key, Func> valueFactory, TimeSpan ttl, long maxMsToWaitForFactory = 10000)
+ {
+ return await hzCache.GetOrSetAsync(key, valueFactory, ttl, maxMsToWaitForFactory);
+ }
+
+ public async Task> GetOrSetBatchAsync(IList keys, Func, Task>>> valueFactory)
+ {
+ return await GetOrSetBatchAsync(keys, valueFactory, options.defaultTTL);
+ }
+
+ public async Task> GetOrSetBatchAsync(IList keys, Func, Task>>> valueFactory, TimeSpan ttl)
+ {
+ Func, Task>>> redisFactory = async idList =>
+ {
+ // Create a list of redis keys from the list of cache keys
+ var redisKeyList = idList.Select(GetRedisKey).Select(k => new RedisKey(k)).ToArray();
+
+ // Get all values from redis, non-existing values are returned as RedisValue where HasValue == false;
+ var redisBatchResult = redisDb.StringGet(redisKeyList);
+
+ // Create a list of key-value pairs from the redis key list and the redis batch result. Values not found will still have HasValue == false
+ var redisKeyValueBatchResult = redisKeyList.Select((id, i) => new KeyValuePair(id, redisBatchResult[i])).ToList();
+
+ // Create a list of cache keys for which the value factory should be called
+ var idsForFactoryCall = redisKeyValueBatchResult.Where(rb => !rb.Value.HasValue).Select(rb => rb.Key.ToString()).ToList();
+
+ // Call the value factory with the list of cache keys missing in redis and create a Dictionary for lookup.
+ var factoryRetrievedValues =
+ (await valueFactory.Invoke(idsForFactoryCall.Select(CacheKeyFromRedisKey).ToList())).ToDictionary(pair => pair.Key, pair => pair.Value);
+
+ // Merge factory-retrieved values with the redis values
+ return redisKeyValueBatchResult.Select(kv =>
+ {
+ var cacheKey = CacheKeyFromRedisKey(kv.Key);
+ T value;
+ if (kv.Value.HasValue)
+ {
+ var ttlValue = TTLValue.FromRedisValue(Encoding.UTF8.GetBytes(kv.Value));
+ hzCache.SetRaw(cacheKey, ttlValue);
+ value = (T)ttlValue.value;
+ }
+ else if (factoryRetrievedValues.TryGetValue(cacheKey, out var factoryValue))
+ {
+ value = factoryValue;
+ }
+ else
+ {
+ value = default;
+ }
+
+ return new KeyValuePair(cacheKey, value);
+ }).ToList();
+ };
+ return await hzCache.GetOrSetBatchAsync(keys, redisFactory, ttl);
+ }
+
+ public async Task ClearAsync()
+ {
+ await hzCache.ClearAsync();
+ }
+
+ public async Task RemoveAsync(string key)
+ {
+ return await hzCache.RemoveAsync(key);
+ }
+ }
+}
diff --git a/RedisBackplaneHzCache/RedisInvalidationMessage.cs b/RedisBackedHzCache/RedisInvalidationMessage.cs
similarity index 88%
rename from RedisBackplaneHzCache/RedisInvalidationMessage.cs
rename to RedisBackedHzCache/RedisInvalidationMessage.cs
index 7ce6f01..5ff85c8 100644
--- a/RedisBackplaneHzCache/RedisInvalidationMessage.cs
+++ b/RedisBackedHzCache/RedisInvalidationMessage.cs
@@ -1,4 +1,4 @@
-namespace RedisBackplaneMemoryCache
+namespace HzCache
{
public class RedisInvalidationMessage
{
@@ -15,7 +15,7 @@ public RedisInvalidationMessage(string applicationCachePrefix, string instanceId
public string applicationCachePrefix { get; }
public string instanceId { get; set; }
public string key { get; set; }
- public bool? isPattern { get; set; } = false;
+ public bool? isPattern { get; set; }
public string checksum { get; set; }
public long? timestamp { get; set; }
}
diff --git a/RedisIDistributedCache/RedisIDistributedCache.cs b/RedisIDistributedCache/RedisIDistributedCache.cs
index 6c696b3..3202be7 100644
--- a/RedisIDistributedCache/RedisIDistributedCache.cs
+++ b/RedisIDistributedCache/RedisIDistributedCache.cs
@@ -14,8 +14,8 @@ public class RedisIDistributedCacheOptions
public class RedisIDistributedCache : IDistributedCache
{
- private RedisIDistributedCacheOptions options;
- private IDatabase redis;
+ private readonly RedisIDistributedCacheOptions options;
+ private readonly IDatabase redis;
public RedisIDistributedCache(RedisIDistributedCacheOptions options)
{
@@ -30,38 +30,32 @@ public byte[] Get(string key)
{
return null;
}
+
var ttlValue = JsonSerializer.Deserialize(redisValue.ToString());
return ttlValue.value;
}
- public async Task GetAsync(string key, CancellationToken token = new CancellationToken())
+ public async Task GetAsync(string key, CancellationToken token = new())
{
var redisValue = await redis.StringGetAsync(key);
if (!redisValue.HasValue)
{
return null;
}
+
var ttlValue = JsonSerializer.Deserialize(redisValue.ToString());
return ttlValue.value;
}
public void Set(string key, byte[] value, DistributedCacheEntryOptions cacheOptions)
{
- var ttlValue = new TTLValue
- {
- value = value,
- slidingExpiration = cacheOptions.SlidingExpiration?.TotalMilliseconds ?? 0
- };
+ var ttlValue = new TTLValue {value = value, slidingExpiration = cacheOptions.SlidingExpiration?.TotalMilliseconds ?? 0};
redis.StringSet(key, JsonSerializer.Serialize(ttlValue), cacheOptions.AbsoluteExpirationRelativeToNow);
}
- public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions cacheOptions, CancellationToken token = new CancellationToken())
+ public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions cacheOptions, CancellationToken token = new())
{
- var ttlValue = new TTLValue
- {
- value = value,
- slidingExpiration = cacheOptions.SlidingExpiration?.TotalMilliseconds ?? 0
- };
+ var ttlValue = new TTLValue {value = value, slidingExpiration = cacheOptions.SlidingExpiration?.TotalMilliseconds ?? 0};
await redis.StringSetAsync(key, JsonSerializer.Serialize(ttlValue), cacheOptions.AbsoluteExpirationRelativeToNow);
}
@@ -72,25 +66,21 @@ public void Refresh(string key)
{
return;
}
+
var ttlValue = JsonSerializer.Deserialize(redisValue.ToString());
- Set(key, ttlValue.value, new DistributedCacheEntryOptions
- {
- SlidingExpiration = TimeSpan.FromMilliseconds(ttlValue.slidingExpiration)
- });
+ Set(key, ttlValue.value, new DistributedCacheEntryOptions {SlidingExpiration = TimeSpan.FromMilliseconds(ttlValue.slidingExpiration)});
}
- public async Task RefreshAsync(string key, CancellationToken token = new CancellationToken())
+ public async Task RefreshAsync(string key, CancellationToken token = new())
{
var redisValue = await redis.StringGetAsync(key);
if (!redisValue.HasValue)
{
return;
}
+
var ttlValue = JsonSerializer.Deserialize(redisValue.ToString());
- await SetAsync(key, ttlValue.value, new DistributedCacheEntryOptions
- {
- SlidingExpiration = TimeSpan.FromMilliseconds(ttlValue.slidingExpiration)
- }, token);
+ await SetAsync(key, ttlValue.value, new DistributedCacheEntryOptions {SlidingExpiration = TimeSpan.FromMilliseconds(ttlValue.slidingExpiration)}, token);
}
public void Remove(string key)
@@ -98,7 +88,7 @@ public void Remove(string key)
redis.KeyDelete(key);
}
- public async Task RemoveAsync(string key, CancellationToken token = new CancellationToken())
+ public async Task RemoveAsync(string key, CancellationToken token = new())
{
await redis.KeyDeleteAsync(key);
}
diff --git a/RedisIDistributedCache/RedisIDistributedCache.csproj b/RedisIDistributedCache/RedisIDistributedCache.csproj
index be8bd2e..73cf86b 100644
--- a/RedisIDistributedCache/RedisIDistributedCache.csproj
+++ b/RedisIDistributedCache/RedisIDistributedCache.csproj
@@ -1,25 +1,25 @@
-
- netstandard2.0
- HzCache.RedisIDistributedCache
- HzCache.RedisIDistributedCache
- Anders Heintz
- HzCache
- https://github.com/aheintz/hzcache
- 0.0.3
- Very simple IDistributedCache implementation for redis
- latestmajor
- Library
- True
- true
- RedisIDistributedCache
-
+
+ netstandard2.0
+ HzCache.RedisIDistributedCache
+ HzCache.RedisIDistributedCache
+ Anders Heintz
+ HzCache
+ https://github.com/aheintz/hzcache
+ 0.0.3
+ Very simple IDistributedCache implementation for redis
+ latestmajor
+ Library
+ True
+ true
+ RedisIDistributedCache
+
-
-
-
-
-
+
+
+
+
+
diff --git a/UnitTests/IntegrationTests.cs b/UnitTests/IntegrationTests.cs
index b1da7db..e1f5803 100644
--- a/UnitTests/IntegrationTests.cs
+++ b/UnitTests/IntegrationTests.cs
@@ -1,6 +1,7 @@
using System.Diagnostics;
-using hzcache;
-using RedisBackplaneMemoryCache;
+using HzCache;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
using StackExchange.Redis;
namespace UnitTests
@@ -13,6 +14,9 @@ public Mocko(long num)
str = num.ToString();
}
+ public string guid { get; } = Guid.NewGuid().ToString();
+ public long timestamp { get; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+
public long num { get; set; }
public string str { get; set; }
}
@@ -21,21 +25,19 @@ public class LargeMocko
{
public LargeMocko() { }
- public LargeMocko(long numOfItems)
+ public LargeMocko(string key, long numOfItems)
{
+ this.key = key;
for (var i = 0; i < numOfItems; i++)
{
var m = new Mocko(i);
- for (var j = 0; j < 100; j++)
- {
- m.str += Guid.NewGuid().ToString();
- }
-
+ m.str = $"asd qwe zxc ert {i}";
items[i] = m;
}
}
public Dictionary items { get; set; } = new();
+ public string key { get; set; }
}
[TestClass]
@@ -45,17 +47,17 @@ public class IntegrationTests
[TestCategory("Integration")]
public async Task TestRedisBackplaneInvalidation()
{
- var c1 = new RedisBackplaneHzCache(
- new RedisBackplaneMemoryMemoryCacheOptions {redisConnectionString = "localhost", applicationCachePrefix = "test", instanceId = "c1"});
+ var c1 = new RedisBackedHzCache(
+ new RedisBackedHzCacheOptions {redisConnectionString = "localhost", applicationCachePrefix = "test", instanceId = "c1"});
await Task.Delay(200);
- var c2 = new RedisBackplaneHzCache(
- new RedisBackplaneMemoryMemoryCacheOptions {redisConnectionString = "localhost", applicationCachePrefix = "test", instanceId = "c2"});
+ var c2 = new RedisBackedHzCache(
+ new RedisBackedHzCacheOptions {redisConnectionString = "localhost", applicationCachePrefix = "test", instanceId = "c2"});
Console.WriteLine("Adding 1 to c1");
- c1.Set("1", new Mocko(1));
+ await c1.SetAsync("1", new Mocko(1));
await Task.Delay(200);
Console.WriteLine("Adding 1 to c2");
- c2.Set("1", new Mocko(2));
+ await c2.SetAsync("1", new Mocko(2));
await Task.Delay(200);
Assert.IsNull(c1.Get("1"));
@@ -67,49 +69,84 @@ public async Task TestRedisBackplaneInvalidation()
[TestCategory("Integration")]
public async Task TestLargeObjects()
{
+ var serviceProvider = new ServiceCollection()
+ .AddLogging(
+ builder => builder
+ .AddSimpleConsole(options =>
+ {
+ options.IncludeScopes = true;
+ options.SingleLine = true;
+ options.TimestampFormat = "HH:mm:ss ";
+ })
+ .SetMinimumLevel(LogLevel.Trace)
+ )
+ .BuildServiceProvider();
+
+ var factory = serviceProvider.GetService();
+
+ var logger = factory.CreateLogger();
+
var redis = ConnectionMultiplexer.Connect("localhost");
- var c1 = new RedisBackplaneHzCache(
- new RedisBackplaneMemoryMemoryCacheOptions
+ var c1 = new RedisBackedHzCache(
+ new RedisBackedHzCacheOptions
{
- redisConnectionString = "localhost", applicationCachePrefix = "test", instanceId = "c1", useRedisAs2ndLevelCache = true
+ redisConnectionString = "localhost",
+ applicationCachePrefix = "largeobjectstest",
+ instanceId = "c1",
+ useRedisAs2ndLevelCache = true,
+ notificationType = NotificationType.Async,
+ logger = factory.CreateLogger()
});
await Task.Delay(200);
- var c2 = new RedisBackplaneHzCache(
- new RedisBackplaneMemoryMemoryCacheOptions
+ var c2 = new RedisBackedHzCache(
+ new RedisBackedHzCacheOptions
{
- redisConnectionString = "localhost", applicationCachePrefix = "test", instanceId = "c2", useRedisAs2ndLevelCache = true
+ redisConnectionString = "localhost",
+ applicationCachePrefix = "largeobjectstest",
+ instanceId = "c2",
+ useRedisAs2ndLevelCache = true,
+ notificationType = NotificationType.Async,
+ logger = factory.CreateLogger()
});
+ var objectList = new List();
+
for (var q = 1; q <= 10; q++)
{
- Console.WriteLine("Adding 1 to c1");
- var o = new LargeMocko(5000);
+ objectList.Add(new LargeMocko("o" + q, 50000));
+ }
+
+ foreach (var o in objectList)
+ {
var s = Stopwatch.StartNew();
- c1.Set("" + q, o);
- Console.WriteLine($"Time to write large object: {s.ElapsedMilliseconds} ms");
+ await c1.SetAsync(o.key, o);
+ logger.LogInformation("Time to write large object {Q} to c1: {Elapsed} ms", o.key, s.ElapsedMilliseconds);
s = Stopwatch.StartNew();
- while (c2.Get("" + q) == null && s.ElapsedMilliseconds < 30000)
+ while (await c2.GetAsync(o.key) == null && s.ElapsedMilliseconds < 30000)
{
- await Task.Delay(100);
+ await Task.Delay(50);
}
- Console.WriteLine($"Time from write to available on second node: {s.ElapsedMilliseconds} ms");
- Assert.IsNotNull(c2.Get("1"));
+ logger.LogInformation("Time from write to available on second node: {Elapsed} ms", s.ElapsedMilliseconds);
+ Assert.IsNotNull(await c2.GetAsync(o.key));
}
- c1.Set("testinglargeretrieval", new LargeMocko(10000));
+ logger.LogInformation("Starting testinglargeretrieval");
+ await c1.SetAsync("testinglargeretrieval", new LargeMocko("testinglargeretrieval", 100000));
var stopWatch = Stopwatch.StartNew();
- while (!redis.GetDatabase().KeyExists("testinglargeretrieval") && stopWatch.ElapsedMilliseconds < 30000)
+ while (!await redis.GetDatabase().KeyExistsAsync("testinglargeretrieval") && stopWatch.ElapsedMilliseconds < 30000)
{
- await Task.Delay(100);
+ await Task.Delay(50);
}
stopWatch.Restart();
- c2.Get("testinglargeretrieval");
+ await c2.GetAsync("testinglargeretrieval");
stopWatch.Stop();
- Assert.IsTrue(stopWatch.ElapsedMilliseconds < 500);
- Console.WriteLine($"Reading from redis took {stopWatch.ElapsedMilliseconds} ms");
+ Assert.IsTrue(stopWatch.ElapsedMilliseconds < 1500);
+ logger.LogInformation("Reading from redis took {Elapsed} ms", stopWatch.ElapsedMilliseconds);
+ logger.LogInformation("c1 stats: {Stats}", c1.GetStatistics());
+ logger.LogInformation("c2 stats: {Stats}", c2.GetStatistics());
}
@@ -118,50 +155,63 @@ public async Task TestLargeObjects()
public async Task TestRedisClear()
{
var redis = ConnectionMultiplexer.Connect("localhost");
- var c1 = new RedisBackplaneHzCache(
- new RedisBackplaneMemoryMemoryCacheOptions {redisConnectionString = "localhost", applicationCachePrefix = "test", instanceId = "c1"});
+ var c1 = new RedisBackedHzCache(
+ new RedisBackedHzCacheOptions {redisConnectionString = "localhost", applicationCachePrefix = "test", instanceId = "c1"});
await Task.Delay(200);
- var c2 = new RedisBackplaneHzCache(
- new RedisBackplaneMemoryMemoryCacheOptions {redisConnectionString = "localhost", applicationCachePrefix = "test", instanceId = "c2"});
+ var c2 = new RedisBackedHzCache(
+ new RedisBackedHzCacheOptions {redisConnectionString = "localhost", applicationCachePrefix = "test", instanceId = "c2"});
Console.WriteLine("Adding 1 to c1");
- c1.Set("1", new Mocko(1));
+ await c1.SetAsync("1", new Mocko(1));
await Task.Delay(100);
Console.WriteLine("Adding 2 to c2");
- c1.Set("2", new Mocko(2));
+ await c1.SetAsync("2", new Mocko(2));
Console.WriteLine("Adding 2 to c2");
- c2.Set("3", new Mocko(3));
+ await c2.SetAsync("3", new Mocko(3));
await Task.Delay(100);
- c1.Clear();
+ await c1.ClearAsync();
await Task.Delay(2000);
- Assert.IsNull(c1.Get("1"));
- Assert.IsNull(c2.Get("2"));
- Assert.IsNull(c2.Get("3"));
+ Assert.IsNull(await c1.GetAsync("1"));
+ Assert.IsNull(await c2.GetAsync("2"));
+ Assert.IsNull(await c2.GetAsync("3"));
}
[TestMethod]
[TestCategory("Integration")]
public async Task TestRedisGet()
{
- var redis = ConnectionMultiplexer.Connect("localhost");
- var c1 = new RedisBackplaneHzCache(
- new RedisBackplaneMemoryMemoryCacheOptions
- {
- redisConnectionString = "localhost", applicationCachePrefix = "test", instanceId = "c1", useRedisAs2ndLevelCache = true
- });
+ var c1 = new RedisBackedHzCache(
+ new RedisBackedHzCacheOptions {redisConnectionString = "localhost", applicationCachePrefix = "test", instanceId = "c1", useRedisAs2ndLevelCache = true});
await Task.Delay(200);
- var c2 = new RedisBackplaneHzCache(
- new RedisBackplaneMemoryMemoryCacheOptions
- {
- redisConnectionString = "localhost", applicationCachePrefix = "test", instanceId = "c2", useRedisAs2ndLevelCache = true
- });
+ var c2 = new RedisBackedHzCache(
+ new RedisBackedHzCacheOptions {redisConnectionString = "localhost", applicationCachePrefix = "test", instanceId = "c2", useRedisAs2ndLevelCache = true});
Console.WriteLine("Adding 1 to c1");
- c1.Set("1", new Mocko(10));
+ await c1.SetAsync("1", new Mocko(10));
await Task.Delay(100);
Console.WriteLine("Getting 1 from c2");
- Assert.IsNotNull(c2.Get("1"));
+ Assert.IsNotNull(await c2.GetAsync("1"));
+ }
+
+ [TestMethod]
+ [TestCategory("Integration")]
+ public async Task TestRedisGetOrSet()
+ {
+ var c1 = new RedisBackedHzCache(
+ new RedisBackedHzCacheOptions {redisConnectionString = "localhost", applicationCachePrefix = "test", instanceId = "c1", useRedisAs2ndLevelCache = true});
+ await Task.Delay(200);
+ var c2 = new RedisBackedHzCache(
+ new RedisBackedHzCacheOptions {redisConnectionString = "localhost", applicationCachePrefix = "test", instanceId = "c2", useRedisAs2ndLevelCache = true});
+
+ Console.WriteLine("Adding 1 to c1");
+ var v1 = c1.GetOrSet("1", _ => new Mocko(10), TimeSpan.FromMinutes(1));
+ Assert.IsNotNull(v1);
+ Assert.IsTrue(c1.Get("1").num == 10);
+ await Task.Delay(100);
+ var c21 = await c2.GetAsync("1");
+ Assert.IsTrue(c21.num == 10);
+ Assert.IsTrue(c21.guid != v1.guid);
}
@@ -170,49 +220,63 @@ public async Task TestRedisGet()
public async Task TestRedisBackplaneDelete()
{
var redis = ConnectionMultiplexer.Connect("localhost");
- var c1 = new RedisBackplaneHzCache(
- new RedisBackplaneMemoryMemoryCacheOptions {redisConnectionString = "localhost", applicationCachePrefix = "test", instanceId = "c1"});
+ var c1 = new RedisBackedHzCache(
+ new RedisBackedHzCacheOptions {redisConnectionString = "localhost", applicationCachePrefix = "test", instanceId = "c1"});
await Task.Delay(200);
- var c2 = new RedisBackplaneHzCache(
- new RedisBackplaneMemoryMemoryCacheOptions {redisConnectionString = "localhost", applicationCachePrefix = "test", instanceId = "c2"});
+ var c2 = new RedisBackedHzCache(
+ new RedisBackedHzCacheOptions {redisConnectionString = "localhost", applicationCachePrefix = "test", instanceId = "c2"});
Console.WriteLine("Adding 1 to c1");
- c1.Set("1", new Mocko(1));
+ await c1.SetAsync("1", new Mocko(1));
await Task.Delay(100);
Console.WriteLine("Adding 2 to c2");
- c1.Set("2", new Mocko(2));
+ await c1.SetAsync("2", new Mocko(2));
await Task.Delay(100);
Console.WriteLine("Delete 1 from c2");
- c2.Remove("1");
+ await c2.RemoveAsync("1");
await Task.Delay(300);
- Assert.IsNull(c1.Get("1"));
- Assert.IsNotNull(c1.Get("2"));
+ Assert.IsNull(await c1.GetAsync("1"));
+ Assert.IsNotNull(await c1.GetAsync("2"));
}
-
+
[TestMethod]
[TestCategory("Integration")]
public async Task TestRedisBatchGet()
{
var redis = ConnectionMultiplexer.Connect("localhost");
- var c1 = new RedisBackplaneHzCache(
- new RedisBackplaneMemoryMemoryCacheOptions {redisConnectionString = "localhost", applicationCachePrefix = "batch2", instanceId = "c1", useRedisAs2ndLevelCache = true, defaultTTL = TimeSpan.FromSeconds(60)});
+ var c1 = new RedisBackedHzCache(
+ new RedisBackedHzCacheOptions
+ {
+ redisConnectionString = "localhost",
+ applicationCachePrefix = "batch2",
+ instanceId = "c1",
+ useRedisAs2ndLevelCache = true,
+ defaultTTL = TimeSpan.FromSeconds(60)
+ });
- for (int i = 0; i < 10; i++)
+ for (var i = 0; i < 10; i++)
{
- c1.Set("key."+i, new Mocko(i));
+ await c1.SetAsync("key." + i, new Mocko(i));
}
await Task.Delay(3000);
- var keys = new List { "key.0", "key.2", "key.20", "key.30", "key.40"};
-
- var x = c1.GetOrSetBatch(keys, list =>
+ var keys = new List
{
- return list.Select(k => k.Substring("key.".Length)).Select(int.Parse).Select(i => new KeyValuePair("key."+i, new Mocko(i))).ToList();
+ "key.0",
+ "key.2",
+ "key.20",
+ "key.30",
+ "key.40"
+ };
+
+ var x = await c1.GetOrSetBatchAsync(keys, async list =>
+ {
+ return list.Select(k => k.Substring("key.".Length)).Select(int.Parse).Select(i => new KeyValuePair("key." + i, new Mocko(i))).ToList();
});
- for (int i=0; i("11"));
- Assert.IsNotNull(c1.Get("12"));
- Assert.IsNotNull(c1.Get("13"));
- Assert.IsNotNull(c1.Get("33"));
+ Assert.IsNotNull(await c1.GetAsync("11"));
+ Assert.IsNotNull(await c1.GetAsync("12"));
+ Assert.IsNotNull(await c1.GetAsync("13"));
+ Assert.IsNotNull(await c1.GetAsync("33"));
Assert.IsNull(c1.Get("22"));
Assert.IsNull(c1.Get("23"));
Console.WriteLine("Deleting by pattern 1*");
- c2.RemoveByPattern("1*");
+ await c2.RemoveByPatternAsync("1*");
await Task.Delay(400);
Assert.IsNull(c1.Get("11"));
Assert.IsNull(c1.Get("12"));
Assert.IsNull(c1.Get("13"));
- Assert.IsNotNull(c1.Get("33"));
+ Assert.IsNotNull(await c1.GetAsync("33"));
}
[TestMethod]
[TestCategory("Integration")]
public async Task TestDistributedInvalidationPerformance()
{
- var iterations = 1000000.0d;
+ var iterations = 500000.0d;
var redis = ConnectionMultiplexer.Connect("localhost");
- var c1 = new RedisBackplaneHzCache(new RedisBackplaneMemoryMemoryCacheOptions
+ var c1 = new RedisBackedHzCache(new RedisBackedHzCacheOptions
{
redisConnectionString = "localhost",
applicationCachePrefix = "test",
@@ -272,7 +336,7 @@ public async Task TestDistributedInvalidationPerformance()
useRedisAs2ndLevelCache = true
});
- var c2 = new RedisBackplaneHzCache(new RedisBackplaneMemoryMemoryCacheOptions
+ var c2 = new RedisBackedHzCache(new RedisBackedHzCacheOptions
{
redisConnectionString = "localhost:6379",
applicationCachePrefix = "test",
@@ -282,18 +346,18 @@ public async Task TestDistributedInvalidationPerformance()
});
Console.WriteLine("Adding 1 to c1");
- c1.Set("1", new Mocko(1));
+ await c1.SetAsync("1", new Mocko(1));
await Task.Delay(10);
Console.WriteLine("Adding 1 to c2");
- c2.Set("1", new Mocko(1));
+ await c2.SetAsync("1", new Mocko(1));
await Task.Delay(20);
- Assert.IsNotNull(c1.Get("1"));
- Assert.IsNotNull(c2.Get("1"));
+ Assert.IsNotNull(await c1.GetAsync("1"));
+ Assert.IsNotNull(await c2.GetAsync("1"));
var start = Stopwatch.StartNew();
for (var i = 0; i < iterations; i++)
{
- c1.Set("test" + i, new Mocko(i));
+ await c1.SetAsync("test" + i, new Mocko(i));
}
var end = start.ElapsedTicks;
@@ -313,6 +377,7 @@ public async Task TestDistributedInvalidationPerformance()
Console.WriteLine($"TTA: {setTime / iterations} ms/cache storage operation {setTime} ms/{iterations} items");
Console.WriteLine($"Postprocessing {iterations} items took {postProcessingTime} ms, {postProcessingTime / iterations} ms/item");
Console.WriteLine($"Complete throughput to redis: {iterations / (setTime + postProcessingTime)} items/ms");
+ Console.WriteLine(c1.GetStatistics().ToString());
}
}
}
diff --git a/UnitTests/UnitTests.cs b/UnitTests/UnitTests.cs
index beca677..47bf11d 100644
--- a/UnitTests/UnitTests.cs
+++ b/UnitTests/UnitTests.cs
@@ -1,5 +1,5 @@
using System.Diagnostics;
-using hzcache;
+using HzCache;
namespace UnitTests
{
@@ -305,21 +305,28 @@ public async Task TestRedisBatchGet()
{
var cache = new HzMemoryCache(new HzCacheOptions {evictionPolicy = EvictionPolicy.FIFO, cleanupJobInterval = 50});
- for (int i = 0; i < 10; i++)
+ for (var i = 0; i < 10; i++)
{
- cache.Set("key."+i, new Mocko(i));
+ cache.Set("key." + i, new Mocko(i));
}
- var keys = new List { "key.0", "key.2", "key.20", "key.30", "key.40"};
+ var keys = new List
+ {
+ "key.0",
+ "key.2",
+ "key.20",
+ "key.30",
+ "key.40"
+ };
var x = cache.GetOrSetBatch(keys, list =>
{
var strKeys = String.Join(",", list);
Assert.AreEqual("key.20,key.30,key.40", strKeys);
- return list.Select(k => k.Substring("key.".Length)).Select(int.Parse).Select(i => new KeyValuePair("key."+i, new Mocko(i))).ToList();
+ return list.Select(k => k.Substring("key.".Length)).Select(int.Parse).Select(i => new KeyValuePair("key." + i, new Mocko(i))).ToList();
});
- for (int i=0; i("key"));
}
-
+
[TestMethod]
public async Task TestTtlExtended()
{
diff --git a/UnitTests/UnitTests.csproj b/UnitTests/UnitTests.csproj
index 2e3ce6f..1c2c203 100644
--- a/UnitTests/UnitTests.csproj
+++ b/UnitTests/UnitTests.csproj
@@ -9,6 +9,9 @@
+
+
+
@@ -17,7 +20,7 @@
-
+
diff --git a/UnitTests/UnitTestsAsync.cs b/UnitTests/UnitTestsAsync.cs
new file mode 100644
index 0000000..5aa2484
--- /dev/null
+++ b/UnitTests/UnitTestsAsync.cs
@@ -0,0 +1,357 @@
+using System.Diagnostics;
+using HzCache;
+
+namespace UnitTests
+{
+ [TestClass]
+ public class UnitTestsAsync
+ {
+ [TestMethod]
+ public async Task TestValueChangeNotificationAsync()
+ {
+ var addOrUpdates = 0;
+ var removals = 0;
+ var expires = 0;
+ var cache = new HzMemoryCache(
+ new HzCacheOptions
+ {
+ cleanupJobInterval = 50,
+ notificationType = NotificationType.Sync,
+ valueChangeListener = (_, changeType, _, _, _) =>
+ {
+ switch (changeType)
+ {
+ case CacheItemChangeType.AddOrUpdate:
+ addOrUpdates++;
+ break;
+ case CacheItemChangeType.Expire:
+ expires++;
+ break;
+ case CacheItemChangeType.Remove:
+ removals++;
+ break;
+ }
+ }
+ });
+ await cache.SetAsync("mock2", new MockObject(1));
+ Assert.AreEqual(1, addOrUpdates);
+ await cache.SetAsync("mock2", new MockObject(2));
+ Assert.AreEqual(2, addOrUpdates);
+ await cache.RemoveAsync("mock2");
+ Assert.AreEqual(1, removals);
+ await cache.RemoveAsync("mock2");
+ Assert.AreEqual(2, removals);
+ await cache.GetOrSetAsync("m", async _ => Task.FromResult(new MockObject(1)), TimeSpan.FromMilliseconds(100));
+ Assert.AreEqual(3, addOrUpdates);
+ await Task.Delay(200);
+ Assert.AreEqual(1, expires);
+ }
+
+ [TestMethod]
+ public async Task TestRemoveByPatternAsync()
+ {
+ var removals = 0;
+ var cache = new HzMemoryCache(
+ new HzCacheOptions
+ {
+ cleanupJobInterval = 200,
+ valueChangeListener = async (key, changeType, _, _, _) =>
+ {
+ switch (changeType)
+ {
+ case CacheItemChangeType.Remove:
+ Console.WriteLine("Removed " + key);
+ removals++;
+ break;
+ }
+ }
+ });
+ await cache.SetAsync("pelle", new MockObject(42));
+ await cache.SetAsync("olle", new MockObject(42));
+ await cache.SetAsync("kalle", new MockObject(42));
+ await cache.SetAsync("stina", new MockObject(42));
+ await cache.SetAsync("lina", new MockObject(42));
+ await cache.SetAsync("nina", new MockObject(42));
+ await cache.SetAsync("tom", new MockObject(42));
+ await cache.SetAsync("tomma", new MockObject(42));
+ await cache.SetAsync("flina", new MockObject(42));
+
+ await cache.RemoveByPatternAsync("tom*");
+ Assert.IsNull(cache.Get("tom"));
+ Assert.IsNull(cache.Get("tomma"));
+
+ await cache.RemoveByPatternAsync("*lle*");
+ Assert.IsNull(cache.Get("pelle"));
+ Assert.IsNull(cache.Get("kalle"));
+ Assert.IsNull(cache.Get("olle"));
+ Assert.IsNotNull(cache.Get("stina"));
+ Assert.IsNotNull(cache.Get("lina"));
+ Assert.IsNotNull(cache.Get("nina"));
+ await Task.Delay(100);
+ Assert.AreEqual(2, removals); // Regex removals are not object by object.
+ Assert.AreEqual(4, cache.Count);
+ }
+
+ [TestMethod]
+ public async Task TestGetSetCleanupAsync()
+ {
+ var cache = new HzMemoryCache(new HzCacheOptions {cleanupJobInterval = 200});
+ await cache.SetAsync("42", new MockObject(42), TimeSpan.FromMilliseconds(100));
+ var v = await cache.GetAsync("42");
+ Assert.IsTrue(v != null);
+ Assert.IsTrue(v.num == 42);
+
+ await Task.Delay(300);
+ Assert.IsTrue(cache.Count == 0); //cleanup job has run?
+ }
+
+ [TestMethod]
+ public async Task TestEvictionAsync()
+ {
+ var list = new List();
+ for (var i = 0; i < 20; i++)
+ {
+ var cache = new HzMemoryCache(new HzCacheOptions {cleanupJobInterval = 200});
+ await cache.SetAsync("42", new MockObject(42), TimeSpan.FromMilliseconds(100));
+ list.Add(cache);
+ }
+
+ await Task.Delay(300);
+
+ for (var i = 0; i < 20; i++)
+ {
+ Assert.IsTrue(list[i].Count == 0); //cleanup job has run?
+ }
+
+ //cleanup
+ for (var i = 0; i < 20; i++)
+ {
+ list[i].Dispose();
+ }
+ }
+
+ [TestMethod]
+ public async Task ShortdelayAsync()
+ {
+ var cache = new HzMemoryCache();
+ await cache.SetAsync("42", new MockObject(42), TimeSpan.FromMilliseconds(500));
+
+ await Task.Delay(20);
+
+ var result = await cache.GetAsync("42");
+ Assert.IsNotNull(result); //not evicted
+ Assert.IsTrue(result.num == 42);
+ }
+
+ [TestMethod]
+ public async Task TestPrimitivesAsync()
+ {
+ var cache = new HzMemoryCache();
+ await cache.GetOrSetAsync("42", v => Task.FromResult(42), TimeSpan.FromMilliseconds(500));
+
+ await Task.Delay(20);
+
+ var result = await cache.GetAsync("42");
+ Assert.IsNotNull(result); //not evicted
+ Assert.IsTrue(result == 42);
+ }
+
+ [TestMethod]
+ public async Task TestWithDefaultJobIntervalAsync()
+ {
+ var cache2 = new HzMemoryCache();
+ await cache2.SetAsync("42", new MockObject(42), TimeSpan.FromMilliseconds(100));
+ Assert.IsNotNull(cache2.Get("42"));
+ await Task.Delay(150);
+ Assert.IsNull(await cache2.GetAsync("42"));
+ }
+
+ [TestMethod]
+ public async Task TestRemoveAsync()
+ {
+ var cache = new HzMemoryCache();
+ await cache.SetAsync("42", new MockObject(42), TimeSpan.FromMilliseconds(100));
+ await cache.RemoveAsync("42");
+ Assert.IsNull(cache.Get("42"));
+ }
+
+ [TestMethod]
+ public async Task TestTryRemoveAsync()
+ {
+ var cache = new HzMemoryCache();
+ cache.Set("42", new MockObject(42), TimeSpan.FromMilliseconds(100));
+ var res = await cache.RemoveAsync("42");
+ Assert.IsTrue(res);
+ Assert.IsNull(await cache.GetAsync("42"));
+
+ //now try remove non-existing item
+ Assert.IsFalse(await cache.RemoveAsync("blabblah"));
+ }
+
+ [TestMethod]
+ public async Task TestTryRemoveWithTtlAsync()
+ {
+ var cache = new HzMemoryCache();
+ await cache.SetAsync("42", new MockObject(42), TimeSpan.FromMilliseconds(100));
+ await Task.Delay(120); //let the item expire
+
+ var res = await cache.RemoveAsync("42");
+ Assert.IsFalse(res);
+ }
+
+ [TestMethod]
+ public async Task TestClear()
+ {
+ var cache = new HzMemoryCache();
+ await cache.GetOrSetAsync("key", _ => Task.FromResult(new MockObject(1024)), TimeSpan.FromSeconds(100));
+
+ await cache.ClearAsync();
+
+ Assert.IsNull(cache.Get("key"));
+ }
+
+ [TestMethod]
+ public async Task TestStampedeProtectionAsync()
+ {
+ var cache = new HzMemoryCache();
+ var stopwatch = Stopwatch.StartNew();
+ Task.Run(async () =>
+ {
+ await cache.GetOrSetAsync("key", _ =>
+ {
+ Task.Delay(2000).GetAwaiter().GetResult();
+ return Task.FromResult(new MockObject(1024));
+ }, TimeSpan.FromSeconds(100));
+ });
+ await Task.Delay(50);
+ var timeToInsert = stopwatch.ElapsedTicks / Stopwatch.Frequency * 1000;
+ stopwatch.Restart();
+ var x = await cache.GetOrSetAsync("key", _ => Task.FromResult(new MockObject(1024)), TimeSpan.FromSeconds(100));
+ var timeToWait = stopwatch.ElapsedTicks / Stopwatch.Frequency * 1000;
+ stopwatch.Restart();
+ await cache.GetOrSetAsync("key", _ => Task.FromResult(new MockObject(1024)), TimeSpan.FromSeconds(100));
+
+ var timeToGetFromMemoryCache = stopwatch.ElapsedTicks / Stopwatch.Frequency * 1000;
+
+ Console.WriteLine($"Insert: {timeToInsert}, Wait: {timeToWait}, Get: {timeToGetFromMemoryCache}");
+
+ Assert.IsTrue(timeToInsert < 50);
+ Assert.IsTrue(timeToWait is > 950 and < 1050);
+ Assert.IsTrue(timeToGetFromMemoryCache < 50);
+ }
+
+ [TestMethod]
+ public async Task TestStampedeProtectionTimeoutAsync()
+ {
+ var cache = new HzMemoryCache();
+ var stopwatch = Stopwatch.StartNew();
+ Task.Run(() =>
+ {
+ cache.GetOrSet("key", _ =>
+ {
+ Task.Delay(2000).GetAwaiter().GetResult();
+ return new MockObject(1024);
+ }, TimeSpan.FromSeconds(100), 1000);
+ });
+ await Task.Delay(50);
+ var timeToInsert = stopwatch.ElapsedTicks / Stopwatch.Frequency * 1000;
+ stopwatch.Restart();
+ var x = cache.GetOrSet("key", _ => new MockObject(2048), TimeSpan.FromSeconds(100));
+ var timeToWait = stopwatch.ElapsedTicks / Stopwatch.Frequency * 1000;
+ stopwatch.Restart();
+ var value = cache.GetOrSet("key", _ => new MockObject(1024), TimeSpan.FromSeconds(100));
+ Assert.AreEqual(value?.num, 2048);
+ }
+
+ [TestMethod]
+ public async Task TestNullValueAsync()
+ {
+ var cache = new HzMemoryCache();
+ await cache.SetAsync("key", null, TimeSpan.FromSeconds(100));
+ Assert.IsNull(await cache.GetAsync("key"));
+ }
+
+ [TestMethod]
+ public async Task TestLRUPolicyAsync()
+ {
+ var cache = new HzMemoryCache(new HzCacheOptions {evictionPolicy = EvictionPolicy.LRU, cleanupJobInterval = 50});
+ await cache.SetAsync("key", new MockObject(1), TimeSpan.FromMilliseconds(120));
+ Assert.IsNotNull(await cache.GetAsync("key"));
+ await Task.Delay(100);
+ Assert.IsNotNull(await cache.GetAsync("key"));
+ await Task.Delay(100);
+ Assert.IsNotNull(await cache.GetAsync("key"));
+ await Task.Delay(125);
+ Assert.IsNull(await cache.GetAsync("key"));
+ }
+
+ [TestMethod]
+ [TestCategory("Integration")]
+ public async Task TestRedisBatchGetAsync()
+ {
+ var cache = new HzMemoryCache(new HzCacheOptions {evictionPolicy = EvictionPolicy.FIFO, cleanupJobInterval = 50});
+
+ for (var i = 0; i < 10; i++)
+ {
+ cache.Set("key." + i, new Mocko(i));
+ }
+
+ var keys = new List
+ {
+ "key.0",
+ "key.2",
+ "key.20",
+ "key.30",
+ "key.40"
+ };
+
+ var x = await cache.GetOrSetBatchAsync(keys, async list =>
+ {
+ var strKeys = String.Join(",", list);
+ Assert.AreEqual("key.20,key.30,key.40", strKeys);
+ return list.Select(k => k.Substring("key.".Length)).Select(int.Parse).Select(i => new KeyValuePair("key." + i, new Mocko(i))).ToList();
+ });
+
+ for (var i = 0; i < keys.Count; i++)
+ {
+ var key = keys[i];
+ var num = int.Parse(key.Substring("key.".Length));
+ Assert.AreEqual(num, x[i].num);
+ }
+ }
+
+
+ [TestMethod]
+ public async Task TestFIFOPolicyAsync()
+ {
+ var cache = new HzMemoryCache(new HzCacheOptions {evictionPolicy = EvictionPolicy.FIFO, cleanupJobInterval = 50});
+ await cache.SetAsync("key", new MockObject(1), TimeSpan.FromMilliseconds(220));
+ await Task.Delay(100);
+ Assert.IsNotNull(await cache.GetAsync("key"));
+ await Task.Delay(100);
+ Assert.IsNotNull(await cache.GetAsync("key"));
+ await Task.Delay(100);
+ Assert.IsNull(await cache.GetAsync("key"));
+ }
+
+ [TestMethod]
+ public async Task TestTtlExtended()
+ {
+ var cache = new HzMemoryCache();
+ await cache.SetAsync("42", new MockObject(42), TimeSpan.FromMilliseconds(300));
+
+ await Task.Delay(50);
+ var result = await cache.GetAsync("42");
+ Assert.IsNotNull(result); //not evicted
+ Assert.IsTrue(result.num == 42);
+
+ await cache.SetAsync("42", new MockObject(42), TimeSpan.FromMilliseconds(300));
+
+ await Task.Delay(250);
+
+ result = await cache.GetAsync("42");
+ Assert.IsNotNull(result); //still not evicted
+ Assert.IsTrue(result.num == 42);
+ }
+ }
+}
diff --git a/hzcache.sln b/hzcache.sln
index 72ad1ca..40e93b3 100644
--- a/hzcache.sln
+++ b/hzcache.sln
@@ -9,7 +9,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "UnitTests\Unit
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HzCache.Benchmarks", "HzCache.Benchmarks\HzCache.Benchmarks.csproj", "{DD603B8C-5216-4079-B6B2-5AA54ED1B1F3}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedisBackplaneHzCache", "RedisBackplaneHzCache\RedisBackplaneHzCache.csproj", "{21F6B22C-EE16-47F4-A4E3-E10D4F2060D8}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedisBackedHzCache", "RedisBackedHzCache\RedisBackedHzCache.csproj", "{21F6B22C-EE16-47F4-A4E3-E10D4F2060D8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedisIDistributedCache", "RedisIDistributedCache\RedisIDistributedCache.csproj", "{F91CB554-BF50-49BB-898E-809DAF3E3382}"
EndProject