Skip to content

Commit

Permalink
General tweaks and changes
Browse files Browse the repository at this point in the history
- More efficient In-Memory cache purging and compacting background workers
- Provide more ways to register a custom cache implementations
- Little optimizations
- Create interface for CustomRedisCacheService
- Fix dynamic access warnings for health checks
  • Loading branch information
JonathanBout committed Jan 27, 2025
1 parent 41478a4 commit 89533e3
Show file tree
Hide file tree
Showing 21 changed files with 429 additions and 203 deletions.
4 changes: 2 additions & 2 deletions extensions/Redis/CustomRedisCacheService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

namespace SimpleCDN.Extensions.Redis
{
public sealed class CustomRedisCacheService(IOptionsMonitor<RedisCacheConfiguration> options, IOptionsMonitor<CacheConfiguration> cacheOptions)
: IDistributedCache, IAsyncDisposable, IDisposable
internal sealed class CustomRedisCacheService(IOptionsMonitor<RedisCacheConfiguration> options, IOptionsMonitor<CacheConfiguration> cacheOptions)
: IRedisCacheService, IAsyncDisposable, IDisposable
{
private readonly IOptionsMonitor<RedisCacheConfiguration> options = options;
private readonly IOptionsMonitor<CacheConfiguration> cacheOptions = cacheOptions;
Expand Down
11 changes: 11 additions & 0 deletions extensions/Redis/IRedisCacheService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Microsoft.Extensions.Caching.Distributed;
using StackExchange.Redis;

namespace SimpleCDN.Extensions.Redis
{
public interface IRedisCacheService : IDistributedCache
{
public ConnectionMultiplexer GetRedisConnection();
public Task<ConnectionMultiplexer> GetRedisConnectionAsync();
}
}
6 changes: 2 additions & 4 deletions extensions/Redis/SimpleCDNBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,13 @@ public static class SimpleCDNBuilderExtensions
/// </summary>
public static ISimpleCDNBuilder AddRedisCache(this ISimpleCDNBuilder builder, Action<RedisCacheConfiguration> configure)
{
builder.Services.AddSingleton<CustomRedisCacheService>();

builder.Services.AddSingleton<IDistributedCache>(sp => sp.GetRequiredService<CustomRedisCacheService>());
builder.Services.AddSingleton<IRedisCacheService, CustomRedisCacheService>();

builder.Services.AddOptionsWithValidateOnStart<RedisCacheConfiguration>()
.Configure(configure)
.Validate<ILogger<RedisCacheConfiguration>>((config, logger) => config.Validate(logger), InvalidConfigurationMessage);

builder.UseCacheImplementation<CustomRedisCacheService>();
builder.UseCacheImplementation<IRedisCacheService>();

return builder;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection.Extensions;
using SimpleCDN.Services;
using SimpleCDN.Services.Caching;
using SimpleCDN.Services.Caching.Implementations;
using SimpleCDN.Services.Compression;
using SimpleCDN.Services.Compression.Implementations;
using SimpleCDN.Services.Implementations;
using System.ComponentModel;
using System.Text.Json;

namespace SimpleCDN.Configuration
{
/// <summary>
/// Provides extension methods for configuring SimpleCDN.
/// </summary>
public static class ConfigurationExtensions
public static class CDNBuilderExtensions
{
const string InvalidConfigurationMessage = "See the log meessages for details.";

Expand Down Expand Up @@ -53,15 +56,13 @@ public static ISimpleCDNBuilder AddSimpleCDN(this IServiceCollection services)

private class SimpleCDNBuilder : ISimpleCDNBuilder
{
private Type _cacheImplementationType = typeof(DisabledCache);

public SimpleCDNBuilder(IServiceCollection services)
{
Services = services;

Services.AddSingleton<IDistributedCache, DisabledCache>();
Services.AddSingleton<ICacheManager, CacheManager>();
Services.AddSingleton<ICacheImplementationResolver>(sp => new CacheImplementationResolver(sp, _cacheImplementationType));
UseCacheImplementation<DisabledCache>();
Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.Add(SourceGenerationContext.Default));
}

Expand All @@ -79,29 +80,33 @@ public ISimpleCDNBuilder ConfigureCaching(Action<CacheConfiguration> configure)
return this;
}

public ISimpleCDNBuilder DisableCaching() => UseCacheImplementation<DisabledCache>();

private void RemoveCacheImplementations()
{
Services.RemoveAll<ICacheImplementationResolver>();
}

public ISimpleCDNBuilder UseCacheImplementation<TImplementation>() where TImplementation : IDistributedCache
{
/*
* - if the service is registered as IDistributedCache
* - and
* | the implementation type is the specified type
* | or the implementation instance is the specified type
* | or the implementation factory is not null (we can't check the type)
*
* then we can use the specified cache implementation.
*/
if (!Services.Any(s => s.ServiceType == typeof(IDistributedCache)
&& (s.ImplementationType == typeof(TImplementation)
|| s.ImplementationInstance?.GetType() == typeof(TImplementation)
|| s.ImplementationFactory is not null)))
{
throw new InvalidOperationException("The specified cache implementation is not registered.");
}

_cacheImplementationType = typeof(TImplementation);
RemoveCacheImplementations();

Services.AddSingleton<ICacheImplementationResolver>(sp => new CacheImplementationResolver(sp, typeof(TImplementation)));
return this;
}

public ISimpleCDNBuilder UseCacheImplementation<TImplementation>(Func<IServiceProvider, TImplementation> resolve) where TImplementation : IDistributedCache
{
RemoveCacheImplementations();

Services.AddSingleton<ICacheImplementationResolver>(sp => new CacheImplementationResolver(resolve(sp)));
return this;
}

public ISimpleCDNBuilder UseCacheImplementation<TImplementation>(TImplementation implementation) where TImplementation : IDistributedCache
{
return UseCacheImplementation(_ => implementation);
}
}
}

Expand All @@ -125,11 +130,33 @@ public interface ISimpleCDNBuilder
/// </summary>
ISimpleCDNBuilder ConfigureCaching(Action<CacheConfiguration> configure);

/// <summary>
/// Disables caching. This may be overridden again when you register a cache implementation afterwards.
/// </summary>
ISimpleCDNBuilder DisableCaching();

/// <summary>
/// Configures SimpleCDN to use the specified cache implementation. This should be used when a custom cache implementation is used.
/// </summary>
/// <typeparam name="TImplementation">
/// The serivce type to use as the cache implementation. Must implement <see cref="IDistributedCache"/>.
/// </typeparam>
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
ISimpleCDNBuilder UseCacheImplementation<TImplementation>() where TImplementation : IDistributedCache;

/// <summary>
/// Configures SimpleCDN to use the specified cache implementation. This should be used when a custom cache implementation is used.
/// </summary>
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
ISimpleCDNBuilder UseCacheImplementation<TImplementation>(Func<IServiceProvider, TImplementation> provider) where TImplementation : IDistributedCache;

/// <summary>
/// Configures SimpleCDN to use the specified cache implementation. This should be used when a custom cache implementation is used.
/// </summary>
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
ISimpleCDNBuilder UseCacheImplementation<TImplementation>(TImplementation implementation) where TImplementation : IDistributedCache;
}
}
4 changes: 4 additions & 0 deletions src/core/Configuration/InMemoryCacheConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ public class InMemoryCacheConfiguration
public uint MaxSize { get; set; } = 500_000; // 500 MB

