forked from xiongyuxing/tiku-backend.net
refactor: consolidate backend into modular monolith
This commit is contained in:
154
Tiku.Api/BackgroundProcessing/BackgroundProcessingServices.cs
Normal file
154
Tiku.Api/BackgroundProcessing/BackgroundProcessingServices.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
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<int> 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<ITenantContextInitializer>().InitializeSystem(null, reason);
|
||||
}
|
||||
|
||||
internal sealed class TenantDomainBackgroundService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOptions<DomainLifecycleOptions> domainOptions,
|
||||
IOptions<BackgroundProcessingOptions> backgroundOptions,
|
||||
ILogger<TenantDomainBackgroundService> logger)
|
||||
: PeriodicBackgroundService(
|
||||
logger,
|
||||
TimeSpan.FromSeconds(Math.Clamp(domainOptions.Value.PollSeconds, 10, 3600)),
|
||||
backgroundOptions.Value.Enabled)
|
||||
{
|
||||
protected override async Task<int> 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<ITenantDomainLifecycleService>()
|
||||
.ProcessPendingAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class SaasSubscriptionBackgroundService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOptions<BackgroundProcessingOptions> options,
|
||||
ILogger<SaasSubscriptionBackgroundService> logger)
|
||||
: PeriodicBackgroundService(logger, TimeSpan.FromSeconds(60), options.Value.Enabled)
|
||||
{
|
||||
protected override async Task<int> ProcessAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await using var scope = scopeFactory.CreateAsyncScope();
|
||||
InitializeSystem(scope.ServiceProvider, "SaaS subscription lifecycle background service");
|
||||
return await scope.ServiceProvider.GetRequiredService<ISaasSubscriptionLifecycleService>()
|
||||
.ProcessDueAsync(cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FeatureUsageBackgroundService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOptions<FeatureUsageReconciliationOptions> featureOptions,
|
||||
IOptions<BackgroundProcessingOptions> backgroundOptions,
|
||||
ILogger<FeatureUsageBackgroundService> logger)
|
||||
: PeriodicBackgroundService(
|
||||
logger,
|
||||
TimeSpan.FromMinutes(Math.Clamp(featureOptions.Value.IntervalMinutes, 1, 1440)),
|
||||
backgroundOptions.Value.Enabled)
|
||||
{
|
||||
protected override async Task<int> ProcessAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await using var scope = scopeFactory.CreateAsyncScope();
|
||||
InitializeSystem(scope.ServiceProvider, "Tenant feature usage reconciliation background service");
|
||||
return await scope.ServiceProvider.GetRequiredService<IFeatureUsageReconciliationService>()
|
||||
.ProcessDueAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class BackgroundJobsBackgroundService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOptions<BackgroundProcessingOptions> options,
|
||||
ILogger<BackgroundJobsBackgroundService> 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<int> ProcessAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var workers = Enumerable.Range(0, parallelism)
|
||||
.Select(index => ProcessPartitionAsync(index, cancellationToken));
|
||||
return (await Task.WhenAll(workers)).Sum();
|
||||
}
|
||||
|
||||
private async Task<int> ProcessPartitionAsync(int index, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var scope = scopeFactory.CreateAsyncScope();
|
||||
InitializeSystem(scope.ServiceProvider, "Background job lease service");
|
||||
return await scope.ServiceProvider.GetRequiredService<IBackgroundJobService>()
|
||||
.ProcessPendingAsync(
|
||||
$"{workerId}:{index}",
|
||||
batchSize,
|
||||
includeImmediateJobs: true,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user