Merge pull request #838 from yileicn/RefactorCache

refactor cache
This commit is contained in:
Haiping 2025-01-22 09:22:51 -06:00 committed by GitHub
commit 61e3bb83e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 185 additions and 163 deletions

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(TargetFramework)</TargetFramework>
@ -37,7 +37,7 @@
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Rougamo.Fody" Version="4.0.0" />
<PackageReference Include="Rougamo.Fody" Version="4.0.4" />
<PackageReference Include="AspectInjector" Version="2.8.2" />
</ItemGroup>

View file

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BotSharp.Abstraction.Infrastructures.Enums
{
public enum CacheType
{
MemoryCache,
RedisCache
}
}

View file

@ -6,4 +6,5 @@ public interface ICacheService
Task<object> GetAsync(string key, Type type);
Task SetAsync<T>(string key, T value, TimeSpan? expiry);
Task RemoveAsync(string key);
Task ClearCacheAsync(string prefix);
}

View file

@ -6,33 +6,37 @@ using Rougamo.Context;
namespace BotSharp.Core.Infrastructures;
public class SharpCacheAttribute : MoAttribute
public class SharpCacheAttribute : AsyncMoAttribute
{
public static IServiceProvider Services { get; set; } = null!;
private static readonly object NullMarker = new { __is_null = "$_is_null" };
private int _minutes;
private readonly int _minutes;
private readonly bool _perInstanceCache;
private readonly ICacheService _cache;
private readonly SharpCacheSettings _settings;
public SharpCacheAttribute(int minutes = 60)
public SharpCacheAttribute(int minutes = 60, bool perInstanceCache = false)
{
_minutes = minutes;
_perInstanceCache = perInstanceCache;
_cache = Services.GetRequiredService<ICacheService>();
_settings = Services.GetRequiredService<SharpCacheSettings>();
}
public override void OnEntry(MethodContext context)
public override async ValueTask OnEntryAsync(MethodContext context)
{
var settings = Services.GetRequiredService<SharpCacheSettings>();
if (!settings.Enabled)
if (!_settings.Enabled)
{
return;
}
var cache = Services.GetRequiredService<ICacheService>();
var key = GetCacheKey(settings, context);
var value = cache.GetAsync(key, context.TaskReturnType).Result;
var key = GetCacheKey(context);
var value = await _cache.GetAsync(key, context.TaskReturnType);
if (value != null)
{
// check if the cache is out of date
var isOutOfDate = IsOutOfDate(context, value).Result;
var isOutOfDate = await IsOutOfDate(context, value);
if (!isOutOfDate)
{
@ -41,10 +45,9 @@ public class SharpCacheAttribute : MoAttribute
}
}
public override void OnSuccess(MethodContext context)
public override async ValueTask OnSuccessAsync(MethodContext context)
{
var settings = Services.GetRequiredService<SharpCacheSettings>();
if (!settings.Enabled)
if (!_settings.Enabled)
{
return;
}
@ -57,12 +60,10 @@ public class SharpCacheAttribute : MoAttribute
return;
}
var cache = Services.GetRequiredService<ICacheService>();
if (context.ReturnValue != null)
{
var key = GetCacheKey(settings, context);
cache.SetAsync(key, context.ReturnValue, new TimeSpan(0, _minutes, 0)).Wait();
var key = GetCacheKey(context);
await _cache.SetAsync(key, context.ReturnValue, new TimeSpan(0, _minutes, 0));
}
}
@ -71,25 +72,45 @@ public class SharpCacheAttribute : MoAttribute
return Task.FromResult(false);
}
private string GetCacheKey(SharpCacheSettings settings, MethodContext context)
{
var key = settings.Prefix + ":" + context.Method.Name;
foreach (var arg in context.Arguments)
{
if (arg is null)
{
key += "-" + "<NULL>";
}
else if (arg is ICacheKey withCacheKey)
{
key += "-" + withCacheKey.GetCacheKey();
}
else
{
key += "-" + arg.ToString();
}
}
return key;
private string GetCacheKey(MethodContext context)
{
var prefixKey = GetPrefixKey(context.Method.Name);
var argsKey = string.Join("_", context.Arguments.Select(arg => GetCacheKeyByArg(arg)));
if (_perInstanceCache && context.Target != null)
{
return $"{prefixKey}-{context.Target.GetHashCode()}_{argsKey}";
}
else
{
return $"{prefixKey}_{argsKey}";
}
}
private string GetPrefixKey(string name)
{
return _settings.Prefix + ":" + name;
}
private string GetCacheKeyByArg(object? arg)
{
if (arg is null)
{
return NullMarker.GetHashCode().ToString();
}
else if (arg is ICacheKey withCacheKey)
{
return withCacheKey.GetCacheKey();
}
else
{
return arg.GetHashCode().ToString();
}
}
public async Task ClearCacheAsync()
{
await _cache.ClearCacheAsync(_settings.Prefix);
}
}

