diff --git a/Iceshrimp.Backend/Core/Extensions/DistributedCacheExtensions.cs b/Iceshrimp.Backend/Core/Extensions/DistributedCacheExtensions.cs index b7187f3c..859059fd 100644 --- a/Iceshrimp.Backend/Core/Extensions/DistributedCacheExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/DistributedCacheExtensions.cs @@ -105,4 +105,43 @@ public static class DistributedCacheExtensions : new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = ttl }; await cache.SetAsync(key, stream.ToArray(), options); } + + public static async Task CacheAsync( + this IDistributedCache cache, string key, TimeSpan ttl, Func> fetcher, Type type, + bool renew = false + ) + { + var res = await cache.GetAsync(key); + if (res != null) return; + await SetAsync(cache, key, await fetcher(), type, ttl, renew); + } + + public static async Task CacheAsync( + this IDistributedCache cache, string key, TimeSpan ttl, Func fetcher, Type type, bool renew = false + ) + { + var res = await cache.GetAsync(key); + if (res != null) return; + await SetAsync(cache, key, fetcher(), type, ttl, renew); + } + + public static async Task CacheAsync( + this IDistributedCache cache, string key, TimeSpan ttl, object? value, Type type, bool renew = false + ) + { + await CacheAsync(cache, key, ttl, () => value, type, renew); + } + + private static async Task SetAsync( + this IDistributedCache cache, string key, object? data, Type type, TimeSpan ttl, bool sliding = false + ) + { + using var stream = new MemoryStream(); + await JsonSerializer.SerializeAsync(stream, data, type, Options); + stream.Position = 0; + var options = sliding + ? new DistributedCacheEntryOptions { SlidingExpiration = ttl } + : new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = ttl }; + await cache.SetAsync(key, stream.ToArray(), options); + } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Services/MetaService.cs b/Iceshrimp.Backend/Core/Services/MetaService.cs index d6110151..2b99c86d 100644 --- a/Iceshrimp.Backend/Core/Services/MetaService.cs +++ b/Iceshrimp.Backend/Core/Services/MetaService.cs @@ -11,10 +11,10 @@ namespace Iceshrimp.Backend.Core.Services; public class MetaService(IServiceScopeFactory scopeFactory, IDistributedCache cache) { public async Task Get(Meta meta) where T : class? => - await cache.FetchAsync($"cache:meta:{meta.Key}", meta.Ttl, async () => await Fetch(meta)); + await cache.FetchAsync($"meta:{meta.Key}", meta.Ttl, async () => await Fetch(meta), true); public async Task GetValue(Meta meta) where T : struct => - await cache.FetchAsyncValue($"cache:meta:{meta.Key}", meta.Ttl, async () => await Fetch(meta)); + await cache.FetchAsyncValue($"meta:{meta.Key}", meta.Ttl, async () => await Fetch(meta), true); public async Task EnsureSet(Meta meta, T value) => await EnsureSet(meta, () => value); @@ -47,35 +47,28 @@ public class MetaService(IServiceScopeFactory scopeFactory, IDistributedCache ca public async Task WarmupCache() { - var entities = typeof(MetaEntity).GetMembers(BindingFlags.Static | BindingFlags.Public) - .OfType(); + var entities = + typeof(MetaEntity) + .GetMembers(BindingFlags.Static | BindingFlags.Public) + .OfType() + .Where(p => p.FieldType.IsAssignableTo(typeof(Meta))) + .Select(p => p.GetValue(this)) + .Cast(); - foreach (var entity in entities) - { - var value = entity.GetValue(this); - var type = entity.FieldType; + var store = await GetDbContext().MetaStore.ToListAsync(); + var dict = entities.ToDictionary(p => p, p => p.ConvertCache(store.FirstOrDefault(i => i.Key == p.Key)?.Value)); + var invalid = dict.Where(p => !p.Key.IsNullable && p.Value == null).Select(p => p.Key.Key).ToList(); + if (invalid.Count != 0) + throw new Exception($"Invalid meta store entries: [{string.Join(", ", invalid)}] must not be null"); - while (type?.GenericTypeArguments == null || - type.GenericTypeArguments.Length == 0 || - type.GetGenericTypeDefinition() != typeof(Meta<>)) - { - if (type == typeof(object) || type == null) - continue; - - type = type.BaseType; - } - - var genericType = type.GenericTypeArguments.First(); - var task = typeof(MetaService) - .GetMethod(nameof(Get))! - .MakeGenericMethod(genericType) - .Invoke(this, [value]); - - await (Task)task!; - } + foreach (var entry in dict) + await Cache(entry.Key, entry.Value); } - private async Task Fetch(Meta meta) => meta.GetConverter(await Fetch(meta.Key)); + private async Task Cache(Meta meta, object? value) => + await cache.CacheAsync($"meta:{meta.Key}", meta.Ttl, value, meta.Type, true); + + private async Task Fetch(Meta meta) => meta.ConvertGet(await Fetch(meta.Key)); private async Task Fetch(string key) => await GetDbContext().MetaStore.Where(p => p.Key == key).Select(p => p.Value).FirstOrDefaultAsync(); @@ -107,7 +100,7 @@ public class MetaService(IServiceScopeFactory scopeFactory, IDistributedCache ca } } - await cache.SetAsync($"cache:meta:{key}", value, ttl, true); + await cache.SetAsync($"meta:{key}", value, ttl, true); } private DatabaseContext GetDbContext() => @@ -122,12 +115,25 @@ public static class MetaEntity public static readonly NullableStringMeta InstanceDescription = new("instance_description"); } -public class Meta(string key, TimeSpan? ttl, Func getConverter, Func setConverter) +public class Meta( + string key, + TimeSpan? ttl, + Func getConverter, + Func setConverter, + bool isNullable = true +) : Meta(key, ttl, typeof(T), isNullable, val => val != null ? getConverter(val) : null) { - public string Key => key; - public TimeSpan Ttl => ttl ?? TimeSpan.FromDays(30); - public Func GetConverter => getConverter; - public Func ConvertSet => setConverter; + public Func ConvertGet => getConverter; + public Func ConvertSet => setConverter; +} + +public class Meta(string key, TimeSpan? ttl, Type type, bool isNullable, Func cacheConverter) +{ + public Type Type => type; + public bool IsNullable => isNullable; + public string Key => key; + public TimeSpan Ttl => ttl ?? TimeSpan.FromDays(30); + public Func ConvertCache => cacheConverter; } public class StringMeta(string key, TimeSpan? ttl = null) : NonNullableMeta(key, ttl, val => val, val => val); @@ -145,7 +151,8 @@ public class NullableIntMeta(string key, TimeSpan? ttl = null) public class NonNullableMeta(string key, TimeSpan? ttl, Func getConverter, Func setConverter) : Meta(key, ttl, val => getConverter(val ?? throw new Exception($"Fetched meta value {key} was null")), - setConverter) where T : class; + setConverter, + false) where T : class; public class NonNullableValueMeta( string key, @@ -154,4 +161,5 @@ public class NonNullableValueMeta( Func setConverter ) : Meta(key, ttl, val => getConverter(val ?? throw new Exception($"Fetched meta value {key} was null")), - setConverter) where T : struct; \ No newline at end of file + setConverter, + false) where T : struct; \ No newline at end of file