Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimizations #109

Merged
merged 2 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions benchmarks/StringBuilderConversionBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using BenchmarkDotNet.Attributes;
using SimpleCDN.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SimpleCDN.Benchmarks
{
[MemoryDiagnoser(false)]
public class StringBuilderConversionBenchmarks
{
public StringBuilder[] GetData()
{
Random random = new(293);
StringBuilder longStringBuilder = new();
for (int i = 0; i < 1000; i++)
{
longStringBuilder.AppendFormat("Test {0}: ", i).Append(random.GetItems<char>("abcdefghijklmnopqrstuvwxyz", 10));
}

StringBuilder shortStringBuilder = new();
for (int i = 0; i < 10; i++)
{
shortStringBuilder.AppendFormat("Test {0}: ", i).Append(random.GetItems<char>("abcdefghijklmnopqrstuvwxyz", 10));
}
return [longStringBuilder, shortStringBuilder];
}

public Encoding[] GetEncodings()
{
return [Encoding.UTF8, Encoding.ASCII];
}

[ParamsSource(nameof(GetEncodings))]
public Encoding CurrentEncoding { get; set; } = Encoding.UTF8;

[ArgumentsSource(nameof(GetData))]
[Benchmark]
public byte[] Manual(StringBuilder sb)
{
return sb.ToByteArray(CurrentEncoding);
}

[ArgumentsSource(nameof(GetData))]
[Benchmark]
public byte[] BuiltIn(StringBuilder sb)
{
return CurrentEncoding.GetBytes(sb.ToString());
}
}
}
135 changes: 28 additions & 107 deletions extensions/Redis/CustomRedisCacheService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,142 +3,63 @@
using Microsoft.Extensions.Options;
using SimpleCDN.Configuration;
using StackExchange.Redis;
using StackExchange.Redis.KeyspaceIsolation;
using System.Collections.Concurrent;
using System.Diagnostics;

