test(workers): prove clock and restart safety
CI and Deploy / test (pull_request) Successful in 5m13s
CI and Deploy / deploy (pull_request) Has been skipped

This commit is contained in:
cesnimda
2026-08-15 20:18:02 +02:00
parent a08ba9b45d
commit dbf28b97ce
7 changed files with 124 additions and 36 deletions
@@ -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);
}
}
+6 -5
View File
@@ -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) })