97 lines
3.8 KiB
C#
97 lines
3.8 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Tiku.Application.Jobs;
|
|
using Tiku.Application.Security;
|
|
using Tiku.Domain.Operations;
|
|
|
|
namespace Tiku.Infrastructure.Jobs;
|
|
|
|
internal sealed partial class BackgroundJobService
|
|
{
|
|
public async Task<BackgroundJobItem> EnqueueAsync(
|
|
CreateBackgroundJobCommand command,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var normalizedJobType = NormalizeJobType(command.JobType);
|
|
var idempotencyKey = NormalizeIdempotencyKey(command.IdempotencyKey);
|
|
if (idempotencyKey is not null)
|
|
{
|
|
var existing = await dbContext.BackgroundJobs.AsNoTracking().SingleOrDefaultAsync(
|
|
item => item.TenantId == command.TenantId && item.JobType == normalizedJobType &&
|
|
item.IdempotencyKey == idempotencyKey,
|
|
cancellationToken);
|
|
if (existing is not null) return ToItem(existing);
|
|
}
|
|
|
|
if (!command.IsSystemJob && !(await featureAccessService.EvaluateAsync(
|
|
command.TenantId,
|
|
ResolveRequiredFeature(normalizedJobType, command.Payload),
|
|
FeatureAccessOperation.Write,
|
|
cancellationToken)).Allowed)
|
|
throw new InvalidOperationException("Tenant feature entitlement does not allow this background job.");
|
|
var quotaMetric = command.IsSystemJob ? null : ResolveQuotaMetric(normalizedJobType);
|
|
var quotaConsumed = false;
|
|
if (quotaMetric is not null)
|
|
{
|
|
var quota = (await featureAccessService.GetQuotaSummaryAsync(command.TenantId, cancellationToken))
|
|
.SingleOrDefault(value => value.MetricCode == quotaMetric);
|
|
if (quota is not null)
|
|
{
|
|
quotaConsumed = await featureAccessService.TryConsumeQuotaAsync(
|
|
command.TenantId, quotaMetric, 1, cancellationToken);
|
|
if (!quotaConsumed)
|
|
throw new FeatureAccessException("The background job quota has been exhausted.",
|
|
"feature_quota_exhausted");
|
|
}
|
|
}
|
|
|
|
var job = new BackgroundJob
|
|
{
|
|
TenantId = command.TenantId,
|
|
JobType = normalizedJobType,
|
|
IdempotencyKey = idempotencyKey,
|
|
Payload = command.Payload,
|
|
RunAfter = command.RunAfter,
|
|
MaxRetries = Math.Clamp(command.MaxRetries, 0, 20)
|
|
};
|
|
dbContext.BackgroundJobs.Add(job);
|
|
try
|
|
{
|
|
await dbContext.SaveChangesAsync(cancellationToken);
|
|
}
|
|
catch (DbUpdateException) when (idempotencyKey is not null)
|
|
{
|
|
dbContext.ChangeTracker.Clear();
|
|
var existing = await dbContext.BackgroundJobs.AsNoTracking().SingleOrDefaultAsync(
|
|
item => item.TenantId == command.TenantId && item.JobType == normalizedJobType &&
|
|
item.IdempotencyKey == idempotencyKey,
|
|
cancellationToken);
|
|
if (existing is not null)
|
|
{
|
|
if (quotaConsumed && quotaMetric is not null)
|
|
await featureAccessService.ReleaseQuotaAsync(command.TenantId, quotaMetric, 1,
|
|
CancellationToken.None);
|
|
return ToItem(existing);
|
|
}
|
|
|
|
throw;
|
|
}
|
|
catch
|
|
{
|
|
if (quotaConsumed && quotaMetric is not null)
|
|
await featureAccessService.ReleaseQuotaAsync(command.TenantId, quotaMetric, 1, CancellationToken.None);
|
|
throw;
|
|
}
|
|
|
|
return ToItem(job);
|
|
}
|
|
|
|
private static string? ResolveQuotaMetric(string jobType)
|
|
{
|
|
return jobType switch
|
|
{
|
|
"content_import" => SaasQuotaMetricCatalog.ImportCount,
|
|
"content_export" => SaasQuotaMetricCatalog.ExportCount,
|
|
_ => null
|
|
};
|
|
}
|
|
} |