using Microsoft.Extensions.Options; using Tiku.Application.Jobs; using Tiku.Application.PlatformBilling; using Tiku.Application.Security; using Tiku.Application.Tenancy; namespace Tiku.Api.BackgroundProcessing; public sealed class BackgroundProcessingOptions { public const string SectionName = "BackgroundProcessing"; public bool Enabled { get; set; } = true; public int JobPollSeconds { get; set; } = 2; public int JobParallelism { get; set; } = 4; public int JobBatchSize { get; set; } = 5; public static bool BeValid(BackgroundProcessingOptions options) => options.JobPollSeconds is >= 1 and <= 3600 && options.JobParallelism is >= 1 and <= 32 && options.JobBatchSize is >= 1 and <= 100; } internal abstract class PeriodicBackgroundService( ILogger logger, TimeSpan interval, bool enabled) : BackgroundService { protected abstract Task ProcessAsync(CancellationToken cancellationToken); protected override async Task ExecuteAsync(CancellationToken stoppingToken) { if (!enabled) { return; } while (!stoppingToken.IsCancellationRequested) { try { var processed = await ProcessAsync(stoppingToken); if (processed > 0) { logger.LogInformation("{Worker} processed {Count} items.", GetType().Name, processed); } await Task.Delay(interval, stoppingToken); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { break; } catch (Exception exception) { logger.LogError(exception, "{Worker} iteration failed.", GetType().Name); try { await Task.Delay(interval, stoppingToken); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { break; } } } } protected static void InitializeSystem(IServiceProvider services, string reason) => services.GetRequiredService().InitializeSystem(null, reason); } internal sealed class TenantDomainBackgroundService( IServiceScopeFactory scopeFactory, IOptions domainOptions, IOptions backgroundOptions, ILogger logger) : PeriodicBackgroundService( logger, TimeSpan.FromSeconds(Math.Clamp(domainOptions.Value.PollSeconds, 10, 3600)), backgroundOptions.Value.Enabled) { protected override async Task ProcessAsync(CancellationToken cancellationToken) { await using var scope = scopeFactory.CreateAsyncScope(); InitializeSystem(scope.ServiceProvider, "Tenant domain DNS and TLS lifecycle background service"); return await scope.ServiceProvider.GetRequiredService() .ProcessPendingAsync(cancellationToken); } } internal sealed class SaasSubscriptionBackgroundService( IServiceScopeFactory scopeFactory, IOptions options, ILogger logger) : PeriodicBackgroundService(logger, TimeSpan.FromSeconds(60), options.Value.Enabled) { protected override async Task ProcessAsync(CancellationToken cancellationToken) { await using var scope = scopeFactory.CreateAsyncScope(); InitializeSystem(scope.ServiceProvider, "SaaS subscription lifecycle background service"); return await scope.ServiceProvider.GetRequiredService() .ProcessDueAsync(cancellationToken: cancellationToken); } } internal sealed class FeatureUsageBackgroundService( IServiceScopeFactory scopeFactory, IOptions featureOptions, IOptions backgroundOptions, ILogger logger) : PeriodicBackgroundService( logger, TimeSpan.FromMinutes(Math.Clamp(featureOptions.Value.IntervalMinutes, 1, 1440)), backgroundOptions.Value.Enabled) { protected override async Task ProcessAsync(CancellationToken cancellationToken) { await using var scope = scopeFactory.CreateAsyncScope(); InitializeSystem(scope.ServiceProvider, "Tenant feature usage reconciliation background service"); return await scope.ServiceProvider.GetRequiredService() .ProcessDueAsync(cancellationToken); } } internal sealed class BackgroundJobsBackgroundService( IServiceScopeFactory scopeFactory, IOptions options, ILogger logger) : PeriodicBackgroundService(logger, TimeSpan.FromSeconds(options.Value.JobPollSeconds), options.Value.Enabled) { private readonly string workerId = $"{Environment.MachineName}:{Guid.NewGuid():N}"; private readonly int parallelism = options.Value.JobParallelism; private readonly int batchSize = options.Value.JobBatchSize; protected override async Task ProcessAsync(CancellationToken cancellationToken) { var workers = Enumerable.Range(0, parallelism) .Select(index => ProcessPartitionAsync(index, cancellationToken)); return (await Task.WhenAll(workers)).Sum(); } private async Task ProcessPartitionAsync(int index, CancellationToken cancellationToken) { await using var scope = scopeFactory.CreateAsyncScope(); InitializeSystem(scope.ServiceProvider, "Background job lease service"); return await scope.ServiceProvider.GetRequiredService() .ProcessPendingAsync( $"{workerId}:{index}", batchSize, includeImmediateJobs: true, cancellationToken: cancellationToken); } }