namespace SimpleCDN.Extensions.Redis
{
internal sealed class CustomRedisCacheService : IDistributedCache, IAsyncDisposable
internal sealed class CustomRedisCacheService : IDistributedCache
{
private readonly IOptionsMonitor<RedisCacheConfiguration> _redisOptions;
private readonly IOptionsMonitor<CacheConfiguration> _cacheOptions;
private readonly ObjectAccessBalancer<ConnectionMultiplexer> _redisConnectionMultiplexers;
private readonly ILogger<CustomRedisCacheService> _logger;
private readonly IOptionsMonitor<RedisCacheConfiguration> options;
private readonly IOptionsMonitor<CacheConfiguration> cacheOptions;

private IDatabase Database => _redisConnectionMultiplexers.Next().GetDatabase();
private TimeSpan MaxAge => TimeSpan.FromMinutes(_cacheOptions.CurrentValue.MaxAge);
private readonly ConnectionMultiplexer _redisConnection;

DateTimeOffset lastTimeout = DateTimeOffset.MaxValue;
ushort multiplexerTargetCount = 1;

public CustomRedisCacheService(IOptionsMonitor<RedisCacheConfiguration> redisOptions, IOptionsMonitor<CacheConfiguration> cacheOptions, ILogger<CustomRedisCacheService> logger)
public CustomRedisCacheService(IOptionsMonitor<RedisCacheConfiguration> options, IOptionsMonitor<CacheConfiguration> cacheOptions)
{
_redisOptions = redisOptions;
_cacheOptions = cacheOptions;
_logger = logger;

// create the instance balancer with the instance factory
_redisConnectionMultiplexers = new(
() => ConnectionMultiplexer.Connect(
_redisOptions.CurrentValue.ConnectionString,
options => options.ClientName = redisOptions.CurrentValue.ClientName),
instance => instance.IsConnected && !instance.IsConnecting);
this.options = options;
this.cacheOptions = cacheOptions;
_redisConnection = ConnectionMultiplexer.Connect(Configuration.ConnectionString,
config => config.ClientName = Configuration.ClientName);
}

public ValueTask DisposeAsync() => _redisConnectionMultiplexers.DisposeAsync();
private RedisCacheConfiguration Configuration => options.CurrentValue;
private CacheConfiguration CacheConfiguration => cacheOptions.CurrentValue;

private IDatabase Database => _redisConnection.GetDatabase().WithKeyPrefix(Configuration.KeyPrefix);

public byte[]? Get(string key)
{
return Get(key, _redisConnectionMultiplexers.Next());
return Database.StringGetSetExpiry(key, TimeSpan.FromMinutes(CacheConfiguration.MaxAge));
}

private byte[]? Get(string key, ConnectionMultiplexer multiplexer, int retryCount = 0)
public async Task<byte[]?> GetAsync(string key, CancellationToken token = default)
{
const int maxRetries = 3;
if (retryCount > maxRetries)
return null;

try
{
// keep track of how long the operation takes,

var start = Stopwatch.GetTimestamp();
IDatabase database = multiplexer.GetDatabase();

RedisValue result = database.StringGet(key);
if (result.HasValue)
{
// reset the expiration time to have a sliding expiration
database.KeyExpire(key, MaxAge);
}

TimeSpan elapsed = Stopwatch.GetElapsedTime(start);

// if the operation took more than 100ms, create a new multiplexer
if (elapsed.TotalMilliseconds > 100)
{
CreateNewMultiplexer();
} else
{
// no timeout occurred, check if we can reduce the number of multiplexers
SyncMultiplexers();
}

return result;
} catch (RedisTimeoutException)
{
// timeout occured, create a new multiplexer and use that to try again, up to 3 times
_logger.LogError("Redis timeout exception occurred. Trying to reconnect ({count}/{maxRetries}).", retryCount, maxRetries);

return Get(key, CreateNewMultiplexer(), retryCount + 1);
}
return await Database.StringGetSetExpiryAsync(key, TimeSpan.FromMinutes(CacheConfiguration.MaxAge));
}

/// <summary>
/// Create a new multiplexer and set the last timeout to now.
/// </summary>
private ConnectionMultiplexer CreateNewMultiplexer()
{
lastTimeout = DateTimeOffset.UtcNow;
return _redisConnectionMultiplexers.AddInstance();
}
public void Refresh(string key) { }

public Task<byte[]?> GetAsync(string key, CancellationToken token = default)
public Task RefreshAsync(string key, CancellationToken token = default) => token.IsCancellationRequested ? Task.FromCanceled(token) : Task.CompletedTask;
public void Remove(string key)
{
if (token.IsCancellationRequested)
return Task.FromCanceled<byte[]?>(token);
return Task.FromResult(Get(key));
Database.KeyDelete(key);
}

public void Refresh(string key) { }
public Task RefreshAsync(string key, CancellationToken token = default) => Task.CompletedTask;
public void Remove(string key) => Database.StringGetDelete(key);
public Task RemoveAsync(string key, CancellationToken token = default) => Database.StringGetDeleteAsync(key);

public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
public Task RemoveAsync(string key, CancellationToken token = default)
{
Database.StringSet(key, value, MaxAge, flags: CommandFlags.FireAndForget);
return Database.KeyDeleteAsync(key);
}

public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default)
public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
{
await Database.StringSetAsync(key, value, MaxAge, flags: CommandFlags.FireAndForget);
Database.StringSet(key, value, TimeSpan.FromMinutes(CacheConfiguration.MaxAge));
}

private readonly SemaphoreSlim _syncLock = new(1, 1);
/// <summary>
/// If no timeouts have occurred in the last 15 seconds, reduce the number of multiplexers by 1.
/// </summary>
private void SyncMultiplexers()
public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default)
{
if (_syncLock.CurrentCount == 0)
return;
_syncLock.Wait();
Task.Run(() =>
{
if (DateTimeOffset.Now - lastTimeout > TimeSpan.FromSeconds(15))
{
multiplexerTargetCount--;
if (multiplexerTargetCount < 1)
multiplexerTargetCount = 1;
}
while (_redisConnectionMultiplexers.Count > multiplexerTargetCount)
{
_redisConnectionMultiplexers.RemoveOneInstance();
}
})
// release the lock when the task is done.
// ContinueWith is equivalent to finally in this case.
.ContinueWith(_ => _syncLock.Release());
return Database.StringSetAsync(key, value, TimeSpan.FromMinutes(CacheConfiguration.MaxAge));
}
}
}
165 changes: 0 additions & 165 deletions extensions/Redis/ObjectAccessBalancer.cs

This file was deleted.

2 changes: 1 addition & 1 deletion extensions/Redis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ var cdnBuilder = builder.Services.AddSimpleCDN();

- `options.ConnectionString`: The configuration string for the Redis server. This is a required property.
- `options.ClientName`: How the client should be identified to Redis. Default is `SimpleCDN`. This value can't contain whitespace.
- `options.KeyPrefix`: A string to prepend to all keys SimpleCDN inserts. Default is `SimpleCDN`. An empty value is allowed, meaning no prefix is added.
- `options.KeyPrefix`: A string to prepend to all keys SimpleCDN inserts. Default is `SimpleCDN::`. An empty value is allowed, meaning no prefix is added.
Loading
Loading