/// <summary>
/// This value is not used anymore, and will be removed in a future version.
/// The cache doesn't work with purging intervals anymore, and instead directly waits for the oldest item to expire.
/// <br/> <br/>
/// The interval at which the cache is purged of expired items in Minutes. Default is 5 minutes.
/// Set to 0 to disable automatic purging.
/// </summary>
[Obsolete("The cache doesn't work with purging intervals anymore. This value is not used anymore, and will be removed in a future version.")]
public TimeSpan PurgeInterval { get; set; } = TimeSpan.FromMinutes(5); // 5 minutes
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ public static ISimpleCDNBuilder AddInMemoryCache(this ISimpleCDNBuilder builder,
if (configure is not null)
builder.Services.Configure(configure);

builder.Services.AddSingleton<InMemoryCache>()
.AddHostedService(sp => sp.GetRequiredService<InMemoryCache>())
.AddSingleton<IDistributedCache>(sp => sp.GetRequiredService<InMemoryCache>());
builder.Services.AddSingleton<IDistributedCache, InMemoryCache>();
builder.UseCacheImplementation<InMemoryCache>();
return builder;
}
Expand Down
14 changes: 10 additions & 4 deletions src/core/GlobalConstants.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/*
* This file combines AssemblyInfo.cs and GlobalSuppressions.cs into a single file, as they are both used to configure the assembly.
* In here, we also have the Globals class for constant Assembly-wide values.
* This file combines AssemblyInfo.cs and GlobalSuppressions.cs into a single file,
* as they are both used to configure things for the whole assembly.
* In here, we also have the Globals class for constant Assembly-wide values, and the SourceGenerationContext for JSON serialization.
*/

using Microsoft.Extensions.Diagnostics.HealthChecks;
using SimpleCDN.Services.Caching.Implementations;
using SimpleCDN.Services.Implementations;
using System.Runtime.CompilerServices;
Expand All @@ -25,24 +27,28 @@ internal static class GlobalConstants
// NOTE: when changing this value, make sure to also change the references in
// other places like the Dockerfile health check (use the global search feature of your IDE!)
// but ultimately, this value should never change.
// This value should not start nor end with a slash.
/// <summary>
/// The root URL for the system files, like the style sheet.
/// </summary>
public const string SystemFilesRelativePath = "_cdn";

