Skip to content

Commit

Permalink
V1 custom code
Browse files Browse the repository at this point in the history
  • Loading branch information
bpapillon committed Jun 19, 2024
1 parent 90a52f6 commit a65d915
Show file tree
Hide file tree
Showing 10 changed files with 656 additions and 2 deletions.
8 changes: 8 additions & 0 deletions .fernignore
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
4 changes: 2 additions & 2 deletions src/SchematicHQ.Client.Test/SchematicHQ.Client.Test.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

Expand All @@ -21,4 +21,4 @@
<ProjectReference Include="..\SchematicHQ.Client\SchematicHQ.Client.csproj" />
</ItemGroup>

</Project>
</Project>
80 changes: 80 additions & 0 deletions src/SchematicHQ.Client.Test/TestCache.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"));
}
}
2 changes: 2 additions & 0 deletions src/SchematicHQ.Client.Test/TestClient.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
namespace SchematicHQ.Client.Test;

#nullable enable

public class TestClient { }
81 changes: 81 additions & 0 deletions src/SchematicHQ.Client.Test/TestEventBuffer.cs
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
}
}
147 changes: 147 additions & 0 deletions src/SchematicHQ.Client/Cache.cs
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);
}
}
34 changes: 34 additions & 0 deletions src/SchematicHQ.Client/Core/ClientOptionsCustom.cs
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

View workflow job for this annotation

GitHub Actions / compile

'ClientOptions' does not contain a definition for 'Timeout'

Check failure on line 29 in src/SchematicHQ.Client/Core/ClientOptionsCustom.cs

View workflow job for this annotation

GitHub Actions / compile

'ClientOptions' does not contain a definition for 'Timeout' and no accessible extension method 'Timeout' accepting a first argument of type 'ClientOptions' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 29 in src/SchematicHQ.Client/Core/ClientOptionsCustom.cs

View workflow job for this annotation

GitHub Actions / compile

'ClientOptions' does not contain a definition for 'Timeout'

Check failure on line 29 in src/SchematicHQ.Client/Core/ClientOptionsCustom.cs

View workflow job for this annotation

GitHub Actions / compile

'ClientOptions' does not contain a definition for 'Timeout' and no accessible extension method 'Timeout' accepting a first argument of type 'ClientOptions' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 29 in src/SchematicHQ.Client/Core/ClientOptionsCustom.cs

View workflow job for this annotation

GitHub Actions / compile

'ClientOptions' does not contain a definition for 'Timeout'

Check failure on line 29 in src/SchematicHQ.Client/Core/ClientOptionsCustom.cs

View workflow job for this annotation

GitHub Actions / compile

'ClientOptions' does not contain a definition for 'Timeout' and no accessible extension method 'Timeout' accepting a first argument of type 'ClientOptions' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 29 in src/SchematicHQ.Client/Core/ClientOptionsCustom.cs

View workflow job for this annotation

GitHub Actions / compile

'ClientOptions' does not contain a definition for 'Timeout'

Check failure on line 29 in src/SchematicHQ.Client/Core/ClientOptionsCustom.cs

View workflow job for this annotation

GitHub Actions / compile

'ClientOptions' does not contain a definition for 'Timeout' and no accessible extension method 'Timeout' accepting a first argument of type 'ClientOptions' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 29 in src/SchematicHQ.Client/Core/ClientOptionsCustom.cs

View workflow job for this annotation

GitHub Actions / compile

'ClientOptions' does not contain a definition for 'Timeout'

Check failure on line 29 in src/SchematicHQ.Client/Core/ClientOptionsCustom.cs

View workflow job for this annotation

GitHub Actions / compile

'ClientOptions' does not contain a definition for 'Timeout' and no accessible extension method 'Timeout' accepting a first argument of type 'ClientOptions' could be found (are you missing a using directive or an assembly reference?)
Offline = options.Offline,
EventBufferPeriod = options.EventBufferPeriod
};
}
}
Loading

0 comments on commit a65d915

Please sign in to comment.