Simplify infrastructure services and move all recurring jobs to Hangfire
This commit is contained in:
@@ -23,14 +23,7 @@ public class DailyMixGenerationJob(
|
||||
logger.LogInformation("Starting daily mix generation for {Count} users", userIds.Count);
|
||||
|
||||
foreach (var userId in userIds)
|
||||
{
|
||||
var result = await mediator.Send(new GenerateMixesForUserCommand(userId), cancellationToken);
|
||||
if (result.IsError)
|
||||
{
|
||||
logger.LogWarning("Failed to generate mixes for user {UserId}: {Error}",
|
||||
userId, result.FirstError.Description);
|
||||
}
|
||||
}
|
||||
await mediator.Send(new GenerateMixesForUserCommand(userId), cancellationToken);
|
||||
|
||||
logger.LogInformation("Finished daily mix generation");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
using Hangfire;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Musify.Application.Configuration;
|
||||
|
||||
namespace Musify.Infrastructure.Jobs;
|
||||
|
||||
/// <summary>Registers every recurring Hangfire job when the host starts.</summary>
|
||||
public sealed class RecurringJobsRegistrar(
|
||||
IRecurringJobManager recurringJobManager,
|
||||
UploadIntentConfiguration uploadIntentConfiguration)
|
||||
: IHostedService
|
||||
{
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var options = new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc };
|
||||
|
||||
recurringJobManager.AddOrUpdate<DailyMixGenerationJob>(
|
||||
"daily-mix-generation", job => job.RunAsync(CancellationToken.None), Cron.Daily(), options);
|
||||
recurringJobManager.AddOrUpdate<ListeningHistoryCleanupJob>(
|
||||
"listening-history-cleanup", job => job.RunAsync(CancellationToken.None), Cron.Daily(3), options);
|
||||
recurringJobManager.AddOrUpdate<UploadIntentExpirationJob>(
|
||||
"upload-intent-expiration", job => job.RunAsync(CancellationToken.None),
|
||||
EveryCron(uploadIntentConfiguration.ExpirationJobIntervalSeconds), options);
|
||||
recurringJobManager.AddOrUpdate<TempUploadsCleanupJob>(
|
||||
"temp-uploads-cleanup", job => job.RunAsync(CancellationToken.None),
|
||||
EveryCron(uploadIntentConfiguration.TempCleanupJobIntervalSeconds), options);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <summary>Hangfire cron has a one-minute resolution, so the interval is rounded up to whole minutes.</summary>
|
||||
internal static string EveryCron(int intervalSeconds)
|
||||
{
|
||||
var minutes = Math.Max(1, (int)Math.Ceiling(intervalSeconds / 60.0));
|
||||
return minutes < 60
|
||||
? $"*/{minutes} * * * *"
|
||||
: $"0 */{Math.Max(1, minutes / 60)} * * *";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Mediator;
|
||||
using Musify.Application.UploadIntents;
|
||||
|
||||
namespace Musify.Infrastructure.Jobs;
|
||||
|
||||
public class TempUploadsCleanupJob(IMediator mediator)
|
||||
{
|
||||
public async Task RunAsync(CancellationToken cancellationToken) =>
|
||||
await mediator.Send(new DeleteStaleTempUploadsCommand(), cancellationToken);
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Musify.Application.Configuration;
|
||||
using Musify.Application.Contracts;
|
||||
|
||||
namespace Musify.Infrastructure.Jobs;
|
||||
|
||||
public class TemporalUploadsCleanUpJob(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<TemporalUploadsCleanUpJob> logger,
|
||||
ApplicationStorageConfiguration storageConfiguration,
|
||||
UploadIntentConfiguration uploadIntentConfiguration)
|
||||
: BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
logger.LogInformation("TemporalUploadsCleanUpJob started (interval: {Interval}s, prefix: {Prefix})",
|
||||
uploadIntentConfiguration.TempCleanupJobIntervalSeconds,
|
||||
uploadIntentConfiguration.TempRootPrefix);
|
||||
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(uploadIntentConfiguration.TempCleanupJobIntervalSeconds));
|
||||
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
await RunAsync(stoppingToken);
|
||||
}
|
||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||
{
|
||||
logger.LogError(exception, "Error in TemporalUploadsCleanUpJob");
|
||||
}
|
||||
}
|
||||
while (await timer.WaitForNextTickAsync(stoppingToken));
|
||||
}
|
||||
|
||||
private async Task RunAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await using var scope = scopeFactory.CreateAsyncScope();
|
||||
var storageService = scope.ServiceProvider.GetRequiredService<IStorageService>();
|
||||
|
||||
var cutOff = DateTime.UtcNow.AddDays(-uploadIntentConfiguration.TempUploadsRetentionDays);
|
||||
var deleted = 0;
|
||||
var errors = 0;
|
||||
|
||||
await foreach (var (key, lastModified) in storageService.ListObjectsAsync(
|
||||
storageConfiguration.Bucket,
|
||||
uploadIntentConfiguration.TempRootPrefix,
|
||||
cancellationToken))
|
||||
{
|
||||
if (lastModified >= cutOff)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
await storageService.RemoveFileAsync(storageConfiguration.Bucket, key, cancellationToken);
|
||||
deleted++;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
errors++;
|
||||
logger.LogWarning(exception, "Failed to delete orphaned temp object {Key}", key);
|
||||
}
|
||||
}
|
||||
|
||||
if (deleted > 0 || errors > 0)
|
||||
logger.LogInformation("TemporalUploadsCleanUpJob: deleted {Deleted} orphaned objects, {Errors} errors", deleted, errors);
|
||||
}
|
||||
}
|
||||
@@ -1,70 +1,10 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Musify.Application.Configuration;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Domain.ValueObjects;
|
||||
using Mediator;
|
||||
using Musify.Application.UploadIntents;
|
||||
|
||||
namespace Musify.Infrastructure.Jobs;
|
||||
|
||||
public class UploadIntentExpirationJob(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<UploadIntentExpirationJob> logger,
|
||||
UploadIntentConfiguration uploadIntentConfiguration)
|
||||
: BackgroundService
|
||||
public class UploadIntentExpirationJob(IMediator mediator)
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
logger.LogInformation("UploadIntentExpirationJob started (interval: {Interval}s)", uploadIntentConfiguration.ExpirationJobIntervalSeconds);
|
||||
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(uploadIntentConfiguration.ExpirationJobIntervalSeconds));
|
||||
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
await RunAsync(stoppingToken);
|
||||
}
|
||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||
{
|
||||
logger.LogError(exception, "Error in UploadIntentExpirationJob");
|
||||
}
|
||||
}
|
||||
while (await timer.WaitForNextTickAsync(stoppingToken));
|
||||
}
|
||||
|
||||
private async Task RunAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await using var scope = scopeFactory.CreateAsyncScope();
|
||||
var database = scope.ServiceProvider.GetRequiredService<IDatabase>();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
var expiredIntents = await database.UploadIntents
|
||||
.Where(i => i.Status == UploadIntentStatus.Issued && i.ExpiresAt < now)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (expiredIntents.Count > 0)
|
||||
{
|
||||
foreach (var intent in expiredIntents)
|
||||
intent.Status = UploadIntentStatus.Expired;
|
||||
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("Marked {Count} upload intents as Expired", expiredIntents.Count);
|
||||
}
|
||||
|
||||
var retentionCutoff = now.AddDays(-uploadIntentConfiguration.ExpiredIntentsRetentionDays);
|
||||
|
||||
var oldExpiredIntents = await database.UploadIntents
|
||||
.Where(i => i.Status == UploadIntentStatus.Expired && i.ExpiresAt < retentionCutoff)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (oldExpiredIntents.Count > 0)
|
||||
{
|
||||
database.UploadIntents.RemoveRange(oldExpiredIntents);
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("Deleted {Count} old Expired upload intents past retention window", oldExpiredIntents.Count);
|
||||
}
|
||||
}
|
||||
public async Task RunAsync(CancellationToken cancellationToken) =>
|
||||
await mediator.Send(new ExpireUploadIntentsCommand(), cancellationToken);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user