forked from gongxuegit/tiku-backend.net
perf: optimize authorization scoreline and workers
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
using Tiku.Application.Tenancy;
|
||||
|
||||
namespace Tiku.Infrastructure.Tenancy;
|
||||
|
||||
internal sealed class NullTenantPublicCacheInvalidator : ITenantPublicCacheInvalidator
|
||||
{
|
||||
public Task InvalidateAsync(Guid tenantId, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
}
|
||||
@@ -1,11 +1,21 @@
|
||||
using Npgsql;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Tiku.Application.Tenancy;
|
||||
using Tiku.Domain.Tenancy;
|
||||
|
||||
namespace Tiku.Infrastructure.Tenancy;
|
||||
|
||||
public sealed class TenantDirectory(NpgsqlDataSource dataSource) : ITenantDirectory
|
||||
public sealed class TenantDirectory(
|
||||
NpgsqlDataSource dataSource,
|
||||
IMemoryCache memoryCache,
|
||||
IServiceProvider serviceProvider) : ITenantDirectory
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
|
||||
private sealed record CacheEnvelope(bool Found, TenantDirectoryEntry? Entry);
|
||||
|
||||
public Task<TenantDirectoryEntry?> FindByHostAsync(
|
||||
string host,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -19,7 +29,7 @@ public sealed class TenantDirectory(NpgsqlDataSource dataSource) : ITenantDirect
|
||||
and t.status = 'active'
|
||||
limit 1
|
||||
""";
|
||||
return FindAsync(sql, host, cancellationToken);
|
||||
return FindAsync(sql, "host", Normalize(host), cancellationToken);
|
||||
}
|
||||
|
||||
public Task<TenantDirectoryEntry?> FindByCodeAsync(
|
||||
@@ -33,29 +43,90 @@ public sealed class TenantDirectory(NpgsqlDataSource dataSource) : ITenantDirect
|
||||
and t.status = 'active'
|
||||
limit 1
|
||||
""";
|
||||
return FindAsync(sql, tenantCode, cancellationToken);
|
||||
return FindAsync(sql, "code", Normalize(tenantCode), cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<TenantDirectoryEntry?> FindAsync(
|
||||
string sql,
|
||||
string kind,
|
||||
string lookup,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var cacheKey = $"tenant-directory:v1:{kind}:{lookup}";
|
||||
if (memoryCache.TryGetValue<CacheEnvelope>(cacheKey, out var memoryValue) && memoryValue is not null)
|
||||
{
|
||||
return memoryValue.Entry;
|
||||
}
|
||||
|
||||
var distributedCache = serviceProvider.GetService<IDistributedCache>();
|
||||
if (distributedCache is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = await distributedCache.GetStringAsync(cacheKey, cancellationToken);
|
||||
if (json is not null)
|
||||
{
|
||||
var distributedValue = JsonSerializer.Deserialize<CacheEnvelope>(json, SerializerOptions);
|
||||
if (distributedValue is not null)
|
||||
{
|
||||
memoryCache.Set(cacheKey, distributedValue, distributedValue.Found
|
||||
? TimeSpan.FromSeconds(30)
|
||||
: TimeSpan.FromSeconds(20));
|
||||
return distributedValue.Entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||
{
|
||||
// Tenant resolution must fall back to PostgreSQL when Redis is unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
await using var command = dataSource.CreateCommand(sql);
|
||||
command.Parameters.AddWithValue("lookup", lookup);
|
||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||
if (!await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
await StoreAsync(distributedCache, cacheKey, new CacheEnvelope(false, null), TimeSpan.FromSeconds(20), cancellationToken);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new TenantDirectoryEntry(
|
||||
var result = new TenantDirectoryEntry(
|
||||
reader.GetGuid(0),
|
||||
reader.GetString(1),
|
||||
reader.GetString(2),
|
||||
ParseEnum<TenantStatus>(reader.GetString(3)),
|
||||
ParseEnum<TenantMode>(reader.GetString(4)),
|
||||
reader.IsDBNull(5) ? null : reader.GetString(5));
|
||||
await StoreAsync(distributedCache, cacheKey, new CacheEnvelope(true, result), TimeSpan.FromSeconds(300), cancellationToken);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task StoreAsync(
|
||||
IDistributedCache? distributedCache,
|
||||
string cacheKey,
|
||||
CacheEnvelope value,
|
||||
TimeSpan distributedDuration,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
memoryCache.Set(cacheKey, value, value.Found ? TimeSpan.FromSeconds(30) : TimeSpan.FromSeconds(20));
|
||||
if (distributedCache is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await distributedCache.SetStringAsync(
|
||||
cacheKey,
|
||||
JsonSerializer.Serialize(value, SerializerOptions),
|
||||
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = distributedDuration },
|
||||
cancellationToken);
|
||||
}
|
||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||
{
|
||||
// L1 remains usable and the next miss will fall back to PostgreSQL.
|
||||
}
|
||||
}
|
||||
|
||||
private static TEnum ParseEnum<TEnum>(string value)
|
||||
@@ -63,4 +134,6 @@ public sealed class TenantDirectory(NpgsqlDataSource dataSource) : ITenantDirect
|
||||
{
|
||||
return Enum.Parse<TEnum>(value.Replace("_", string.Empty, StringComparison.Ordinal), true);
|
||||
}
|
||||
|
||||
private static string Normalize(string value) => value.Trim().ToLowerInvariant();
|
||||
}
|
||||
|
||||
@@ -115,9 +115,15 @@ public sealed class HttpDomainGatewayProvisioner(
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TenantRuntimeCacheInvalidator(IMemoryCache cache) : ITenantRuntimeCacheInvalidator
|
||||
public sealed class TenantRuntimeCacheInvalidator(
|
||||
IMemoryCache cache,
|
||||
ITenantPublicCacheInvalidator publicCacheInvalidator) : ITenantRuntimeCacheInvalidator
|
||||
{
|
||||
public void Invalidate(Guid tenantId) => cache.Remove($"tenant-runtime:{tenantId:N}");
|
||||
public async Task InvalidateAsync(Guid tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cache.Remove($"tenant-runtime:{tenantId:N}");
|
||||
await publicCacheInvalidator.InvalidateAsync(tenantId, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TenantDomainLifecycleService(
|
||||
@@ -185,7 +191,7 @@ public sealed class TenantDomainLifecycleService(
|
||||
domain.TlsReadyAt ??= DateTimeOffset.UtcNow;
|
||||
domain.Status = TenantDomainStatus.Active;
|
||||
domain.LastFailureReason = null;
|
||||
cacheInvalidator.Invalidate(domain.TenantId);
|
||||
await cacheInvalidator.InvalidateAsync(domain.TenantId, cancellationToken);
|
||||
}
|
||||
|
||||
private static void Fail(TenantDomain domain, bool configured, string? reason)
|
||||
|
||||
@@ -13,7 +13,8 @@ namespace Tiku.Infrastructure.Tenancy;
|
||||
public sealed class TenantFrontendConfigService(
|
||||
TikuDbContext dbContext,
|
||||
IMemoryCache cache,
|
||||
IFeatureAccessService featureAccessService) : ITenantFrontendConfigService
|
||||
IFeatureAccessService featureAccessService,
|
||||
ITenantPublicCacheInvalidator publicCacheInvalidator) : ITenantFrontendConfigService
|
||||
{
|
||||
private static readonly TimeSpan RuntimeCacheDuration = TimeSpan.FromMinutes(2);
|
||||
|
||||
@@ -77,6 +78,7 @@ public sealed class TenantFrontendConfigService(
|
||||
config.PublishedAt = DateTimeOffset.UtcNow;
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
cache.Remove(CacheKey(tenantId));
|
||||
await publicCacheInvalidator.InvalidateAsync(tenantId, cancellationToken);
|
||||
return ToItem(config);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user