using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using StackExchange.Redis; using Tiku.Application.Security; using Tiku.Application.Tenancy; namespace Tiku.Infrastructure.Security; internal sealed class TenantFeatureCacheInvalidator( IMemoryCache memoryCache, IServiceProvider serviceProvider, ITenantRuntimeCacheInvalidator runtimeCacheInvalidator, ILogger logger) : ITenantFeatureCacheInvalidator, IHostedService { private const string ChannelName = "tiku:tenant-feature-snapshot:invalidate:v1"; private ISubscriber? subscriber; public async Task InvalidateAsync(Guid tenantId, CancellationToken cancellationToken = default) { RemoveMemory(tenantId); await runtimeCacheInvalidator.InvalidateAsync(tenantId, cancellationToken); var distributedCache = serviceProvider.GetService(); if (distributedCache is not null) { try { await Task.WhenAll( distributedCache.RemoveAsync(TenantFeatureSnapshotProvider.CacheKey(tenantId, FeatureAccessOperation.Read), cancellationToken), distributedCache.RemoveAsync(TenantFeatureSnapshotProvider.CacheKey(tenantId, FeatureAccessOperation.Write), cancellationToken)); } catch (Exception exception) when (exception is not OperationCanceledException) { logger.LogWarning(exception, "Tenant feature distributed cache invalidation failed for tenant {TenantId}.", tenantId); } } var connection = serviceProvider.GetService(); if (connection is not null) { try { await connection.GetSubscriber() .PublishAsync(RedisChannel.Literal(ChannelName), tenantId.ToString("N")) .WaitAsync(cancellationToken); } catch (Exception exception) when (exception is RedisException or TimeoutException) { logger.LogWarning(exception, "Tenant feature L1 invalidation broadcast failed for tenant {TenantId}.", tenantId); } } } public async Task StartAsync(CancellationToken cancellationToken) { var connection = serviceProvider.GetService(); if (connection is null) { return; } subscriber = connection.GetSubscriber(); await subscriber.SubscribeAsync(RedisChannel.Literal(ChannelName), (_, value) => { if (Guid.TryParseExact(value.ToString(), "N", out var tenantId)) { RemoveMemory(tenantId); } }).WaitAsync(cancellationToken); } public async Task StopAsync(CancellationToken cancellationToken) { if (subscriber is not null) { await subscriber.UnsubscribeAsync(RedisChannel.Literal(ChannelName)).WaitAsync(cancellationToken); } } private void RemoveMemory(Guid tenantId) { memoryCache.Remove(TenantFeatureSnapshotProvider.CacheKey(tenantId, FeatureAccessOperation.Read)); memoryCache.Remove(TenantFeatureSnapshotProvider.CacheKey(tenantId, FeatureAccessOperation.Write)); } }