test(workers): prove clock and restart safety
This commit is contained in:
@@ -10,7 +10,8 @@ public sealed class DailyExportHostedService(
|
||||
ILogger<DailyExportHostedService> logger,
|
||||
IConfiguration configuration,
|
||||
AppPaths paths,
|
||||
IStartupReadiness startupReadiness) : BackgroundService
|
||||
IStartupReadiness startupReadiness,
|
||||
TimeProvider timeProvider) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
@@ -25,11 +26,11 @@ public sealed class DailyExportHostedService(
|
||||
if (hour is < 0 or > 23) hour = 2;
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
var now = timeProvider.GetLocalNow().DateTime;
|
||||
var next = new DateTime(now.Year, now.Month, now.Day, hour, 0, 0);
|
||||
if (next <= now) next = next.AddDays(1);
|
||||
logger.LogInformation("Next daily export scheduled at {Next}.", next);
|
||||
await Task.Delay(next - now, stoppingToken);
|
||||
await Task.Delay(next - now, timeProvider, stoppingToken);
|
||||
await RunOnceAsync(stoppingToken);
|
||||
}
|
||||
}
|
||||
@@ -48,12 +49,13 @@ public sealed class DailyExportHostedService(
|
||||
{
|
||||
var db = services.GetRequiredService<JobTrackerContext>();
|
||||
var owner = db.CurrentUserId ?? throw new InvalidOperationException("Daily export requires an explicit owner scope.");
|
||||
var now = timeProvider.GetLocalNow().DateTime;
|
||||
var jobs = await db.JobApplications.AsNoTracking().OrderByDescending(job => job.DateApplied).ToListAsync(cancellationToken);
|
||||
var jobIds = jobs.Select(job => job.Id).ToList();
|
||||
var export = new
|
||||
{
|
||||
Version = "dailyexport.v2",
|
||||
CreatedAt = DateTime.Now,
|
||||
CreatedAt = now,
|
||||
OwnerUserId = owner,
|
||||
Companies = await db.Companies.AsNoTracking().OrderBy(company => company.Name).ToListAsync(cancellationToken),
|
||||
JobApplications = jobs,
|
||||
@@ -96,7 +98,7 @@ public sealed class DailyExportHostedService(
|
||||
|
||||
var folder = paths.GetOwnerDailyExportsRoot(configuration["Exports:DailyFolder"], owner);
|
||||
Directory.CreateDirectory(folder);
|
||||
var finalPath = Path.Combine(folder, $"daily_export_{DateTime.Now:yyyyMMdd}.json");
|
||||
var finalPath = Path.Combine(folder, $"daily_export_{now:yyyyMMdd}.json");
|
||||
var temporaryPath = finalPath + $".{Guid.NewGuid():N}.tmp";
|
||||
try
|
||||
{
|
||||
|
||||
@@ -10,7 +10,8 @@ public sealed class FollowUpReminderHostedService(
|
||||
IConfiguration configuration,
|
||||
ILogger<FollowUpReminderHostedService> logger,
|
||||
IStartupReadiness startupReadiness,
|
||||
ExternalOrigin externalOrigin) : BackgroundService
|
||||
ExternalOrigin externalOrigin,
|
||||
TimeProvider timeProvider) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
@@ -21,11 +22,11 @@ public sealed class FollowUpReminderHostedService(
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), timeProvider, stoppingToken);
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
await RunOnceAsync(stoppingToken);
|
||||
await Task.Delay(TimeSpan.FromHours(6), stoppingToken);
|
||||
await Task.Delay(TimeSpan.FromHours(6), timeProvider, stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +46,7 @@ public sealed class FollowUpReminderHostedService(
|
||||
var users = services.GetRequiredService<UserManager<ApplicationUser>>();
|
||||
var email = services.GetRequiredService<IAppEmailSender>();
|
||||
var settings = await RulesEngine.GetSettings(db, cancellationToken);
|
||||
var now = DateTime.Now;
|
||||
var now = timeProvider.GetLocalNow().DateTime;
|
||||
var lookAheadDays = Math.Clamp(configuration.GetValue("Email:FollowUpReminders:UpcomingDays", 2), 1, 14);
|
||||
var upcomingTo = now.AddDays(lookAheadDays);
|
||||
var lastMessages = await db.Correspondences.AsNoTracking()
|
||||
|
||||
@@ -11,7 +11,8 @@ public sealed class JobEnrichmentHostedService(
|
||||
BackgroundTenantRunner tenants,
|
||||
IConfiguration configuration,
|
||||
ILogger<JobEnrichmentHostedService> logger,
|
||||
IStartupReadiness startupReadiness) : BackgroundService
|
||||
IStartupReadiness startupReadiness,
|
||||
TimeProvider timeProvider) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
@@ -22,11 +23,11 @@ public sealed class JobEnrichmentHostedService(
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), timeProvider, stoppingToken);
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
await RunOnceAsync(stoppingToken);
|
||||
await Task.Delay(TimeSpan.FromMinutes(10), stoppingToken);
|
||||
await Task.Delay(TimeSpan.FromMinutes(10), timeProvider, stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ public sealed class RulesHostedService(
|
||||
BackgroundTenantRunner tenants,
|
||||
IConfiguration configuration,
|
||||
ILogger<RulesHostedService> logger,
|
||||
IStartupReadiness startupReadiness) : BackgroundService
|
||||
IStartupReadiness startupReadiness,
|
||||
TimeProvider timeProvider) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
@@ -19,11 +20,11 @@ public sealed class RulesHostedService(
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken);
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), timeProvider, stoppingToken);
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
await RunOnceAsync(stoppingToken);
|
||||
await Task.Delay(TimeSpan.FromMinutes(30), stoppingToken);
|
||||
await Task.Delay(TimeSpan.FromMinutes(30), timeProvider, stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,11 +36,11 @@ public sealed class RulesHostedService(
|
||||
return tenants.RunForJobOwnersAsync("rules", ProcessOwnerAsync, cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task ProcessOwnerAsync(IServiceProvider services, CancellationToken cancellationToken)
|
||||
private async Task ProcessOwnerAsync(IServiceProvider services, CancellationToken cancellationToken)
|
||||
{
|
||||
var db = services.GetRequiredService<JobTrackerContext>();
|
||||
var settings = await RulesEngine.GetSettings(db, cancellationToken);
|
||||
var now = DateTime.Now;
|
||||
var now = timeProvider.GetLocalNow().DateTime;
|
||||
var lastMessages = await db.Correspondences
|
||||
.GroupBy(message => message.JobApplicationId)
|
||||
.Select(group => new { JobApplicationId = group.Key, Last = group.Max(x => x.Date) })
|
||||
|
||||
Reference in New Issue
Block a user