// This value should only contain letters, numbers, and dashes.
// But if you need to change it, you're probably doing something wrong.
/// <summary>
/// The key of the <see cref="HttpRequest.RouteValues"/> item that contains the CDN route.
/// This is used by the <see cref="CDNContext"/> to determine the base URL the CDN is placed at.
/// </summary>
public const string CDNRouteValueKey = "cdnRoute";
}

[JsonSourceGenerationOptions]
[JsonSerializable(typeof(SizeLimitedCacheDebugView))]
[JsonSourceGenerationOptions(Converters = [typeof(JsonStringEnumConverter<HealthStatus>)])]
#if DEBUG // only generate serializers for debug views in debug mode
[JsonSerializable(typeof(SizeLimitedCacheDebugView))]
[JsonSerializable(typeof(BasicDebugView))]
[JsonSerializable(typeof(DebugView))]
[JsonSerializable(typeof(DetailedDebugView))]
#endif
[JsonSerializable(typeof(object))]
internal partial class SourceGenerationContext : JsonSerializerContext;
}
47 changes: 12 additions & 35 deletions src/core/Helpers/InternalExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,29 @@

namespace SimpleCDN.Helpers
{
internal static partial class InternalExtensions
internal static class InternalExtensions
{
static readonly string[] sizeNames = ["", "k", "M", "G", "T"];
// only up to exabytes, because 1 yottabyte doesn't even fit in a ulong
// (I also don't think we are going to need it anytime soon)
static readonly string[] sizeNames = ["k", "M", "G", "T", "P", "E"];

public static string FormatByteCount(this long number)
{
var isNegative = false;
if (number < 0)
if (number is < 1000 and > -1000)
{
isNegative = true;
number = -number;
return number + "B";
}

var sizeNameIndex = 0;
int sizeNameIndex = 0;
double result = double.Abs(number);

double result = number;

for (; sizeNameIndex < sizeNames.Length - 1; sizeNameIndex++)
while (result >= 1000 && sizeNameIndex < sizeNames.Length - 1)
{
var div = result / 1000;
if (div < 1)
break;

result = div;
result /= 1000;
sizeNameIndex++;
}

if (isNegative)
result = -result;

return $"{result:0.##}{sizeNames[sizeNameIndex]}B";
return $"{(number < 0 ? "-": "")}{result:0.##}{sizeNames[sizeNameIndex]}B";
}

/// <summary>
Expand Down Expand Up @@ -144,22 +137,6 @@ public static string ForLog(this ReadOnlySpan<char> input)
return builder.ToString();
}

public static bool HasAnyFlag<T>(this T enumValue, T flag) where T : Enum, IConvertible
{
var intValue = enumValue.ToInt64(null);
var intFlag = flag.ToInt64(null);

return (intValue & intFlag) != 0;
}

public static bool DoesNotHaveFlag<T>(this T enumValue, T flag) where T : Enum, IConvertible
{
var intValue = enumValue.ToInt64(null);
var intFlag = flag.ToInt64(null);

return (intValue & intFlag) == 0;
}

public static void RemoveWhere<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, Func<KeyValuePair<TKey, TValue>, bool> predicate)
{
foreach ((TKey key, _) in dictionary.Where(predicate).ToList())
Expand Down
5 changes: 5 additions & 0 deletions src/core/Services/Caching/ICacheDebugInfoProvider.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
namespace SimpleCDN.Services.Caching
{
#if DEBUG
/// <summary>
/// Provides debug information about the cache implementation.
/// </summary>
internal interface ICacheDebugInfoProvider
{
internal object GetDebugInfo();
}
#endif
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,23 @@ namespace SimpleCDN.Services.Caching.Implementations
/// <summary>
/// Resolves the selected cache implementation from the registered services.
/// </summary>
internal class CacheImplementationResolver(IServiceProvider services, Type implementationType) : ICacheImplementationResolver
internal class CacheImplementationResolver : ICacheImplementationResolver
{
private IDistributedCache? _impl;
public IDistributedCache Implementation
public CacheImplementationResolver(IServiceProvider services, Type implementationType)
{
get
{
_impl ??= services.GetServices<IDistributedCache>().FirstOrDefault(s => s.GetType() == implementationType);

if (_impl is null)
throw new InvalidOperationException($"The specified cache implementation ({implementationType.Name}) is not registered.");
var resolved = services.GetService(implementationType)
?? services.GetServices<IDistributedCache>().FirstOrDefault(s => s.GetType() == implementationType);

return _impl;
if (resolved is not IDistributedCache dc)
{
throw new InvalidOperationException($"The specified cache implementation type '{implementationType}' is not registered.");
}

Implementation = dc;
}

public CacheImplementationResolver(IDistributedCache implementation) => Implementation = implementation;

public IDistributedCache Implementation { get; }
}
}
Loading

0 comments on commit 89533e3

Please sign in to comment.