using JobTrackerApi.Data; using Microsoft.EntityFrameworkCore; namespace JobTrackerApi.Services; public sealed record BackgroundWorkerRunResult(bool Enabled, int Owners, int Succeeded, int Failed) { public static readonly BackgroundWorkerRunResult Disabled = new(false, 0, 0, 0); } public sealed class BackgroundTenantRunner( IServiceScopeFactory scopes, ILogger logger) { public async Task RunForJobOwnersAsync( string worker, Func work, CancellationToken cancellationToken) { await using var enumerationScope = scopes.CreateAsyncScope(); var ownerIds = await enumerationScope.ServiceProvider.GetRequiredService() .JobApplications.IgnoreQueryFilters().AsNoTracking() .Where(job => job.OwnerUserId != null) .Select(job => job.OwnerUserId!) .ToListAsync(cancellationToken); var owners = ownerIds.Where(owner => !string.IsNullOrWhiteSpace(owner)) .Distinct(StringComparer.Ordinal) .Order(StringComparer.Ordinal) .ToList(); var succeeded = 0; var failed = 0; foreach (var owner in owners) { await using var ownerScope = scopes.CreateAsyncScope(); using var ownerContext = ownerScope.ServiceProvider.GetRequiredService() .UseBackgroundUser(owner); try { await work(ownerScope.ServiceProvider, cancellationToken); succeeded++; } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } catch (Exception ex) { failed++; logger.LogWarning("{Worker} owner pass failed with {FailureCategory}; private owner data was omitted from the log.", worker, ex.GetType().Name); } } logger.LogInformation("{Worker} pass complete: {Owners} owners, {Succeeded} succeeded, {Failed} failed.", worker, owners.Count, succeeded, failed); return new BackgroundWorkerRunResult(true, owners.Count, succeeded, failed); } }