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 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 }; } }