diff --git a/README.md b/README.md index 2fc23bd..de4c182 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,10 @@ cache.Set(key, "my cache sync", TimeSpan.FromMinutes(5)); var result = cache.Get(key); Console.WriteLine(result); +// get or add +result = cache.GetOrAdd(key, () => "my cache sync", TimeSpan.FromMinutes(5)); +Console.WriteLine(result); + // delete cache.Delete(key); @@ -75,6 +79,10 @@ await cache.SetAsync(key, "my cache async"); result = await cache.GetAsync(key); Console.WriteLine(result); +// get or add +result = await cache.GetOrAddAsync(key, () => "my cache async"); +Console.WriteLine(result); + // delete await cache.DeleteAsync(key); diff --git a/src/FasterKv.Cache.Core/FasterKvCache.cs b/src/FasterKv.Cache.Core/FasterKvCache.cs index ed66c22..f58ba90 100644 --- a/src/FasterKv.Cache.Core/FasterKvCache.cs +++ b/src/FasterKv.Cache.Core/FasterKvCache.cs @@ -104,6 +104,35 @@ public FasterKvCache(string name, return result.output.Get(_valueSerializer); } + public TValue GetOrAdd(string key, Func factory) + { + key.ArgumentNotNullOrEmpty(); + factory.ArgumentNotNull(); + + var result = Get(key); + if (result is not null) + return result; + + result = factory(key); + Set(key, result); + return result; + } + + public TValue GetOrAdd(string key, Func factory, TimeSpan expiryTime) + { + key.ArgumentNotNullOrEmpty(); + factory.ArgumentNotNull(); + expiryTime.ArgumentNotNegativeOrZero(); + + var result = Get(key); + if (result is not null) + return result; + + result = factory(key); + Set(key, result, expiryTime); + return result; + } + public void Delete(string key) { using var scopeSession = GetSessionWrap(); @@ -147,7 +176,36 @@ public void Set(string key, TValue value, TimeSpan expiryTime) return result.output.Get(_valueSerializer); } + + public async Task GetOrAddAsync(string key, Func> factory, CancellationToken token = default) + { + key.ArgumentNotNullOrEmpty(); + factory.ArgumentNotNull(); + + var result = await GetAsync(key, token); + if (result is not null) + return result; + + result = await factory(key); + await SetAsync(key, result, token); + return result; + } + + public async Task GetOrAddAsync(string key, Func> factory, TimeSpan expiryTime, CancellationToken token = default) + { + key.ArgumentNotNullOrEmpty(); + factory.ArgumentNotNull(); + expiryTime.ArgumentNotNegativeOrZero(); + var result = await GetAsync(key, token); + if (result is not null) + return result; + + result = await factory(key); + await SetAsync(key, result, expiryTime, token); + return result; + } + public async Task DeleteAsync(string key, CancellationToken token = default) { key.ArgumentNotNull(); @@ -171,7 +229,7 @@ public async Task SetAsync(string key, TValue? value, TimeSpan expiryTim using var sessionWrap = GetSessionWrap(); await SetInternalAsync(sessionWrap, key, value, token, expiryTime); } - + private async Task SetInternalAsync(ClientSessionWrap sessionWrap, string key, TValue? value, CancellationToken cancellationToken, TimeSpan? expiryTime = null) { diff --git a/src/FasterKv.Cache.Core/FasterKvStore.TValue.cs b/src/FasterKv.Cache.Core/FasterKvStore.TValue.cs index c55c68a..4539e72 100644 --- a/src/FasterKv.Cache.Core/FasterKvStore.TValue.cs +++ b/src/FasterKv.Cache.Core/FasterKvStore.TValue.cs @@ -97,6 +97,32 @@ public FasterKvCache(string name, return result.output.Data; } + public TValue GetOrAdd(string key, Func factory) + { + key.ArgumentNotNullOrEmpty(); + + var value = Get(key); + if (value is not null) + return value; + + value = factory(key); + Set(key, value); + return value; + } + + public TValue GetOrAdd(string key, Func factory, TimeSpan expiryTime) + { + key.ArgumentNotNullOrEmpty(); + + var value = Get(key); + if (value is not null) + return value; + + value = factory(key); + Set(key, value, expiryTime); + return value; + } + public void Delete(string key) { key.ArgumentNotNull(); @@ -141,7 +167,33 @@ public void Set(string key, TValue value, TimeSpan expiryTime) return result.output.Data; } - + + public async Task GetOrAddAsync(string key, Func> factory, CancellationToken token = default) + { + key.ArgumentNotNullOrEmpty(); + + var value = await GetAsync(key, token); + if (value is not null) + return value; + + value = await factory(key); + await SetAsync(key, value, token); + return value; + } + + public async Task GetOrAddAsync(string key, Func> factory, TimeSpan expiryTime, CancellationToken token = default) + { + key.ArgumentNotNullOrEmpty(); + + var value = await GetAsync(key, token); + if (value is not null) + return value; + + value = await factory(key); + await SetAsync(key, value, expiryTime, token); + return value; + } + public async Task DeleteAsync(string key, CancellationToken token = default) { key.ArgumentNotNull(); @@ -149,7 +201,7 @@ public async Task DeleteAsync(string key, CancellationToken token = default) using var scopeSession = GetSessionWrap(); (await scopeSession.Session.DeleteAsync(ref key, token: token).ConfigureAwait(false)).Complete(); } - + public async Task SetAsync(string key, TValue? value, CancellationToken token = default) { key.ArgumentNotNullOrEmpty(); @@ -157,7 +209,7 @@ public async Task SetAsync(string key, TValue? value, CancellationToken token = using var sessionWrap = GetSessionWrap(); await SetInternalAsync(sessionWrap, key, value, token); } - + public async Task SetAsync(string key, TValue? value, TimeSpan expiryTime, CancellationToken token = default) { key.ArgumentNotNullOrEmpty(); @@ -165,7 +217,7 @@ public async Task SetAsync(string key, TValue? value, TimeSpan expiryTime, Cance using var sessionWrap = GetSessionWrap(); await SetInternalAsync(sessionWrap, key, value, token, expiryTime); } - + private void SetInternal(ClientSessionWrap sessionWrap, string key, TValue? value, TimeSpan? expiryTime = null) diff --git a/tests/FasterKv.Cache.Core.Tests/KvStore/FasterKvStoreObjectTest.GetOrAdd.cs b/tests/FasterKv.Cache.Core.Tests/KvStore/FasterKvStoreObjectTest.GetOrAdd.cs new file mode 100644 index 0000000..57d4f35 --- /dev/null +++ b/tests/FasterKv.Cache.Core.Tests/KvStore/FasterKvStoreObjectTest.GetOrAdd.cs @@ -0,0 +1,222 @@ +using FasterKv.Cache.Core.Abstractions; +using FasterKv.Cache.Core.Configurations; +using FasterKv.Cache.MessagePack; + +namespace FasterKv.Cache.Core.Tests.KvStore; + +public class FasterKvStoreObjectTestGetOrAdd +{ + private static FasterKvCache CreateKvStore(string guid, ISystemClock? systemClock = null) + { + return new FasterKvCache(null!, + systemClock ?? new DefaultSystemClock(), + new FasterKvCacheOptions + { + IndexCount = 16384, + MemorySizeBit = 10, + PageSizeBit = 10, + ReadCacheMemorySizeBit = 10, + ReadCachePageSizeBit = 10, + SerializerName = "MessagePack", + ExpiryKeyScanInterval = TimeSpan.FromSeconds(1), + LogPath = $"./unit-test/{guid}" + }, + new IFasterKvCacheSerializer[] + { + new MessagePackFasterKvCacheSerializer + { + Name = "MessagePack" + } + }, + null); + } + + [Fact] + public void GetOrAdd_Should_Return_Existing_Value() + { + + var guid = Guid.NewGuid().ToString("N"); + using var fasterKv = CreateKvStore(guid); + + var data = new Data + { + One = "one", + Two = 2 + }; + fasterKv.Set(guid, data); + + var result = fasterKv.GetOrAdd(guid, _ => new Data + { + One = "two", + Two = 3 + }); + + Assert.Equal(data, result); + } + + [Fact] + public void GetOrAdd_Should_Return_New_Value() + { + var guid = Guid.NewGuid().ToString("N"); + using var fasterKv = CreateKvStore(guid); + + var result = fasterKv.GetOrAdd(guid, (_) => new Data + { + One = "two", + Two = 3 + }); + + Assert.Equal("two", result.One); + Assert.Equal(3, result.Two); + } + + [Fact] + public void GetOrAdd_Should_Return_NewValue_When_Expired() + { + var guid = Guid.NewGuid().ToString("N"); + var mockSystemClock = new MockSystemClock(DateTimeOffset.Now); + using var fasterKv = CreateKvStore(guid, mockSystemClock); + + var data = new Data + { + One = "one", + Two = 2 + }; + fasterKv.Set(guid, data, TimeSpan.FromSeconds(1)); + + mockSystemClock.AddSeconds(2); + + var result = fasterKv.GetOrAdd(guid, _ => new Data + { + One = "two", + Two = 3 + }); + + Assert.Equal("two", result.One); + Assert.Equal(3, result.Two); + } + + [Fact] + public void GetOrAdd_Should_Return_NewValue_When_Expired_And_Refresh() + { + var guid = Guid.NewGuid().ToString("N"); + var mockSystemClock = new MockSystemClock(DateTimeOffset.Now); + using var fasterKv = CreateKvStore(guid, mockSystemClock); + + var result = fasterKv.GetOrAdd(guid, _ => new Data() + { + One = "one", + Two = 2 + }, TimeSpan.FromSeconds(1)); + + Assert.Equal("one", result.One); + Assert.Equal(2, result.Two); + + mockSystemClock.AddSeconds(2); + + result = fasterKv.GetOrAdd(guid, _ => new Data + { + One = "two", + Two = 3 + }, TimeSpan.FromSeconds(1)); + + Assert.Equal("two", result.One); + Assert.Equal(3, result.Two); + } + + // below test GetOrAddAsync + + [Fact] + public async Task GetOrAddAsync_Should_Return_Existing_Value() + { + + var guid = Guid.NewGuid().ToString("N"); + using var fasterKv = CreateKvStore(guid); + + var data = new Data + { + One = "one", + Two = 2 + }; + fasterKv.Set(guid, data); + + var result = await fasterKv.GetOrAddAsync(guid, _ => Task.FromResult(new Data + { + One = "two", + Two = 3 + })); + + Assert.Equal(data, result); + } + + [Fact] + public async Task GetOrAddAsync_Should_Return_New_Value() + { + var guid = Guid.NewGuid().ToString("N"); + using var fasterKv = CreateKvStore(guid); + + var result = await fasterKv.GetOrAddAsync(guid, (_) => Task.FromResult(new Data + { + One = "two", + Two = 3 + })); + + Assert.Equal("two", result.One); + Assert.Equal(3, result.Two); + } + + [Fact] + public async Task GetOrAddAsync_Should_Return_NewValue_When_Expired() + { + var guid = Guid.NewGuid().ToString("N"); + var mockSystemClock = new MockSystemClock(DateTimeOffset.Now); + using var fasterKv = CreateKvStore(guid, mockSystemClock); + + var data = new Data + { + One = "one", + Two = 2 + }; + fasterKv.Set(guid, data, TimeSpan.FromSeconds(1)); + + mockSystemClock.AddSeconds(2); + + var result = await fasterKv.GetOrAddAsync(guid, _ => Task.FromResult(new Data + { + One = "two", + Two = 3 + })); + + Assert.Equal("two", result.One); + Assert.Equal(3, result.Two); + } + + [Fact] + public async Task GetOrAddAsync_Should_Return_NewValue_When_Expired_And_Refresh() + { + var guid = Guid.NewGuid().ToString("N"); + var mockSystemClock = new MockSystemClock(DateTimeOffset.Now); + using var fasterKv = CreateKvStore(guid, mockSystemClock); + + var result = await fasterKv.GetOrAddAsync(guid, _ => Task.FromResult(new Data() + { + One = "one", + Two = 2 + }), TimeSpan.FromSeconds(1)); + + Assert.Equal("one", result.One); + Assert.Equal(2, result.Two); + + mockSystemClock.AddSeconds(2); + + result = await fasterKv.GetOrAddAsync(guid, _ => Task.FromResult(new Data + { + One = "two", + Two = 3 + }), TimeSpan.FromSeconds(1)); + + Assert.Equal("two", result.One); + Assert.Equal(3, result.Two); + } +} + diff --git a/tests/FasterKv.Cache.Core.Tests/KvStore/FasterKvStoreTest.GetOrAdd.cs b/tests/FasterKv.Cache.Core.Tests/KvStore/FasterKvStoreTest.GetOrAdd.cs new file mode 100644 index 0000000..9258968 --- /dev/null +++ b/tests/FasterKv.Cache.Core.Tests/KvStore/FasterKvStoreTest.GetOrAdd.cs @@ -0,0 +1,221 @@ +using FasterKv.Cache.Core.Abstractions; +using FasterKv.Cache.Core.Configurations; +using FasterKv.Cache.MessagePack; + +namespace FasterKv.Cache.Core.Tests.KvStore; + +public class FasterKvStoreTestGetOrAdd +{ + private static FasterKvCache CreateKvStore(string guid, ISystemClock? systemClock = null) + { + return new FasterKvCache(null!, + systemClock ?? new DefaultSystemClock(), + new FasterKvCacheOptions + { + IndexCount = 16384, + MemorySizeBit = 10, + PageSizeBit = 10, + ReadCacheMemorySizeBit = 10, + ReadCachePageSizeBit = 10, + SerializerName = "MessagePack", + ExpiryKeyScanInterval = TimeSpan.FromSeconds(1), + LogPath = $"./unit-test/{guid}" + }, + new IFasterKvCacheSerializer[] + { + new MessagePackFasterKvCacheSerializer + { + Name = "MessagePack" + } + }, + null); + } + + [Fact] + public void GetOrAdd_Should_Return_Existing_Value() + { + + var guid = Guid.NewGuid().ToString("N"); + using var fasterKv = CreateKvStore(guid); + + var data = new Data + { + One = "one", + Two = 2 + }; + fasterKv.Set(guid, data); + + var result = fasterKv.GetOrAdd(guid, (_) => new Data + { + One = "two", + Two = 3 + }); + + Assert.Equal(data, result); + } + + [Fact] + public void GetOrAdd_Should_Return_New_Value() + { + var guid = Guid.NewGuid().ToString("N"); + using var fasterKv = CreateKvStore(guid); + + var result = fasterKv.GetOrAdd(guid, (_) => new Data + { + One = "two", + Two = 3 + }); + + Assert.Equal("two", result.One); + Assert.Equal(3, result.Two); + } + + [Fact] + public void GetOrAdd_Should_Return_NewValue_When_Expired() + { + var guid = Guid.NewGuid().ToString("N"); + var mockSystemClock = new MockSystemClock(DateTimeOffset.Now); + using var fasterKv = CreateKvStore(guid, mockSystemClock); + + var data = new Data + { + One = "one", + Two = 2 + }; + fasterKv.Set(guid, data, TimeSpan.FromSeconds(1)); + + mockSystemClock.AddSeconds(2); + + var result = fasterKv.GetOrAdd(guid, _ => new Data + { + One = "two", + Two = 3 + }); + + Assert.Equal("two", result.One); + Assert.Equal(3, result.Two); + } + + [Fact] + public void GetOrAdd_Should_Return_NewValue_When_Expired_And_Refresh() + { + var guid = Guid.NewGuid().ToString("N"); + var mockSystemClock = new MockSystemClock(DateTimeOffset.Now); + using var fasterKv = CreateKvStore(guid, mockSystemClock); + + var result = fasterKv.GetOrAdd(guid, _ => new Data() + { + One = "one", + Two = 2 + }, TimeSpan.FromSeconds(1)); + + Assert.Equal("one", result.One); + Assert.Equal(2, result.Two); + + mockSystemClock.AddSeconds(2); + + result = fasterKv.GetOrAdd(guid, _ => new Data + { + One = "two", + Two = 3 + }, TimeSpan.FromSeconds(1)); + + Assert.Equal("two", result.One); + Assert.Equal(3, result.Two); + } + + // below test GetOrAddAsync + + [Fact] + public async Task GetOrAddAsync_Should_Return_Existing_Value() + { + + var guid = Guid.NewGuid().ToString("N"); + using var fasterKv = CreateKvStore(guid); + + var data = new Data + { + One = "one", + Two = 2 + }; + fasterKv.Set(guid, data); + + var result = await fasterKv.GetOrAddAsync(guid, _ => Task.FromResult(new Data + { + One = "two", + Two = 3 + })); + + Assert.Equal(data, result); + } + + [Fact] + public async Task GetOrAddAsync_Should_Return_New_Value() + { + var guid = Guid.NewGuid().ToString("N"); + using var fasterKv = CreateKvStore(guid); + + var result = await fasterKv.GetOrAddAsync(guid, (_) => Task.FromResult(new Data + { + One = "two", + Two = 3 + })); + + Assert.Equal("two", result.One); + Assert.Equal(3, result.Two); + } + + [Fact] + public async Task GetOrAddAsync_Should_Return_NewValue_When_Expired() + { + var guid = Guid.NewGuid().ToString("N"); + var mockSystemClock = new MockSystemClock(DateTimeOffset.Now); + using var fasterKv = CreateKvStore(guid, mockSystemClock); + + var data = new Data + { + One = "one", + Two = 2 + }; + fasterKv.Set(guid, data, TimeSpan.FromSeconds(1)); + + mockSystemClock.AddSeconds(2); + + var result = await fasterKv.GetOrAddAsync(guid, _ => Task.FromResult(new Data + { + One = "two", + Two = 3 + })); + + Assert.Equal("two", result.One); + Assert.Equal(3, result.Two); + } + + [Fact] + public async Task GetOrAddAsync_Should_Return_NewValue_When_Expired_And_Refresh() + { + var guid = Guid.NewGuid().ToString("N"); + var mockSystemClock = new MockSystemClock(DateTimeOffset.Now); + using var fasterKv = CreateKvStore(guid, mockSystemClock); + + var result = await fasterKv.GetOrAddAsync(guid, _ => Task.FromResult(new Data() + { + One = "one", + Two = 2 + }), TimeSpan.FromSeconds(1)); + + Assert.Equal("one", result.One); + Assert.Equal(2, result.Two); + + mockSystemClock.AddSeconds(2); + + result = await fasterKv.GetOrAddAsync(guid, _ => Task.FromResult(new Data + { + One = "two", + Two = 3 + }), TimeSpan.FromSeconds(1)); + + Assert.Equal("two", result.One); + Assert.Equal(3, result.Two); + } +} \ No newline at end of file diff --git a/tests/FasterKv.Cache.Core.Tests/MockSystemClock.cs b/tests/FasterKv.Cache.Core.Tests/MockSystemClock.cs new file mode 100644 index 0000000..ab97134 --- /dev/null +++ b/tests/FasterKv.Cache.Core.Tests/MockSystemClock.cs @@ -0,0 +1,28 @@ +using FasterKv.Cache.Core.Abstractions; + +namespace FasterKv.Cache.Core.Tests; + +public class MockSystemClock : ISystemClock +{ + private DateTimeOffset _now; + + public MockSystemClock(DateTimeOffset now) + { + _now = now; + } + + public DateTimeOffset Now() + { + return _now; + } + + public long NowUnixTimestamp() + { + return _now.ToUnixTimeMilliseconds(); + } + + public void AddSeconds(int seconds) + { + _now = _now.AddSeconds(seconds); + } +} \ No newline at end of file