-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
650 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
namespace SchematicHQ.Client.Test; | ||
|
||
#nullable enable | ||
|
||
public class TestClient { } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
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 TimeSpan? Timeout { get; set; } // TODO | ||
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, | ||
Offline = options.Offline, | ||
EventBufferPeriod = options.EventBufferPeriod | ||
}; | ||
} | ||
} |
Oops, something went wrong.