Rework jobs

This commit is contained in:
2026-03-20 21:40:05 +01:00
parent 93a975e9b5
commit b34a711d2d
22 changed files with 449 additions and 157 deletions
@@ -1,7 +0,0 @@
namespace Musify.Application.Contracts.Application
{
public interface IEvent
{
Guid JobId { get; }
}
}
@@ -15,9 +15,9 @@ namespace Musify.Application.Contracts.Infrastructure
DbSet<Upload> Uploads { get; } DbSet<Upload> Uploads { get; }
DbSet<Job> Jobs { get; } DbSet<ProcessExecution> ProcessExecutions { get; }
DbSet<JobOperation> JobOperations { get; } DbSet<ProcessStepExecution> ProcessStepExecutions { get; }
Task<int> SaveChangesAsync(CancellationToken cancellationToken); Task<int> SaveChangesAsync(CancellationToken cancellationToken);
} }
@@ -0,0 +1,25 @@
using Musify.Domain.Entities;
namespace Musify.Application.Contracts.Infrastructure
{
public interface IProcessTrackingStore
{
Task<Guid> GetOrCreateProcessAsync(
string processName,
Guid? correlationId,
Guid? conversationId,
Guid? messageId,
CancellationToken cancellationToken);
Task<Guid> StartStepAsync(
Guid processExecutionId,
string stepName,
ProcessStepComponentType componentType,
int attempt,
CancellationToken cancellationToken);
Task CompleteStepAsync(Guid processExecutionId, Guid stepExecutionId, CancellationToken cancellationToken);
Task FailStepAsync(Guid processExecutionId, Guid stepExecutionId, string errorMessage, CancellationToken cancellationToken);
}
}
+1 -2
View File
@@ -3,7 +3,6 @@
namespace Musify.Application.Events namespace Musify.Application.Events
{ {
public record RemoveFileEvent( public record RemoveFileEvent(
Guid JobId,
string BucketName, string BucketName,
string KeyName) : IEvent; string KeyName);
} }
@@ -70,7 +70,6 @@ namespace Musify.Application.PlayLists.Commands.DeletePlayList
try try
{ {
await eventBus.PublishAsync(new RemoveFileEvent( await eventBus.PublishAsync(new RemoveFileEvent(
Guid.NewGuid(),
storageConfiguration.BucketName, storageConfiguration.BucketName,
keyName), cancellationToken); keyName), cancellationToken);
@@ -1,6 +1,5 @@
using Ardalis.Result; using Ardalis.Result;
using DispatchR.Abstractions.Send; using DispatchR.Abstractions.Send;
using MassTransit;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Musify.Application.Configuration; using Musify.Application.Configuration;
@@ -54,7 +53,6 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList
try try
{ {
await eventBus.PublishAsync(new RemoveFileEvent( await eventBus.PublishAsync(new RemoveFileEvent(
Guid.NewGuid(),
storageConfiguration.BucketName, storageConfiguration.BucketName,
originalImageStorageKey), cancellationToken); originalImageStorageKey), cancellationToken);
} }
@@ -72,7 +70,6 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList
try try
{ {
await eventBus.PublishAsync(new RemoveFileEvent( await eventBus.PublishAsync(new RemoveFileEvent(
Guid.NewGuid(),
storageConfiguration.BucketName, storageConfiguration.BucketName,
playList.SmallPictureKeyName), cancellationToken); playList.SmallPictureKeyName), cancellationToken);
} }
@@ -88,7 +85,6 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList
try try
{ {
await eventBus.PublishAsync(new RemoveFileEvent( await eventBus.PublishAsync(new RemoveFileEvent(
Guid.NewGuid(),
storageConfiguration.BucketName, storageConfiguration.BucketName,
playList.MediumPictureKeyName), cancellationToken); playList.MediumPictureKeyName), cancellationToken);
} }
@@ -104,7 +100,6 @@ namespace Musify.Application.PlayLists.Commands.UpdatePlayList
try try
{ {
await eventBus.PublishAsync(new RemoveFileEvent( await eventBus.PublishAsync(new RemoveFileEvent(
Guid.NewGuid(),
storageConfiguration.BucketName, storageConfiguration.BucketName,
playList.LargePictureKeyName), cancellationToken); playList.LargePictureKeyName), cancellationToken);
} }
-22
View File
@@ -1,22 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Musify.Domain.Entities
{
[Table("Job")]
public class Job
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
public required JobType JobType { get; set; }
[Required]
[MaxLength(512)]
public required string Payload { get; set; }
[Required]
public DateTime CreateAt { get; set; } = DateTime.UtcNow;
}
}
-41
View File
@@ -1,41 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Musify.Domain.Entities
{
#pragma warning disable CS8618
[Table("JobExecution")]
public class JobOperation
{
[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]
[MaxLength(512)]
public string ErrorMessage { get; set; } = string.Empty;
[Required]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[Required]
public DateTime FinishedAt { get; set; }
[ForeignKey(nameof(JobId)) ]
public Job Job { get; set; }
}
}
-10
View File
@@ -1,10 +0,0 @@
namespace Musify.Domain.Entities
{
public enum JobState
{
Pending,
Failed,
InProgress,
Completed
}
}
-8
View File
@@ -1,8 +0,0 @@
namespace Musify.Domain.Entities
{
public enum JobType
{
ResizePicture,
RemoveFile,
}
}
+36
View File
@@ -0,0 +1,36 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Musify.Domain.Entities
{
#pragma warning disable CS8618
[Table("ProcessExecution")]
public class ProcessExecution
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
public Guid? CorrelationId { get; set; }
public Guid? ConversationId { get; set; }
public Guid? MessageId { get; set; }
[Required]
[MaxLength(256)]
public required string ProcessName { get; set; }
[Required]
public ProcessExecutionStatus Status { get; set; } = ProcessExecutionStatus.Running;
[Required]
public DateTime StartedDateTime { get; set; } = DateTime.UtcNow;
public DateTime FinishedDateTime { get; set; }
[MaxLength(2048)]
public string ErrorMessage { get; set; }
public ICollection<ProcessStepExecution> Steps { get; set; } = new List<ProcessStepExecution>();
}
}
+10
View File
@@ -0,0 +1,10 @@
namespace Musify.Domain.Entities
{
public enum ProcessExecutionStatus
{
Running,
Succeeded,
Failed,
Compensated
}
}
@@ -0,0 +1,8 @@
namespace Musify.Domain.Entities
{
public enum ProcessStepComponentType
{
Consumer,
Activity
}
}
+40
View File
@@ -0,0 +1,40 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Musify.Domain.Entities
{
#pragma warning disable CS8618
[Table("ProcessStepExecution")]
public class ProcessStepExecution
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
public Guid ProcessExecutionId { get; set; }
[Required]
[MaxLength(256)]
public required string StepName { get; set; }
[Required]
public ProcessStepComponentType ComponentType { get; set; }
[Required]
public ProcessExecutionStatus Status { get; set; } = ProcessExecutionStatus.Running;
[Required]
public int Attempt { get; set; }
[Required]
public DateTime StartedDateTime { get; set; } = DateTime.UtcNow;
public DateTime FinishedDateTime { get; set; }
[MaxLength(2048)]
public string ErrorMessage { get; set; }
[ForeignKey(nameof(ProcessExecutionId)) ]
public ProcessExecution ProcessExecution { get; set; }
}
}
@@ -21,7 +21,8 @@ namespace Musify.Infrastructure
.AddStorageHandler() .AddStorageHandler()
.AddPictureHandler() .AddPictureHandler()
.AddMassTransitClient(configuration) .AddMassTransitClient(configuration)
.AddScoped<IEventBus, MassTransitEventBus>(); .AddScoped<IEventBus, MassTransitEventBus>()
.AddScoped<IProcessTrackingStore, ProcessTrackingStore>();
return serviceDescriptors; return serviceDescriptors;
} }
@@ -34,7 +35,8 @@ namespace Musify.Infrastructure
.AddStorageHandler() .AddStorageHandler()
.AddPictureHandler() .AddPictureHandler()
.AddMassTransitConsumers(configuration) .AddMassTransitConsumers(configuration)
.AddScoped<IEventBus, MassTransitEventBus>(); .AddScoped<IEventBus, MassTransitEventBus>()
.AddScoped<IProcessTrackingStore, ProcessTrackingStore>();
return serviceDescriptors; return serviceDescriptors;
} }
@@ -1,33 +1,60 @@
using MassTransit; using MassTransit;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Musify.Application.Contracts.Infrastructure; using Musify.Application.Contracts.Infrastructure;
using Musify.Domain.Entities;
using Musify.Infrastructure.Messaging.Activities.Arguments; using Musify.Infrastructure.Messaging.Activities.Arguments;
namespace Musify.Infrastructure.Messaging.Activities namespace Musify.Infrastructure.Messaging.Activities
{ {
public class RemoveFilesActivity(IStorageHandler storageHandler, ILogger<RemoveFilesActivity> logger) public class RemoveFilesActivity(
IStorageHandler storageHandler,
ILogger<RemoveFilesActivity> logger,
IProcessTrackingStore processTrackingStore)
: IExecuteActivity<RemoveFileArguments> : IExecuteActivity<RemoveFileArguments>
{ {
public const string ExecuteEndpointName = "Remove-File"; public const string ExecuteEndpointName = "Remove-File";
public async Task<ExecutionResult> Execute(ExecuteContext<RemoveFileArguments> executeContext) public async Task<ExecutionResult> Execute(ExecuteContext<RemoveFileArguments> executeContext)
{ {
var removeFile = await storageHandler.RemoveFileAsync( var processId = await processTrackingStore.GetOrCreateProcessAsync(
executeContext.Arguments.BucketName, "RoutingSlip",
executeContext.Arguments.KeyName, executeContext.CorrelationId ?? executeContext.TrackingNumber,
executeContext.ConversationId,
executeContext.MessageId,
executeContext.CancellationToken); executeContext.CancellationToken);
if (!removeFile.IsSuccess) var stepId = await processTrackingStore.StartStepAsync(
{ processId,
string errorMessage = string.Join(";", removeFile.Errors); nameof(RemoveFilesActivity),
logger.LogWarning( ProcessStepComponentType.Activity,
"Failed to remove file {KeyName} from bucket {BucketName}: {ErrorMessage}", 0,
executeContext.Arguments.KeyName, executeContext.CancellationToken);
executeContext.Arguments.BucketName,
errorMessage);
}
return executeContext.Completed(); try
{
var removeFile = await storageHandler.RemoveFileAsync(
executeContext.Arguments.BucketName,
executeContext.Arguments.KeyName,
executeContext.CancellationToken);
if (!removeFile.IsSuccess)
{
string errorMessage = string.Join(";", removeFile.Errors);
logger.LogWarning(
"Failed to remove file {KeyName} from bucket {BucketName}: {ErrorMessage}",
executeContext.Arguments.KeyName,
executeContext.Arguments.BucketName,
errorMessage);
}
await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken);
return executeContext.Completed();
}
catch (Exception exception)
{
await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken);
throw;
}
} }
} }
} }
@@ -1,53 +1,80 @@
using MassTransit; using MassTransit;
using Musify.Application.Contracts.Infrastructure; using Musify.Application.Contracts.Infrastructure;
using Musify.Domain.Entities;
using Musify.Infrastructure.Messaging.Activities.Arguments; using Musify.Infrastructure.Messaging.Activities.Arguments;
namespace Musify.Infrastructure.Messaging.Activities namespace Musify.Infrastructure.Messaging.Activities
{ {
public class ResizePictureActivity(IStorageHandler storageHandler, IPictureHandler pictureHandler) public class ResizePictureActivity(
IStorageHandler storageHandler,
IPictureHandler pictureHandler,
IProcessTrackingStore processTrackingStore)
: IExecuteActivity<ResizePictureArgument> : IExecuteActivity<ResizePictureArgument>
{ {
public const string ExecuteEndpointName = "Resize-Picture"; public const string ExecuteEndpointName = "Resize-Picture";
public async Task<ExecutionResult> Execute(ExecuteContext<ResizePictureArgument> executeContext) public async Task<ExecutionResult> Execute(ExecuteContext<ResizePictureArgument> executeContext)
{ {
var file = await storageHandler.GetFileAsync( var processId = await processTrackingStore.GetOrCreateProcessAsync(
executeContext.Arguments.OriginalBucketName, "RoutingSlip",
executeContext.Arguments.OriginalKeyName, executeContext.CorrelationId ?? executeContext.TrackingNumber,
executeContext.ConversationId,
executeContext.MessageId,
executeContext.CancellationToken); executeContext.CancellationToken);
if (!file.IsSuccess) var stepId = await processTrackingStore.StartStepAsync(
{ processId,
string errorMessages = string.Join(", ", file.Errors); nameof(ResizePictureActivity),
throw new Exception(errorMessages); ProcessStepComponentType.Activity,
} 0,
var resizedPicture = await pictureHandler.ResizePictureAsync(
file.Value,
executeContext.Arguments.Width,
executeContext.Arguments.Height,
executeContext.CancellationToken); executeContext.CancellationToken);
if (!resizedPicture.IsSuccess) try
{ {
string errorMessage = string.Join(", ", resizedPicture.Errors); var file = await storageHandler.GetFileAsync(
throw new Exception(errorMessage); executeContext.Arguments.OriginalBucketName,
executeContext.Arguments.OriginalKeyName,
executeContext.CancellationToken);
if (!file.IsSuccess)
{
string errorMessages = string.Join(", ", file.Errors);
throw new Exception(errorMessages);
}
var resizedPicture = await pictureHandler.ResizePictureAsync(
file.Value,
executeContext.Arguments.Width,
executeContext.Arguments.Height,
executeContext.CancellationToken);
if (!resizedPicture.IsSuccess)
{
string errorMessage = string.Join(", ", resizedPicture.Errors);
throw new Exception(errorMessage);
}
var uploadFile = await storageHandler.UploadFileAsync(
resizedPicture.Value,
pictureHandler.ContentType,
executeContext.Arguments.DestinationBucketName,
executeContext.Arguments.DestinationKeyName,
executeContext.CancellationToken);
if (!uploadFile.IsSuccess)
{
var errorMessage = string.Join(", ", uploadFile.Errors);
throw new Exception(errorMessage);
}
await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken);
return executeContext.Completed();
} }
catch (Exception exception)
var uploadFile = await storageHandler.UploadFileAsync(
resizedPicture.Value,
pictureHandler.ContentType,
executeContext.Arguments.DestinationBucketName,
executeContext.Arguments.DestinationKeyName,
executeContext.CancellationToken);
if (!uploadFile.IsSuccess)
{ {
var errorMessage = string.Join(", ", uploadFile.Errors); await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken);
throw new Exception(errorMessage); throw;
} }
return executeContext.Completed();
} }
} }
} }
@@ -1,31 +1,53 @@
using MassTransit; using MassTransit;
using Microsoft.EntityFrameworkCore;
using Musify.Application.Contracts.Infrastructure; using Musify.Application.Contracts.Infrastructure;
using Musify.Domain.Entities;
using Musify.Infrastructure.Messaging.Activities.Arguments; using Musify.Infrastructure.Messaging.Activities.Arguments;
namespace Musify.Infrastructure.Messaging.Activities namespace Musify.Infrastructure.Messaging.Activities
{ {
public class UpdatePlayListPictureActivity(IDatabase database) public class UpdatePlayListPictureActivity(IDatabase database, IProcessTrackingStore processTrackingStore)
: IExecuteActivity<UpdatePlayListPictureArguments> : IExecuteActivity<UpdatePlayListPictureArguments>
{ {
public const string ExecuteEndpointName = "Update-PlayList-Picture"; public const string ExecuteEndpointName = "Update-PlayList-Picture";
public async Task<ExecutionResult> Execute(ExecuteContext<UpdatePlayListPictureArguments> executeContext) public async Task<ExecutionResult> Execute(ExecuteContext<UpdatePlayListPictureArguments> executeContext)
{ {
var playList = await database.PlayLists.SingleOrDefaultAsync(p => p.Id == executeContext.Arguments.PlayListId, var processId = await processTrackingStore.GetOrCreateProcessAsync(
"RoutingSlip",
executeContext.CorrelationId ?? executeContext.TrackingNumber,
executeContext.ConversationId,
executeContext.MessageId,
executeContext.CancellationToken); executeContext.CancellationToken);
if (playList is null) var stepId = await processTrackingStore.StartStepAsync(
throw new Exception($"PlayList with id {executeContext.Arguments.PlayListId} not found."); processId,
nameof(UpdatePlayListPictureActivity),
ProcessStepComponentType.Activity,
0,
executeContext.CancellationToken);
playList.SmallPictureKeyName = executeContext.Arguments.SmallPictureKeyName; try
playList.MediumPictureKeyName = executeContext.Arguments.MediumPictureKeyName; {
playList.LargePictureKeyName = executeContext.Arguments.LargePictureKeyName; var playList = await database.PlayLists.FindAsync(executeContext.Arguments.PlayListId, executeContext.CancellationToken);
database.PlayLists.Update(playList); if (playList is null)
await database.SaveChangesAsync(executeContext.CancellationToken); throw new Exception($"PlayList with id {executeContext.Arguments.PlayListId} not found.");
return executeContext.Completed(); playList.SmallPictureKeyName = executeContext.Arguments.SmallPictureKeyName;
playList.MediumPictureKeyName = executeContext.Arguments.MediumPictureKeyName;
playList.LargePictureKeyName = executeContext.Arguments.LargePictureKeyName;
database.PlayLists.Update(playList);
await database.SaveChangesAsync(executeContext.CancellationToken);
await processTrackingStore.CompleteStepAsync(processId, stepId, executeContext.CancellationToken);
return executeContext.Completed();
}
catch (Exception exception)
{
await processTrackingStore.FailStepAsync(processId, stepId, exception.Message, executeContext.CancellationToken);
throw;
}
} }
} }
} }
@@ -0,0 +1,48 @@
using MassTransit;
using Musify.Application.Contracts.Infrastructure;
using Musify.Domain.Entities;
namespace Musify.Infrastructure.Messaging.Filters
{
public class ProcessTrackingConsumeFilter<TMessage>(IProcessTrackingStore processTrackingStore)
: IFilter<ConsumeContext<TMessage>>
where TMessage : class
{
public async Task Send(ConsumeContext<TMessage> context, IPipe<ConsumeContext<TMessage>> next)
{
var processId = await processTrackingStore.GetOrCreateProcessAsync(
typeof(TMessage).Name,
context.CorrelationId ?? context.MessageId,
context.ConversationId,
context.MessageId,
context.CancellationToken);
var stepId = await processTrackingStore.StartStepAsync(
processId,
$"{typeof(TMessage).Name}.Consume",
ProcessStepComponentType.Consumer,
0,
context.CancellationToken);
try
{
await next.Send(context);
await processTrackingStore.CompleteStepAsync(processId, stepId, context.CancellationToken);
}
catch (Exception exception)
{
await processTrackingStore.FailStepAsync(
processId,
stepId,
exception.Message,
context.CancellationToken);
throw;
}
}
public void Probe(ProbeContext context)
{
context.CreateFilterScope(nameof(ProcessTrackingConsumeFilter<TMessage>));
}
}
}
@@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection;
using Musify.Infrastructure.Messaging.Activities; using Musify.Infrastructure.Messaging.Activities;
using Musify.Infrastructure.Messaging.Activities.Arguments; using Musify.Infrastructure.Messaging.Activities.Arguments;
using Musify.Infrastructure.Messaging.Consumers; using Musify.Infrastructure.Messaging.Consumers;
using Musify.Infrastructure.Messaging.Filters;
namespace Musify.Infrastructure.Messaging namespace Musify.Infrastructure.Messaging
{ {
@@ -34,6 +35,8 @@ namespace Musify.Infrastructure.Messaging
{ {
ConfigureRabbitMqHost(busFactoryConfigurator, configuration); ConfigureRabbitMqHost(busFactoryConfigurator, configuration);
busFactoryConfigurator.UseConsumeFilter(typeof(ProcessTrackingConsumeFilter<>), busRegistrationContext);
busFactoryConfigurator.ReceiveEndpoint(UpdatePlayListPictureConsumer.QueueName, endpointConfigurator => busFactoryConfigurator.ReceiveEndpoint(UpdatePlayListPictureConsumer.QueueName, endpointConfigurator =>
{ {
endpointConfigurator.ConfigureConsumer<UpdatePlayListPictureConsumer>(busRegistrationContext); endpointConfigurator.ConfigureConsumer<UpdatePlayListPictureConsumer>(busRegistrationContext);
+19 -2
View File
@@ -23,8 +23,25 @@ namespace Musify.Infrastructure.Persistence
public DbSet<Upload> Uploads => Set<Upload>(); public DbSet<Upload> Uploads => Set<Upload>();
public DbSet<Job> Jobs => Set<Job>(); public DbSet<ProcessExecution> ProcessExecutions => Set<ProcessExecution>();
public DbSet<JobOperation> JobOperations => Set<JobOperation>(); public DbSet<ProcessStepExecution> ProcessStepExecutions => Set<ProcessStepExecution>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ProcessExecution>()
.HasIndex(process => new { process.CorrelationId, process.ProcessName });
modelBuilder.Entity<ProcessStepExecution>()
.HasIndex(step => new { step.ProcessExecutionId, step.StepName });
modelBuilder.Entity<ProcessExecution>()
.HasMany(process => process.Steps)
.WithOne(step => step.ProcessExecution)
.HasForeignKey(step => step.ProcessExecutionId)
.OnDelete(DeleteBehavior.Cascade);
base.OnModelCreating(modelBuilder);
}
} }
} }
@@ -0,0 +1,122 @@
using Microsoft.EntityFrameworkCore;
using Musify.Application.Contracts.Infrastructure;
using Musify.Domain.Entities;
namespace Musify.Infrastructure.Persistence
{
public class ProcessTrackingStore(IDatabase database)
: IProcessTrackingStore
{
public async Task<Guid> GetOrCreateProcessAsync(
string processName,
Guid? correlationId,
Guid? conversationId,
Guid? messageId,
CancellationToken cancellationToken)
{
ProcessExecution? existingProcess = null;
if (correlationId.HasValue)
{
existingProcess = await database.ProcessExecutions
.OrderByDescending(process => process.StartedDateTime)
.FirstOrDefaultAsync(
process => process.CorrelationId == correlationId && process.ProcessName == processName,
cancellationToken);
}
if (existingProcess is not null)
{
return existingProcess.Id;
}
var newProcess = new ProcessExecution
{
ProcessName = processName,
CorrelationId = correlationId,
ConversationId = conversationId,
MessageId = messageId,
Status = ProcessExecutionStatus.Running,
StartedDateTime = DateTime.UtcNow,
};
await database.ProcessExecutions.AddAsync(newProcess, cancellationToken);
await database.SaveChangesAsync(cancellationToken);
return newProcess.Id;
}
public async Task<Guid> StartStepAsync(
Guid processExecutionId,
string stepName,
ProcessStepComponentType componentType,
int attempt,
CancellationToken cancellationToken)
{
var step = new ProcessStepExecution
{
ProcessExecutionId = processExecutionId,
StepName = stepName,
ComponentType = componentType,
Attempt = attempt,
Status = ProcessExecutionStatus.Running,
StartedDateTime = DateTime.UtcNow,
};
await database.ProcessStepExecutions.AddAsync(step, cancellationToken);
await database.SaveChangesAsync(cancellationToken);
return step.Id;
}
public async Task CompleteStepAsync(Guid processExecutionId, Guid stepExecutionId, CancellationToken cancellationToken)
{
var step = await database.ProcessStepExecutions
.SingleAsync(currentStep => currentStep.Id == stepExecutionId, cancellationToken);
step.Status = ProcessExecutionStatus.Succeeded;
step.FinishedDateTime = DateTime.UtcNow;
var process = await database.ProcessExecutions
.SingleAsync(currentProcess => currentProcess.Id == processExecutionId, cancellationToken);
var hasRunningSteps = await database.ProcessStepExecutions
.AnyAsync(
currentStep => currentStep.ProcessExecutionId == processExecutionId
&& currentStep.Status == ProcessExecutionStatus.Running,
cancellationToken);
var hasFailedSteps = await database.ProcessStepExecutions
.AnyAsync(
currentStep => currentStep.ProcessExecutionId == processExecutionId
&& currentStep.Status == ProcessExecutionStatus.Failed,
cancellationToken);
if (!hasRunningSteps && !hasFailedSteps)
{
process.Status = ProcessExecutionStatus.Succeeded;
process.FinishedDateTime = DateTime.UtcNow;
process.ErrorMessage = string.Empty;
}
await database.SaveChangesAsync(cancellationToken);
}
public async Task FailStepAsync(Guid processExecutionId, Guid stepExecutionId, string errorMessage, CancellationToken cancellationToken)
{
var step = await database.ProcessStepExecutions
.SingleAsync(currentStep => currentStep.Id == stepExecutionId, cancellationToken);
step.Status = ProcessExecutionStatus.Failed;
step.FinishedDateTime = DateTime.UtcNow;
step.ErrorMessage = errorMessage;
var process = await database.ProcessExecutions
.SingleAsync(currentProcess => currentProcess.Id == processExecutionId, cancellationToken);
process.Status = ProcessExecutionStatus.Failed;
process.FinishedDateTime = DateTime.UtcNow;
process.ErrorMessage = errorMessage;
await database.SaveChangesAsync(cancellationToken);
}
}
}