-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,10 @@ | ||
# Specify files that shouldn't be modified by Fern | ||
README.md | ||
|
||
src/SchematicHQ.Client.Test/TestCache.cs | ||
src/SchematicHQ.Client.Test/TestEventBuffer.cs | ||
src/SchematicHQ.Client/Cache.cs | ||
src/SchematicHQ.Client/Core/ClientOptionsCustom.cs | ||
src/SchematicHQ.Client/EventBuffer.cs | ||
src/SchematicHQ.Client/Logger.cs | ||
src/SchematicHQ.Client/Schematic.cs |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
using System; | ||
using System.Threading.Tasks; | ||
using Xunit; | ||
|
||
#nullable enable | ||
|
||
namespace SchematicHQ.Client.Test; | ||
|
||
public class TestCache | ||
{ | ||
[Fact] | ||
public void TestCacheGetAndSet() | ||
{ | ||
var cache = new LocalCache<string>(maxSize: 1024, ttl: 5); | ||
|
||
cache.Set("key1", "value1"); | ||
Assert.Equal("value1", cache.Get("key1")); | ||
|
||
cache.Set("key2", "value2", ttlOverride: 1); | ||
Assert.Equal("value2", cache.Get("key2")); | ||
|
||
// Wait for the TTL to expire | ||
Task.Delay(TimeSpan.FromSeconds(2)).Wait(); | ||
Assert.Null(cache.Get("key2")); | ||
} | ||
|
||
[Fact] | ||
public void TestCacheEviction() | ||
{ | ||
var cache = new LocalCache<string>(maxSize: 100, ttl: 5); | ||
|
||
cache.Set("key1", "longvalue1"); | ||
cache.Set("key2", "longvalue2"); | ||
cache.Set("key3", "shortvalue"); | ||
|
||
// Least recently used item should be evicted | ||
Assert.Null(cache.Get("key1")); | ||
Assert.Equal("longvalue2", cache.Get("key2")); | ||
Assert.Equal("shortvalue", cache.Get("key3")); | ||
} | ||
|
||
[Fact] | ||
public void TestCacheConcurrency() | ||
{ | ||
var cache = new LocalCache<int>(maxSize: 1024, ttl: 5); | ||
|
||
// Simulate concurrent accesses to the cache | ||
Parallel.For(0, 1000, i => | ||
{ | ||
cache.Set($"key{i}", i); | ||
Assert.Equal(i, cache.Get($"key{i}")); | ||
}); | ||
} | ||
|
||
[Fact] | ||
public void TestCacheExpiration() | ||
{ | ||
var cache = new LocalCache<string>(maxSize: 1024, ttl: 1); | ||
|
||
cache.Set("key1", "value1"); | ||
Assert.Equal("value1", cache.Get("key1")); | ||
|
||
// Wait for the TTL to expire | ||
Task.Delay(TimeSpan.FromSeconds(2)).Wait(); | ||
Assert.Null(cache.Get("key1")); | ||
} | ||
|
||
[Fact] | ||
public void TestCacheMaxSize() | ||
{ | ||
var cache = new LocalCache<string>(maxSize: 50, ttl: 5); | ||
|
||
cache.Set("key1", "longvalue1"); | ||
cache.Set("key2", "longvalue2"); | ||
|
||
// Cache size exceeds max size, oldest item should be evicted | ||
Assert.Null(cache.Get("key1")); | ||
Assert.Equal("longvalue2", cache.Get("key2")); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
namespace SchematicHQ.Client.Test; | ||
|
||
#nullable enable | ||
|
||
public class TestClient { } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Threading; | ||
using Moq; | ||
using Xunit; | ||
|
||
#nullable enable | ||
|
||
namespace SchematicHQ.Client.Tests; | ||
|
||
public class TestEventBuffer | ||
{ | ||
[Fact] | ||
public void Push_Should_Add_Event_To_Buffer() | ||
{ | ||
// Arrange | ||
var eventsApiMock = new Mock<EventsClient>(); | ||
var loggerMock = new Mock<ISchematicLogger>(); | ||
var eventBuffer = new EventBuffer(eventsApiMock.Object, loggerMock.Object); | ||
var @event = new CreateEventRequestBody(); | ||
|
||
// Act | ||
eventBuffer.Push(@event); | ||
|
||
// Assert | ||
// Since the events list is private, we can't directly assert its contents. | ||
// However, we can verify that the CreateEventBatch method was called with the expected event. | ||
eventsApiMock.Verify(api => api.CreateEventBatch(It.Is<List<CreateEventRequestBody>>(list => list.Contains(@event))), Times.Once); | ||
} | ||
|
||
[Fact] | ||
public void Push_Should_Flush_Events_When_Buffer_Size_Exceeded() | ||
{ | ||
// Arrange | ||
var eventsApiMock = new Mock<EventsClient>(); | ||
var loggerMock = new Mock<ISchematicLogger>(); | ||
var eventBuffer = new EventBuffer(eventsApiMock.Object, loggerMock.Object, period: 10); | ||
var @event = new CreateEventRequestBody(); | ||
|
||
// Act | ||
for (int i = 0; i < 15; i++) | ||
{ | ||
eventBuffer.Push(@event); | ||
} | ||
|
||
// Assert | ||
// Verify that the CreateEventBatch method was called multiple times due to buffer size being exceeded. | ||
eventsApiMock.Verify(api => api.CreateEventBatch(It.IsAny<List<CreateEventRequestBody>>()), Times.AtLeast(2)); | ||
} | ||
|
||
[Fact] | ||
public void Stop_Should_Prevent_Further_Events_From_Being_Pushed() | ||
{ | ||
// Arrange | ||
var eventsApiMock = new Mock<EventsClient>(); | ||
var loggerMock = new Mock<ISchematicLogger>(); | ||
var eventBuffer = new EventBuffer(eventsApiMock.Object, loggerMock.Object); | ||
var @event = new CreateEventRequestBody(); | ||
|
||
// Act | ||
eventBuffer.Stop(); | ||
eventBuffer.Push(@event); | ||
|
||
// Assert | ||
eventsApiMock.Verify(api => api.CreateEventBatch(It.IsAny<List<CreateEventRequestBody>>()), Times.Never); | ||
loggerMock.Verify(logger => logger.Error(It.Is<string>(msg => msg.Contains("Event buffer is stopped")), null), Times.Once); | ||
} | ||
|
||
[Fact] | ||
public void Dispose_Should_Stop_Event_Buffer() | ||
{ | ||
// Arrange | ||
var eventsApiMock = new Mock<EventsClient>(); | ||
var loggerMock = new Mock<ISchematicLogger>(); | ||
var eventBuffer = new EventBuffer(eventsApiMock.Object, loggerMock.Object); | ||
|
||
// Act | ||
eventBuffer.Dispose(); | ||
Thread.Sleep(100); // Wait for a short duration to allow the background thread to stop | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
using System; | ||
using System.Collections.Concurrent; | ||
using System.Collections.Generic; | ||
using System.Threading; | ||
|
||
#nullable enable | ||
|
||
namespace SchematicHQ.Client; | ||
|
||
public interface ICacheProvider<T> | ||
{ | ||
T? Get(string key); | ||
void Set(string key, T val, int? ttlOverride = null); | ||
} | ||
|
||
public class CachedItem<T> | ||
{ | ||
public T Value { get; } | ||
public int AccessCounter { get; set; } | ||
public int Size { get; } | ||
public DateTime Expiration { get; } | ||
|
||
public CachedItem(T value, int accessCounter, int size, DateTime expiration) | ||
{ | ||
Value = value; | ||
AccessCounter = accessCounter; | ||
Size = size; | ||
Expiration = expiration; | ||
} | ||
} | ||
|
||
public class LocalCache<T> : ICacheProvider<T> | ||
{ | ||
private const int DEFAULT_CACHE_SIZE = 10 * 1024; // 10KB | ||
private const int DEFAULT_CACHE_TTL = 5; // 5 seconds | ||
|
||
private readonly ConcurrentDictionary<string, CachedItem<T>> _cache; | ||
private readonly object _lockObject = new object(); | ||
private readonly int _maxSize; | ||
private int _currentSize; | ||
private int _accessCounter; | ||
private readonly int _ttl; | ||
|
||
public LocalCache(int maxSize = DEFAULT_CACHE_SIZE, int ttl = DEFAULT_CACHE_TTL) | ||
{ | ||
_cache = new ConcurrentDictionary<string, CachedItem<T>>(); | ||
_maxSize = maxSize; | ||
_ttl = ttl; | ||
} | ||
|
||
public T? Get(string key) | ||
{ | ||
if (_maxSize == 0) | ||
return default; | ||
|
||
if (!_cache.TryGetValue(key, out var item)) | ||
return default; | ||
|
||
// Check if the item has expired | ||
if (DateTime.UtcNow > item.Expiration) | ||
{ | ||
lock (_lockObject) | ||
{ | ||
if (_cache.TryRemove(key, out var removedItem)) | ||
{ | ||
Interlocked.Add(ref _currentSize, -removedItem.Size); | ||
} | ||
} | ||
return default; | ||
} | ||
|
||
// Update the access counter for LRU eviction | ||
Interlocked.Increment(ref _accessCounter); | ||
item.AccessCounter = _accessCounter; | ||
_cache[key] = item; | ||
|
||
return item.Value; | ||
} | ||
|
||
public void Set(string key, T val, int? ttlOverride = null) | ||
{ | ||
if (_maxSize == 0) | ||
return; | ||
|
||
var ttl = ttlOverride ?? _ttl; | ||
var size = GetObjectSize(val); | ||
|
||
lock (_lockObject) | ||
{ | ||
// Check if the key already exists in the cache | ||
if (_cache.TryGetValue(key, out var item)) | ||
{ | ||
Interlocked.Add(ref _currentSize, size - item.Size); | ||
Interlocked.Increment(ref _accessCounter); | ||
_cache[key] = new CachedItem<T>(val, _accessCounter, size, DateTime.UtcNow.AddSeconds(ttl)); | ||
return; | ||
} | ||
|
||
// Evict expired items | ||
foreach (var kvp in _cache) | ||
{ | ||
if (DateTime.UtcNow > kvp.Value.Expiration) | ||
{ | ||
if (_cache.TryRemove(kvp.Key, out var removedItem)) | ||
{ | ||
Interlocked.Add(ref _currentSize, -removedItem.Size); | ||
} | ||
} | ||
} | ||
|
||
// Evict records if the cache size exceeds the max size | ||
while (_currentSize + size > _maxSize) | ||
{ | ||
string? oldestKey = null; | ||
var oldestAccessCounter = int.MaxValue; | ||
|
||
foreach (var kvp in _cache) | ||
{ | ||
if (kvp.Value.AccessCounter < oldestAccessCounter) | ||
{ | ||
oldestKey = kvp.Key; | ||
oldestAccessCounter = kvp.Value.AccessCounter; | ||
} | ||
} | ||
|
||
if (oldestKey != null && _cache.TryRemove(oldestKey, out var removedItem)) | ||
{ | ||
Interlocked.Add(ref _currentSize, -removedItem.Size); | ||
} | ||
else | ||
{ | ||
break; | ||
} | ||
} | ||
|
||
// Add the new item to the cache | ||
Interlocked.Increment(ref _accessCounter); | ||
_cache[key] = new CachedItem<T>(val, _accessCounter, size, DateTime.UtcNow.AddSeconds(ttl)); | ||
Interlocked.Add(ref _currentSize, size); | ||
} | ||
} | ||
|
||
private static int GetObjectSize(T obj) | ||
{ | ||
return System.Runtime.InteropServices.Marshal.SizeOf(obj); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
using SchematicHQ.Client.Core; | ||
|
||
#nullable enable | ||
|
||
namespace SchematicHQ.Client; | ||
|
||
public partial class ClientOptions | ||
{ | ||
public Dictionary<string, bool> FlagDefaults { get; set; } | ||
public ISchematicLogger Logger { get; set; } | ||
public List<ICacheProvider<bool>> CacheProviders { get; set; } | ||
public bool Offline { get; set; } | ||
public int? EventBufferPeriod { get; set; } | ||
} | ||
|
||
public static class ClientOptionsExtensions | ||
{ | ||
public static ClientOptions WithHttpClient(this ClientOptions options, HttpClient httpClient) | ||
{ | ||
return new ClientOptions | ||
{ | ||
BaseUrl = options.BaseUrl, | ||
HttpClient = httpClient, | ||
MaxRetries = options.MaxRetries, | ||
TimeoutInSeconds = options.TimeoutInSeconds, | ||
FlagDefaults = options.FlagDefaults, | ||
Logger = options.Logger, | ||
CacheProviders = options.CacheProviders, | ||
Timeout = options.Timeout, | ||
Check failure on line 29 in src/SchematicHQ.Client/Core/ClientOptionsCustom.cs
|
||
Offline = options.Offline, | ||
EventBufferPeriod = options.EventBufferPeriod | ||
}; | ||
} | ||
} |