From d7a24d2704b732b4301e3d487d80dab075d6e9c5 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Fri, 20 Mar 2026 01:58:45 +0100 Subject: [PATCH] JobExecution --- .../Contracts/Infrastructure/IDatabase.cs | 2 + Application/Events/RemoveFileEvent.cs | 3 +- Application/Events/ResizePictureEvent.cs | 3 +- .../CreatePlayListCommandHandler.cs | 3 +- .../DeletePlayListCommandHandler.cs | 12 ++++-- .../UpdatePlayListCommandHandler.cs | 15 +++++--- Domain/Entities/Job.cs | 17 --------- Domain/Entities/JobExecution.cs | 37 +++++++++++++++++++ .../Messaging/Consumers/RemoveFileConsumer.cs | 23 ++++++------ .../Consumers/ResizePictureConsumer.cs | 37 ++++++++++--------- Infrastructure/Persistence/Database.cs | 2 + 11 files changed, 96 insertions(+), 58 deletions(-) create mode 100644 Domain/Entities/JobExecution.cs diff --git a/Application/Contracts/Infrastructure/IDatabase.cs b/Application/Contracts/Infrastructure/IDatabase.cs index 69ea4eb..16534e2 100644 --- a/Application/Contracts/Infrastructure/IDatabase.cs +++ b/Application/Contracts/Infrastructure/IDatabase.cs @@ -17,6 +17,8 @@ namespace Musify.Application.Contracts.Infrastructure DbSet Jobs { get; } + DbSet JobExecutions { get; } + Task SaveChangesAsync(CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/Application/Events/RemoveFileEvent.cs b/Application/Events/RemoveFileEvent.cs index 76fc108..ff89abb 100644 --- a/Application/Events/RemoveFileEvent.cs +++ b/Application/Events/RemoveFileEvent.cs @@ -2,5 +2,6 @@ { public record RemoveFileEvent( string BucketName, - string KeyName); + string KeyName, + Guid JobId); } \ No newline at end of file diff --git a/Application/Events/ResizePictureEvent.cs b/Application/Events/ResizePictureEvent.cs index 00ccfce..df4f3d9 100644 --- a/Application/Events/ResizePictureEvent.cs +++ b/Application/Events/ResizePictureEvent.cs @@ -3,5 +3,6 @@ public record ResizePictureEvent( string BucketName, string KeyName, - ResizePictureItems[] Items); + ResizePictureItems[] Items, + Guid JobId); } \ No newline at end of file diff --git a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs index db4f02e..f57b028 100644 --- a/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/CreatePlayList/CreatePlayListCommandHandler.cs @@ -77,7 +77,8 @@ namespace Musify.Application.PlayLists.Commands.CreatePlayList playListConfiguration.PicturesSizes.LargePictureWidth, playListConfiguration.PicturesSizes.LargePictureHeight, playListConfiguration.Routes.LargePictures) - ] ), cancellationToken); + ], + Guid.NewGuid()), cancellationToken); } catch (Exception exception) { diff --git a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs index a92e092..d980748 100644 --- a/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/DeletePlayList/DeletePlayListCommandHandler.cs @@ -37,7 +37,8 @@ namespace Musify.Application.PlayLists.Commands.DeletePlayList { await publishEndpoint.Publish(new RemoveFileEvent( storageConfiguration.BucketName, - originalImageStorageKey), cancellationToken); + originalImageStorageKey, + Guid.NewGuid()), cancellationToken); } catch (Exception exception) { @@ -51,7 +52,8 @@ namespace Musify.Application.PlayLists.Commands.DeletePlayList { await publishEndpoint.Publish(new RemoveFileEvent( storageConfiguration.BucketName, - playList.SmallPictureKeyName), cancellationToken); + playList.SmallPictureKeyName, + Guid.NewGuid()), cancellationToken); } catch (Exception exception) { @@ -66,7 +68,8 @@ namespace Musify.Application.PlayLists.Commands.DeletePlayList { await publishEndpoint.Publish(new RemoveFileEvent( storageConfiguration.BucketName, - playList.MediumPictureKeyName), cancellationToken); + playList.MediumPictureKeyName, + Guid.NewGuid()), cancellationToken); } catch (Exception exception) { @@ -81,7 +84,8 @@ namespace Musify.Application.PlayLists.Commands.DeletePlayList { await publishEndpoint.Publish(new RemoveFileEvent( storageConfiguration.BucketName, - playList.LargePictureKeyName), cancellationToken); + playList.LargePictureKeyName, + Guid.NewGuid()), cancellationToken); } catch (Exception exception) { diff --git a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs index 0092d1f..52e4550 100644 --- a/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs +++ b/Application/PlayLists/Commands/UpdatePlayList/UpdatePlayListCommandHandler.cs @@ -55,7 +55,8 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList { await publishEndpoint.Publish(new RemoveFileEvent( storageConfiguration.BucketName, - originalImageStorageKey), cancellationToken); + originalImageStorageKey, + Guid.NewGuid()), cancellationToken); } catch (Exception exception) { @@ -72,7 +73,8 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList { await publishEndpoint.Publish(new RemoveFileEvent( storageConfiguration.BucketName, - playList.SmallPictureKeyName), cancellationToken); + playList.SmallPictureKeyName, + Guid.NewGuid()), cancellationToken); } catch (Exception exception) { @@ -87,7 +89,8 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList { await publishEndpoint.Publish(new RemoveFileEvent( storageConfiguration.BucketName, - playList.MediumPictureKeyName), cancellationToken); + playList.MediumPictureKeyName, + Guid.NewGuid()), cancellationToken); } catch (Exception exception) { @@ -102,7 +105,8 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList { await publishEndpoint.Publish(new RemoveFileEvent( storageConfiguration.BucketName, - playList.LargePictureKeyName), cancellationToken); + playList.LargePictureKeyName, + Guid.NewGuid()), cancellationToken); } catch (Exception exception) { @@ -150,7 +154,8 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList playListConfiguration.PicturesSizes.LargePictureWidth, playListConfiguration.PicturesSizes.LargePictureHeight, playListConfiguration.Routes.LargePictures) - ] + ], + Guid.NewGuid() ), cancellationToken); } catch (Exception exception) diff --git a/Domain/Entities/Job.cs b/Domain/Entities/Job.cs index 4560443..a766585 100644 --- a/Domain/Entities/Job.cs +++ b/Domain/Entities/Job.cs @@ -12,27 +12,10 @@ namespace Musify.Domain.Entities [Required] public required JobType JobType { get; set; } - [Required] - public JobState JobState { get; set; } = JobState.Pending; - - [NotMapped] - public bool IsCompleted => JobState == JobState.Completed; - - [NotMapped] - public bool IsFailed => JobState == JobState.Failed; - - [NotMapped] - public bool IsPending => JobState == JobState.Pending; - [Required] public required string Payload { get; set; } [Required] public DateTime CreateAt { get; set; } = DateTime.UtcNow; - - [Required] - public int RetryCount { get; set; } = 0; - - public DateTime CompletedAt { get; set; } } } \ No newline at end of file diff --git a/Domain/Entities/JobExecution.cs b/Domain/Entities/JobExecution.cs new file mode 100644 index 0000000..ad20805 --- /dev/null +++ b/Domain/Entities/JobExecution.cs @@ -0,0 +1,37 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Musify.Domain.Entities +{ +#pragma warning disable CS8618 + [Table("JobExecution")] + public class JobExecution + { + [Key] + public Guid Id { get; set; } = Guid.NewGuid(); + + [Required] + public required Guid JobId { get; set; } + + [Required] + public JobState JobState { get; set; } = JobState.Pending; + + [NotMapped] + public bool IsCompleted => JobState == JobState.Completed; + + [NotMapped] + public bool IsFailed => JobState == JobState.Failed; + + [NotMapped] + public bool IsPending => JobState == JobState.Pending; + + [Required] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + [Required] + public DateTime FinishedAt { get; set; } + + [ForeignKey(nameof(JobId)) ] + public Job Job { get; set; } + } +} \ No newline at end of file diff --git a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs index 3c86b06..8d42353 100644 --- a/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs +++ b/Infrastructure/Messaging/Consumers/RemoveFileConsumer.cs @@ -18,7 +18,7 @@ namespace Musify.Infrastructure.Messaging.Consumers var payload = JsonSerializer.Serialize(consumeContext.Message); var job = await database.Jobs - .SingleOrDefaultAsync(j => j.JobState == JobState.Failed && j.Payload == payload); + .SingleOrDefaultAsync(j => j.Id == consumeContext.Message.JobId); if (job is null) { @@ -30,13 +30,12 @@ namespace Musify.Infrastructure.Messaging.Consumers await database.Jobs.AddAsync(job, consumeContext.CancellationToken); logger.LogInformation("Created new job with id {JobId} for removing file {BucketName}/{KeyName}", job.Id, consumeContext.Message.BucketName, consumeContext.Message.KeyName); } - else + + var jobExecution = new JobExecution { - job.RetryCount++; - job.JobState = JobState.Pending; - database.Jobs.Update(job); - logger.LogInformation("Retrying job with id {JobId} for removing file {BucketName}/{KeyName}. Retry count: {RetryCount}", job.Id, consumeContext.Message.BucketName, consumeContext.Message.KeyName, job.RetryCount); - } + JobId = job.Id + }; + await database.JobExecutions.AddAsync(jobExecution, consumeContext.CancellationToken); var removeFile = await storageHandler.RemoveFileAsync( consumeContext.Message.BucketName, @@ -45,15 +44,17 @@ namespace Musify.Infrastructure.Messaging.Consumers if (!removeFile.IsSuccess) { - job.JobState = JobState.Failed; - logger.LogInformation("Failed to remove file {BucketName}/{KeyName} for job with id {JobId}. Error: {ErrorMessage}", consumeContext.Message.BucketName, consumeContext.Message.KeyName, job.Id, string.Join("; ", removeFile.Errors)); + jobExecution.JobState = JobState.Failed; + logger.LogInformation("Failed to remove file {BucketName}/{KeyName} for jobId={JobId}, JobExecutionId={jobExecutionId}. Error: {ErrorMessage}", + consumeContext.Message.BucketName, consumeContext.Message.KeyName, job.Id, jobExecution.Id, string.Join("; ", removeFile.Errors)); } else { - job.CompletedAt = DateTime.UtcNow; - logger.LogInformation("Successfully removed file {BucketName}/{KeyName} for job with id {JobId}.", consumeContext.Message.BucketName, consumeContext.Message.KeyName, job.Id); + logger.LogInformation("Successfully removed file {BucketName}/{KeyName} for jobId={JobId}, JobExecutionId={jobExecutionId}.", + consumeContext.Message.BucketName, consumeContext.Message.KeyName, job.Id, jobExecution.Id); } + jobExecution.FinishedAt = DateTime.UtcNow; await database.SaveChangesAsync(consumeContext.CancellationToken); } } diff --git a/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs b/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs index 3c5ed0f..bdbb334 100644 --- a/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs +++ b/Infrastructure/Messaging/Consumers/ResizePictureConsumer.cs @@ -18,14 +18,14 @@ namespace Musify.Infrastructure.Messaging.Consumers { try { - Dictionary jobs = new Dictionary(); + Dictionary jobsExecutions = new Dictionary(); foreach (var item in consumeContext.Message.Items) { var payloadJson = JsonSerializer.Serialize(item); var job = await database.Jobs - .SingleOrDefaultAsync(job => job.JobType == JobType.ResizePicture && job.Payload == payloadJson, consumeContext.CancellationToken); + .SingleOrDefaultAsync(job => job.Id == consumeContext.Message.JobId); if (job is null) { @@ -36,14 +36,14 @@ namespace Musify.Infrastructure.Messaging.Consumers }; await database.Jobs.AddAsync(job, consumeContext.CancellationToken); } - else - { - job.RetryCount++; - job.JobState = JobState.InProgress; - database.Jobs.Update(job); - } - jobs.Add(item, job); + var jobExecution = new JobExecution + { + JobId = consumeContext.Message.JobId + }; + await database.JobExecutions.AddAsync(jobExecution, consumeContext.CancellationToken); + + jobsExecutions.Add(item, jobExecution); } var fileResult = await storageHandler.GetFileAsync( @@ -56,7 +56,7 @@ namespace Musify.Infrastructure.Messaging.Consumers logger.LogWarning("File not found in storage: BucketName={BucketName}, KeyName={KeyName}", consumeContext.Message.BucketName, consumeContext.Message.KeyName); - foreach (var job in jobs) + foreach (var job in jobsExecutions) { job.Value.JobState = JobState.Failed; } @@ -70,12 +70,12 @@ namespace Musify.Infrastructure.Messaging.Consumers foreach (var item in consumeContext.Message.Items) { - var job = jobs[item]; + var jobExecution = jobsExecutions[item]; try { await ResizePicture( - job, + jobExecution, consumeContext.Message.BucketName, memoryStream, item, @@ -83,7 +83,7 @@ namespace Musify.Infrastructure.Messaging.Consumers } catch (Exception exception) { - logger.LogError(exception, "An error occurred while resizing picture for UploadId={UploadId} and Resize={Resize}", job.Id, item); + logger.LogError(exception, "An error occurred while resizing picture for UploadId={UploadId} and Resize={Resize}", jobExecution.Id, item); continue; } } @@ -100,10 +100,10 @@ namespace Musify.Infrastructure.Messaging.Consumers } } - async Task ResizePicture(Job job, string bucketName, MemoryStream memoryStream, ResizePictureItems items, CancellationToken cancellationToken) + async Task ResizePicture(JobExecution jobExecution, string bucketName, MemoryStream memoryStream, ResizePictureItems items, CancellationToken cancellationToken) { memoryStream.Position = 0; - job.JobState = JobState.InProgress; + jobExecution.JobState = JobState.InProgress; var pictureResult = await pictureHandler.ResizePictureAsync( memoryStream, @@ -114,7 +114,7 @@ namespace Musify.Infrastructure.Messaging.Consumers if (!pictureResult.IsSuccess) { logger.LogError("Failed to resize picture: {ErrorMessage}", string.Join("; ", pictureResult.Errors)); - job.JobState = JobState.Failed; + jobExecution.JobState = JobState.Failed; return; } @@ -128,11 +128,12 @@ namespace Musify.Infrastructure.Messaging.Consumers if (!saveResult.IsSuccess) { logger.LogError("Failed to upload resized picture: {ErrorMessage}", string.Join("; ", saveResult.Errors)); - job.JobState = JobState.Failed; + jobExecution.JobState = JobState.Failed; return; } - job.JobState = JobState.Completed; + jobExecution.JobState = JobState.Completed; + jobExecution.FinishedAt = DateTime.UtcNow; } } } \ No newline at end of file diff --git a/Infrastructure/Persistence/Database.cs b/Infrastructure/Persistence/Database.cs index 5148f5a..be2c9d1 100644 --- a/Infrastructure/Persistence/Database.cs +++ b/Infrastructure/Persistence/Database.cs @@ -24,5 +24,7 @@ namespace Musify.Infrastructure.Persistence public DbSet Uploads => Set(); public DbSet Jobs => Set(); + + public DbSet JobExecutions => Set(); } } \ No newline at end of file