View file

@ -2,6 +2,7 @@ namespace BotSharp.Abstraction.Infrastructures;
public class SharpCacheSettings
{
public bool Enabled { get; set; } = false;
public string Prefix { get; set; } = "cache";
public bool Enabled { get; set; } = true;
public CacheType CacheType { get; set; } = Enums.CacheType.MemoryCache;
public string Prefix { get; set; } = "cache";
}

View file

@ -5,7 +5,7 @@ namespace BotSharp.Core.Agents.Services;
public partial class AgentService
{
#if !DEBUG
[MemoryCache(10 * 60, perInstanceCache: true)]
[SharpCache(10, perInstanceCache: true)]
#endif
public async Task<PagedItems<Agent>> GetAgents(AgentFilter filter)
{
@ -27,7 +27,7 @@ public partial class AgentService
}
#if !DEBUG
[MemoryCache(10 * 60, perInstanceCache: true)]
[SharpCache(10, perInstanceCache: true)]
#endif
public async Task<Agent> GetAgent(string id)
{

View file

@ -7,7 +7,7 @@ public partial class AgentService
{
public static ConcurrentDictionary<string, Dictionary<string, string>> AgentParameterTypes = new();
[MemoryCache(10 * 60, perInstanceCache: true)]
[SharpCache(10, perInstanceCache: true)]
public async Task<Agent> LoadAgent(string id, bool loadUtility = true)
{
if (string.IsNullOrEmpty(id) || id == Guid.Empty.ToString())

View file

@ -188,13 +188,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Aspects.Cache" Version="2.0.4" />
<PackageReference Include="DistributedLock.Redis" Version="1.0.3" />
<PackageReference Include="EntityFrameworkCore.BootKit" Version="8.8.0" />
<PackageReference Include="Fluid.Core" Version="2.11.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageReference Include="Nanoid" Version="3.1.0" />
<PackageReference Include="Rougamo.Fody" Version="4.0.4" />
</ItemGroup>
<ItemGroup>

View file

@ -14,6 +14,7 @@ using BotSharp.Core.Infrastructures.Events;
using BotSharp.Core.Roles.Services;
using BotSharp.Abstraction.Templating;
using BotSharp.Core.Templating;
using BotSharp.Abstraction.Infrastructures.Enums;
namespace BotSharp.Core;
@ -34,15 +35,9 @@ public static class BotSharpCoreExtensions
services.AddScoped<IUserService, UserService>();
services.AddScoped<ProcessorFactory>();
// Register cache service
var cacheSettings = new SharpCacheSettings();
config.Bind("SharpCache", cacheSettings);
services.AddSingleton(x => cacheSettings);
services.AddSingleton<ICacheService, RedisCacheService>();
AddRedisEvents(services, config);
services.AddMemoryCache();
// Register cache service
AddCacheServices(services, config);
RegisterPlugins(services, config);
AddBotSharpOptions(services, configOptions);
@ -50,6 +45,22 @@ public static class BotSharpCoreExtensions
return services;
}
private static void AddCacheServices(IServiceCollection services, IConfiguration config)
{
services.AddMemoryCache();
var cacheSettings = new SharpCacheSettings();
config.Bind("SharpCache", cacheSettings);
services.AddSingleton(x => cacheSettings);
services.AddSingleton<MemoryCacheService>();
services.AddSingleton<RedisCacheService>();
services.AddSingleton<ICacheService>(sp =>
cacheSettings.CacheType switch
{
CacheType.RedisCache => sp.GetRequiredService<RedisCacheService>(),
_ => sp.GetRequiredService<MemoryCacheService>(),
});
}
public static IServiceCollection UsingSqlServer(this IServiceCollection services, IConfiguration config)
{
services.AddScoped<IBotSharpRepository>(sp =>

View file

@ -1,18 +1,15 @@
using BotSharp.Abstraction.Infrastructures;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
namespace BotSharp.Core.Infrastructures;
public class MemoryCacheService : ICacheService
{
private static IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions
{
});
private readonly BotSharpDatabaseSettings _settings;
private static readonly MemoryCache _cache = new MemoryCache(new OptionsWrapper<MemoryCacheOptions>(new MemoryCacheOptions()));
public MemoryCacheService(BotSharpDatabaseSettings settings)
public MemoryCacheService()
{
_settings = settings;
}
public async Task<T?> GetAsync<T>(string key)
@ -37,4 +34,9 @@ public class MemoryCacheService : ICacheService
{
_cache.Remove(key);
}
public async Task ClearCacheAsync(string prefix)
{
_cache.Compact(1.0);
}
}

View file

@ -0,0 +1,69 @@
using BotSharp.Abstraction.Infrastructures;
using Newtonsoft.Json;
using StackExchange.Redis;
using System.Linq;
namespace BotSharp.Core.Infrastructures;
public class RedisCacheService : ICacheService
{
private IConnectionMultiplexer _redis = null!;
public RedisCacheService(IConnectionMultiplexer redis)
{
_redis = redis;
}
public async Task<T?> GetAsync<T>(string key)
{
var db = _redis.GetDatabase();
var value = await db.StringGetAsync(key);
if (value.HasValue)
{
return JsonConvert.DeserializeObject<T>(value);
}
return default;
}
public async Task<object> GetAsync(string key, Type type)
{
var db = _redis.GetDatabase();
var value = await db.StringGetAsync(key);
if (value.HasValue)
{
return JsonConvert.DeserializeObject(value, type);
}
return default;
}
public async Task SetAsync<T>(string key, T value, TimeSpan? expiry)
{
var db = _redis.GetDatabase();
await db.StringSetAsync(key, JsonConvert.SerializeObject(value), expiry);
}
public async Task RemoveAsync(string key)
{
var db = _redis.GetDatabase();
await db.KeyDeleteAsync(key);
}
public async Task ClearCacheAsync(string prefix)
{
var db = _redis.GetDatabase();
var server = _redis.GetServer(_redis.GetEndPoints().First());
const int pageSize = 1000;
var keys = server.Keys(pattern: $"{prefix}*", pageSize: pageSize).ToList();
for (int i = 0; i < keys.Count; i += pageSize)
{
var batch = keys.Skip(i).Take(pageSize).ToArray();
await db.KeyDeleteAsync(batch);
}
}
}

View file

@ -1,95 +0,0 @@
using BotSharp.Abstraction.Infrastructures;
using Newtonsoft.Json;
using StackExchange.Redis;
namespace BotSharp.Core.Infrastructures;
public class RedisCacheService : ICacheService
{
private readonly BotSharpDatabaseSettings _settings;
private static ConnectionMultiplexer redis = null!;
public RedisCacheService(BotSharpDatabaseSettings settings)
{
_settings = settings;
}
public async Task<T?> GetAsync<T>(string key)
{
if (string.IsNullOrEmpty(_settings.Redis))
{
return default;
}
if (redis == null)
{
redis = ConnectionMultiplexer.Connect(_settings.Redis);
}
var db = redis.GetDatabase();
var value = await db.StringGetAsync(key);
if (value.HasValue)
{
return JsonConvert.DeserializeObject<T>(value);
}
return default;
}
public async Task<object> GetAsync(string key, Type type)
{
if (string.IsNullOrEmpty(_settings.Redis))
{
return default;
}
if (redis == null)
{
redis = ConnectionMultiplexer.Connect(_settings.Redis);
}
var db = redis.GetDatabase();
var value = await db.StringGetAsync(key);
if (value.HasValue)
{
return JsonConvert.DeserializeObject(value, type);
}
return default;
}
public async Task SetAsync<T>(string key, T value, TimeSpan? expiry)
{
if (string.IsNullOrEmpty(_settings.Redis))
{
return;
}
if (redis == null)
{
redis = ConnectionMultiplexer.Connect(_settings.Redis);
}
var db = redis.GetDatabase();
await db.StringSetAsync(key, JsonConvert.SerializeObject(value), expiry);
}
public async Task RemoveAsync(string key)
{
if (string.IsNullOrEmpty(_settings.Redis))
{
return;
}
if (redis == null)
{
redis = ConnectionMultiplexer.Connect(_settings.Redis);
}
var db = redis.GetDatabase();
await db.KeyDeleteAsync(key);
}
}

View file

@ -48,10 +48,8 @@ public static class Utilities
public static void ClearCache()
{
// Clear whole cache.
if (new MemoryCacheAttribute(0).Cache is MemoryCache memcache)
{
memcache.Compact(100);
}
var sharpCache = new SharpCacheAttribute(0);
sharpCache.ClearCacheAsync().ConfigureAwait(false).GetAwaiter().GetResult();
}
public static string HideMiddleDigits(string input, bool isEmail = false)

View file

@ -1,5 +1,6 @@
using BotSharp.Abstraction.Routing.Models;
using BotSharp.Abstraction.Routing.Settings;
using BotSharp.Core.Infrastructures;
namespace BotSharp.Core.Routing;
@ -74,7 +75,7 @@ public partial class RoutingService : IRoutingService
}
#if !DEBUG
[MemoryCache(10 * 60)]
[SharpCache(10)]
#endif
protected RoutingRule[] GetRoutingRecords()
{
@ -99,7 +100,7 @@ public partial class RoutingService : IRoutingService
}
#if !DEBUG
[MemoryCache(10 * 60)]
[SharpCache(10)]
#endif
public RoutableAgent[] GetRoutableAgents(List<string> profiles)
{

View file

@ -377,7 +377,7 @@ public class UserService : IUserService
return await _cacheService.GetAsync<DateTime>(GetUserTokenExpiresCacheKey(_user.Id));
}
[MemoryCache(10 * 60, perInstanceCache: true)]
[SharpCache(10, perInstanceCache: true)]
public async Task<User> GetMyProfile()
{
var db = _services.GetRequiredService<IBotSharpRepository>();
@ -398,7 +398,7 @@ public class UserService : IUserService
return user;
}
[MemoryCache(10 * 60, perInstanceCache: true)]
[SharpCache(10, perInstanceCache: true)]
public async Task<User> GetUser(string id)
{
var db = _services.GetRequiredService<IBotSharpRepository>();

View file

@ -42,5 +42,4 @@ global using BotSharp.Core.Agents.Services;
global using BotSharp.Core.Conversations.Services;
global using BotSharp.Core.Infrastructures;
global using BotSharp.Core.Users.Services;
global using Aspects.Cache;
global using BotSharp.Abstraction.Infrastructures.Events;