using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using System.Text.Json; using System.Formats.Tar; using System.IO.Compression; using Tiku.Application.Assets; using Tiku.Application.Content; using Tiku.Application.Jobs; using Tiku.Application.Security; using Tiku.Application.Tenancy; using Tiku.Domain.Commerce; using Tiku.Domain.Common; using Tiku.Domain.Content; using Tiku.Domain.Operations; using Tiku.Infrastructure.Persistence; using Tiku.Infrastructure.Observability; using System.Diagnostics; 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) => jobType switch { "content_import" => SaasQuotaMetricCatalog.ImportCount, "content_export" => SaasQuotaMetricCatalog.ExportCount, _ => null }; }