Files
tiku-backend.net/Tiku.Worker/WorkerDependencyInjection.cs
xiong 603bc24c26
Some checks are pending
ci / release-gate (push) Waiting to run
feat(cache): adopt FusionCache for business caching
2026-08-05 09:30:48 +08:00

187 lines
11 KiB
C#

using Microsoft.Extensions.Options;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
using Serilog;
using Tiku.Application;
using Tiku.Application.PlatformBilling;
using Tiku.Application.Security;
using Tiku.Application.Storage;
using Tiku.Application.Tenancy;
using Tiku.Infrastructure;
using Tiku.Infrastructure.Caching;
using Tiku.Infrastructure.Assets;
using Tiku.Infrastructure.Observability;
using Tiku.Infrastructure.Security;
using Tiku.Infrastructure.Storage;
namespace Tiku.Worker;
internal static class WorkerDependencyInjection
{
internal static HostApplicationBuilder AddWorkerServices(this HostApplicationBuilder builder)
{
builder.Services.AddSerilog((services, configuration) => configuration
.ReadFrom.Configuration(builder.Configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext(),
true);
var connectionString = builder.Configuration.GetConnectionString("Database") ??
builder.Configuration["DATABASE_URL"] ??
(builder.Environment.IsDevelopment()
? $"Host=localhost;Database=tiku;Username={Environment.UserName}"
: throw new InvalidOperationException(
"Database connection is required outside Development. Configure ConnectionStrings:Database or DATABASE_URL."));
builder.Services.AddApplication();
builder.Services.AddAuthentication();
builder.Services.AddDataProtection();
builder.Services.AddInfrastructure(connectionString);
var redisConnectionString =
builder.Configuration.GetConnectionString("Redis") ?? builder.Configuration["REDIS_URL"];
builder.Services.AddOptions<AuthorizationCacheOptions>()
.Bind(builder.Configuration.GetSection(AuthorizationCacheOptions.SectionName));
if (!string.IsNullOrWhiteSpace(redisConnectionString))
builder.Services.AddRedisSecurity(redisConnectionString, builder.Environment.EnvironmentName);
else if (builder.Environment.IsProduction())
throw new InvalidOperationException(
"Redis is required in Production for authorization cache invalidation retries.");
builder.Services.AddBusinessCaching(
builder.Environment.EnvironmentName,
!string.IsNullOrWhiteSpace(redisConnectionString));
var otlpEndpoint = builder.Configuration["OpenTelemetry:OtlpEndpoint"];
var telemetry = builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing.AddFusionCacheInstrumentation())
.WithMetrics(metrics => metrics
.AddFusionCacheInstrumentation()
.AddMeter(WorkerTelemetry.MeterName, AuthorizationCacheTelemetry.MeterName));
if (Uri.TryCreate(otlpEndpoint, UriKind.Absolute, out var endpoint))
telemetry
.WithTracing(tracing => tracing.AddOtlpExporter(options => options.Endpoint = endpoint))
.WithMetrics(metrics => metrics.AddOtlpExporter(options => options.Endpoint = endpoint));
builder.Services.Configure<ObjectStorageOptions>(
builder.Configuration.GetSection(ObjectStorageOptions.SectionName));
builder.Services.Configure<AliyunOssOptions>(builder.Configuration.GetSection(AliyunOssOptions.SectionName));
builder.Services.Configure<S3CompatibleOptions>(
builder.Configuration.GetSection(S3CompatibleOptions.SectionName));
builder.Services.PostConfigure<ObjectStorageOptions>(options =>
{
options.DefaultProvider = builder.Configuration["STORAGE_DEFAULT_PROVIDER"] ?? options.DefaultProvider;
if (!builder.Environment.IsProduction() &&
options.DefaultProvider == ObjectStorageProviders.AliyunOss &&
!HasConfiguredAliyunOss(builder.Configuration))
options.DefaultProvider = ObjectStorageProviders.LocalDev;
options.DefaultBucket = builder.Configuration["STORAGE_DEFAULT_BUCKET"] ?? options.DefaultBucket;
options.PublicBaseUrl = builder.Configuration["STORAGE_PUBLIC_BASE_URL"] ?? options.PublicBaseUrl;
options.AllowedMimePrefixes = SplitLegacyList(
builder.Configuration["STORAGE_ALLOWED_MIME_PREFIXES"],
options.AllowedMimePrefixes);
options.AllowedMimeTypes = SplitLegacyList(
builder.Configuration["STORAGE_ALLOWED_MIME_TYPES"],
options.AllowedMimeTypes);
options.RequireTenantPrefix = bool.TryParse(
builder.Configuration["STORAGE_REQUIRE_TENANT_PREFIX"],
out var requireTenantPrefix)
? requireTenantPrefix
: options.RequireTenantPrefix;
options.MaxUploadBytes = long.TryParse(
builder.Configuration["STORAGE_MAX_UPLOAD_BYTES"],
out var maxUploadBytes)
? maxUploadBytes
: options.MaxUploadBytes;
});
builder.Services.PostConfigure<AliyunOssOptions>(options =>
{
options.Region = builder.Configuration["ALIYUN_OSS_REGION"] ?? options.Region;
options.Endpoint = builder.Configuration["ALIYUN_OSS_ENDPOINT"] ?? options.Endpoint;
options.AccessKeyId = builder.Configuration["ALIYUN_OSS_ACCESS_KEY_ID"] ?? options.AccessKeyId;
options.AccessKeySecret = builder.Configuration["ALIYUN_OSS_ACCESS_KEY_SECRET"] ?? options.AccessKeySecret;
options.SecurityToken = builder.Configuration["ALIYUN_OSS_STS_TOKEN"] ?? options.SecurityToken;
options.UseInternalEndpoint = bool.TryParse(
builder.Configuration["ALIYUN_OSS_INTERNAL"],
out var useInternalEndpoint)
? useInternalEndpoint
: options.UseInternalEndpoint;
});
builder.Services.PostConfigure<S3CompatibleOptions>(options =>
{
options.Endpoint = builder.Configuration["S3_ENDPOINT"] ?? options.Endpoint;
options.AccessKey = builder.Configuration["S3_ACCESS_KEY"] ?? options.AccessKey;
options.SecretKey = builder.Configuration["S3_SECRET_KEY"] ?? options.SecretKey;
options.Region = builder.Configuration["S3_REGION"] ?? options.Region;
options.Secure = bool.TryParse(builder.Configuration["S3_SECURE"], out var secure)
? secure
: options.Secure;
});
builder.Services.AddOptions<ObjectStorageOptions>()
.Validate(
options => !builder.Environment.IsProduction() ||
options.DefaultProvider == ObjectStorageProviders.AliyunOss,
"Production managed storage must use the configured Aliyun OSS provider.")
.ValidateOnStart();
builder.Services.AddOptions<AliyunOssOptions>()
.Validate<IOptions<ObjectStorageOptions>>(
(aliyun, storage) =>
!builder.Environment.IsProduction() ||
storage.Value.DefaultProvider != ObjectStorageProviders.AliyunOss ||
aliyun.IsConfigured,
"Aliyun OSS credentials and region or endpoint are required when it is the default provider.")
.ValidateOnStart();
builder.Services.AddOptions<S3CompatibleOptions>()
.Validate<IOptions<ObjectStorageOptions>>(
(s3, storage) =>
storage.Value.DefaultProvider != ObjectStorageProviders.LocalDev || s3.IsConfigured,
"S3-compatible endpoint and credentials are required when local_dev is the default provider.")
.ValidateOnStart();
builder.Services.AddOptions<ClamAvOptions>()
.Bind(builder.Configuration.GetSection(ClamAvOptions.SectionName))
.Validate(ClamAvOptions.BeValid, "ClamAV settings are invalid.")
.Validate<IOptions<ObjectStorageOptions>>(
(clamAv, storage) => clamAv.StreamMaxLength >= storage.Value.MaxUploadBytes,
"ClamAV StreamMaxLength must cover the storage max upload size.")
.ValidateOnStart();
builder.Services.AddOptions<WorkerOptions>()
.Bind(builder.Configuration.GetSection(WorkerOptions.SectionName))
.Validate(WorkerOptions.BeValid, "Worker settings are invalid.")
.ValidateOnStart();
builder.Services.AddOptions<DomainLifecycleOptions>()
.Bind(builder.Configuration.GetSection("TenantDomains"));
builder.Services.AddOptions<SaasSubscriptionLifecycleOptions>()
.Bind(builder.Configuration.GetSection("SaasSubscriptions"));
builder.Services.AddOptions<FeatureUsageReconciliationOptions>()
.Bind(builder.Configuration.GetSection("FeatureUsageReconciliation"));
builder.Services.AddOptions<CommercialBillingOptions>()
.Bind(builder.Configuration.GetSection("CommercialBilling"))
.Validate(options => options.BatchSize is >= 1 and <= 1000, "Commercial billing batch size is invalid.")
.ValidateOnStart();
builder.Services.AddSingleton<IPeriodicProcessorLock, PostgresPeriodicProcessorLock>();
builder.Services.AddSingleton<WorkerStateReporter>();
builder.Services.AddHostedService<TenantDomainWorker>();
builder.Services.AddHostedService<SaasSubscriptionWorker>();
builder.Services.AddHostedService<FeatureUsageWorker>();
builder.Services.AddHostedService<BackgroundJobsWorker>();
builder.Services.AddHostedService<AuthorizationCacheInvalidationWorker>();
builder.Services.AddHostedService<CommercialBillingWorker>();
builder.Services.AddHostedService<PlatformApprovalWorker>();
return builder;
}
private static string[] SplitLegacyList(string? value, string[] fallback)
{
return string.IsNullOrWhiteSpace(value)
? fallback
: value.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
}
private static bool HasConfiguredAliyunOss(IConfiguration configuration)
{
return !string.IsNullOrWhiteSpace(configuration["ALIYUN_OSS_ACCESS_KEY_ID"] ??
configuration["Storage:AliyunOss:AccessKeyId"]) &&
!string.IsNullOrWhiteSpace(configuration["ALIYUN_OSS_ACCESS_KEY_SECRET"] ??
configuration["Storage:AliyunOss:AccessKeySecret"]) &&
(!string.IsNullOrWhiteSpace(configuration["ALIYUN_OSS_REGION"] ??
configuration["Storage:AliyunOss:Region"]) ||
!string.IsNullOrWhiteSpace(configuration["ALIYUN_OSS_ENDPOINT"] ??
configuration["Storage:AliyunOss:Endpoint"]));
}
}