using Microsoft.Extensions.Options; using Tiku.Application.Jobs; using Tiku.Application.PlatformBilling; using Tiku.Application.Security; using Tiku.Application.Tenancy; using Tiku.Infrastructure.Messaging; namespace Tiku.Worker; internal abstract class PeriodicWorkerService( ILogger logger, TimeSpan interval) : BackgroundService { protected abstract Task ProcessAsync(CancellationToken cancellationToken); protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { var processed = await ProcessAsync(stoppingToken); if (processed > 0) { logger.LogInformation("{Worker} processed {Count} items.", GetType().Name, processed); } } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { break; } catch (Exception exception) { logger.LogError(exception, "{Worker} iteration failed.", GetType().Name); } await Task.Delay(interval, stoppingToken); } } protected static void InitializeSystem(IServiceProvider services, string reason) => services.GetRequiredService().InitializeSystem(null, reason); } internal sealed class TenantDomainWorker( IServiceScopeFactory scopeFactory, IOptions options, ILogger logger) : PeriodicWorkerService(logger, TimeSpan.FromSeconds(Math.Clamp(options.Value.PollSeconds, 10, 3600))) { protected override async Task ProcessAsync(CancellationToken cancellationToken) { await using var scope = scopeFactory.CreateAsyncScope(); InitializeSystem(scope.ServiceProvider, "Tenant domain DNS and TLS lifecycle worker"); return await scope.ServiceProvider.GetRequiredService() .ProcessPendingAsync(cancellationToken); } } internal sealed class SaasSubscriptionWorker( IServiceScopeFactory scopeFactory, ILogger logger) : PeriodicWorkerService(logger, TimeSpan.FromSeconds(60)) { protected override async Task ProcessAsync(CancellationToken cancellationToken) { await using var scope = scopeFactory.CreateAsyncScope(); InitializeSystem(scope.ServiceProvider, "SaaS subscription lifecycle discovery worker"); return await scope.ServiceProvider.GetRequiredService() .ProcessDueAsync(cancellationToken: cancellationToken); } } internal sealed class FeatureUsageWorker( IServiceScopeFactory scopeFactory, IOptions options, ILogger logger) : PeriodicWorkerService(logger, TimeSpan.FromMinutes(Math.Clamp(options.Value.IntervalMinutes, 1, 1440))) { protected override async Task ProcessAsync(CancellationToken cancellationToken) { await using var scope = scopeFactory.CreateAsyncScope(); InitializeSystem(scope.ServiceProvider, "Tenant feature usage reconciliation worker"); return await scope.ServiceProvider.GetRequiredService() .ProcessDueAsync(cancellationToken); } } internal sealed class BackgroundJobsWorker( IServiceScopeFactory scopeFactory, MessagingOptions messagingOptions, ILogger logger) : PeriodicWorkerService(logger, TimeSpan.FromSeconds(2)) { private readonly string workerId = $"{Environment.MachineName}:{Guid.NewGuid():N}"; protected override async Task ProcessAsync(CancellationToken cancellationToken) { var workers = Enumerable.Range(0, 4).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 worker"); return await scope.ServiceProvider.GetRequiredService() .ProcessPendingAsync( $"{workerId}:{index}", 5, includeImmediateJobs: !messagingOptions.IsConfigured, cancellationToken: cancellationToken); } }