diff --git a/Directory.Build.props b/Directory.Build.props index 4d4d6f2e..c0607132 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,7 +6,7 @@ enable true latest-recommended - $(NoWarn);CA1859;CA1822;CA1848;CA1873;CA1305;CA1707;CA1861;xUnit2031 + $(NoWarn);CA1822;CA1873;CA1305;CA1707;CA1861;xUnit2031 true true diff --git a/src/KArtSell.Modules.ModelOperations/Domain/ModelOperationExecution.cs b/src/KArtSell.Modules.ModelOperations/Domain/ModelOperationExecution.cs index dbcf3b1d..6288df3f 100644 --- a/src/KArtSell.Modules.ModelOperations/Domain/ModelOperationExecution.cs +++ b/src/KArtSell.Modules.ModelOperations/Domain/ModelOperationExecution.cs @@ -19,15 +19,15 @@ public sealed record ModelOperationExecutionTransition( public sealed class ModelOperationExecution { - private static readonly IReadOnlyDictionary> Allowed = - new Dictionary> + private static readonly Dictionary> Allowed = + new() { - [ModelOperationExecutionState.Requested] = Set(ModelOperationExecutionState.Running, ModelOperationExecutionState.BusinessHold, ModelOperationExecutionState.Quarantined), - [ModelOperationExecutionState.Running] = Set(ModelOperationExecutionState.Succeeded, ModelOperationExecutionState.BusinessHold, ModelOperationExecutionState.Failed, ModelOperationExecutionState.Quarantined), - [ModelOperationExecutionState.BusinessHold] = Set(ModelOperationExecutionState.Running, ModelOperationExecutionState.Quarantined), - [ModelOperationExecutionState.Failed] = Set(ModelOperationExecutionState.Running, ModelOperationExecutionState.Quarantined), - [ModelOperationExecutionState.Succeeded] = new HashSet(), - [ModelOperationExecutionState.Quarantined] = new HashSet() + [ModelOperationExecutionState.Requested] = new(new[] { ModelOperationExecutionState.Running, ModelOperationExecutionState.BusinessHold, ModelOperationExecutionState.Quarantined }), + [ModelOperationExecutionState.Running] = new(new[] { ModelOperationExecutionState.Succeeded, ModelOperationExecutionState.BusinessHold, ModelOperationExecutionState.Failed, ModelOperationExecutionState.Quarantined }), + [ModelOperationExecutionState.BusinessHold] = new(new[] { ModelOperationExecutionState.Running, ModelOperationExecutionState.Quarantined }), + [ModelOperationExecutionState.Failed] = new(new[] { ModelOperationExecutionState.Running, ModelOperationExecutionState.Quarantined }), + [ModelOperationExecutionState.Succeeded] = new(), + [ModelOperationExecutionState.Quarantined] = new() }; private readonly List transitions = []; @@ -63,5 +63,5 @@ public sealed class ModelOperationExecution return transition; } - private static IReadOnlySet Set(params ModelOperationExecutionState[] states) => new HashSet(states); + private static HashSet Set(params ModelOperationExecutionState[] states) => new(states); } diff --git a/src/KArtSell.Modules.ModelOperations/FeedbackLoop/ModelFeedbackCycle.cs b/src/KArtSell.Modules.ModelOperations/FeedbackLoop/ModelFeedbackCycle.cs index 0b928d04..0fbe8802 100644 --- a/src/KArtSell.Modules.ModelOperations/FeedbackLoop/ModelFeedbackCycle.cs +++ b/src/KArtSell.Modules.ModelOperations/FeedbackLoop/ModelFeedbackCycle.cs @@ -24,19 +24,19 @@ public sealed record ModelFeedbackTransition( public sealed class ModelFeedbackCycle { - private static readonly IReadOnlyDictionary> Allowed = - new Dictionary> + private static readonly Dictionary> Allowed = + new() { - [ModelFeedbackCycleState.Planned] = Set(ModelFeedbackCycleState.PredictionFrozen, ModelFeedbackCycleState.BusinessHold), - [ModelFeedbackCycleState.PredictionFrozen] = Set(ModelFeedbackCycleState.OutcomesMaturing, ModelFeedbackCycleState.BusinessHold), - [ModelFeedbackCycleState.OutcomesMaturing] = Set(ModelFeedbackCycleState.Evaluated, ModelFeedbackCycleState.BusinessHold), - [ModelFeedbackCycleState.Evaluated] = Set(ModelFeedbackCycleState.ImprovementProposed, ModelFeedbackCycleState.Closed, ModelFeedbackCycleState.BusinessHold), - [ModelFeedbackCycleState.ImprovementProposed] = Set(ModelFeedbackCycleState.ChallengerPlanned, ModelFeedbackCycleState.Closed, ModelFeedbackCycleState.BusinessHold), - [ModelFeedbackCycleState.ChallengerPlanned] = Set(ModelFeedbackCycleState.IndependentlyValidated, ModelFeedbackCycleState.BusinessHold), - [ModelFeedbackCycleState.IndependentlyValidated] = Set(ModelFeedbackCycleState.PromotionReviewPending, ModelFeedbackCycleState.Closed, ModelFeedbackCycleState.BusinessHold), - [ModelFeedbackCycleState.PromotionReviewPending] = Set(ModelFeedbackCycleState.Closed, ModelFeedbackCycleState.BusinessHold), - [ModelFeedbackCycleState.BusinessHold] = Set(ModelFeedbackCycleState.Planned, ModelFeedbackCycleState.Closed), - [ModelFeedbackCycleState.Closed] = new HashSet() + [ModelFeedbackCycleState.Planned] = new(new[] { ModelFeedbackCycleState.PredictionFrozen, ModelFeedbackCycleState.BusinessHold }), + [ModelFeedbackCycleState.PredictionFrozen] = new(new[] { ModelFeedbackCycleState.OutcomesMaturing, ModelFeedbackCycleState.BusinessHold }), + [ModelFeedbackCycleState.OutcomesMaturing] = new(new[] { ModelFeedbackCycleState.Evaluated, ModelFeedbackCycleState.BusinessHold }), + [ModelFeedbackCycleState.Evaluated] = new(new[] { ModelFeedbackCycleState.ImprovementProposed, ModelFeedbackCycleState.Closed, ModelFeedbackCycleState.BusinessHold }), + [ModelFeedbackCycleState.ImprovementProposed] = new(new[] { ModelFeedbackCycleState.ChallengerPlanned, ModelFeedbackCycleState.Closed, ModelFeedbackCycleState.BusinessHold }), + [ModelFeedbackCycleState.ChallengerPlanned] = new(new[] { ModelFeedbackCycleState.IndependentlyValidated, ModelFeedbackCycleState.BusinessHold }), + [ModelFeedbackCycleState.IndependentlyValidated] = new(new[] { ModelFeedbackCycleState.PromotionReviewPending, ModelFeedbackCycleState.Closed, ModelFeedbackCycleState.BusinessHold }), + [ModelFeedbackCycleState.PromotionReviewPending] = new(new[] { ModelFeedbackCycleState.Closed, ModelFeedbackCycleState.BusinessHold }), + [ModelFeedbackCycleState.BusinessHold] = new(new[] { ModelFeedbackCycleState.Planned, ModelFeedbackCycleState.Closed }), + [ModelFeedbackCycleState.Closed] = new() }; private readonly List transitions = []; @@ -89,6 +89,6 @@ public sealed class ModelFeedbackCycle return transition; } - private static IReadOnlySet Set(params ModelFeedbackCycleState[] states) - => new HashSet(states); + private static HashSet Set(params ModelFeedbackCycleState[] states) + => new(states); } diff --git a/src/KArtSell.Modules.ModelOperations/Scheduling/ModelOperationsDispatcherJob.cs b/src/KArtSell.Modules.ModelOperations/Scheduling/ModelOperationsDispatcherJob.cs index f3170a43..0a78c9ca 100644 --- a/src/KArtSell.Modules.ModelOperations/Scheduling/ModelOperationsDispatcherJob.cs +++ b/src/KArtSell.Modules.ModelOperations/Scheduling/ModelOperationsDispatcherJob.cs @@ -15,6 +15,12 @@ public sealed class ModelOperationsDispatcherJob( ScheduleOccurrencePlanner occurrencePlanner, ILogger logger) { + private static readonly Action LogDispatchFailed = + LoggerMessage.Define( + LogLevel.Error, + new EventId(1, nameof(LogDispatchFailed)), + "Failed to dispatch model operation {OperationCode} for {ScopeKey}."); + [Queue("q-control")] [DisableConcurrentExecution(timeoutInSeconds: 840)] [AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)] @@ -47,7 +53,7 @@ public sealed class ModelOperationsDispatcherJob( } catch (Exception exception) { - logger.LogError(exception, "Failed to dispatch model operation {OperationCode} for {ScopeKey}.", item.OperationCode, item.ScopeKey); + LogDispatchFailed(logger, item.OperationCode, item.ScopeKey, exception); await schedules.ReleaseAsync( item.ScheduleId, leaseOwner, diff --git a/src/KArtSell.Modules.ModelOperations/Scheduling/ScheduledModelOperationJob.cs b/src/KArtSell.Modules.ModelOperations/Scheduling/ScheduledModelOperationJob.cs index 4e13a839..a86a653e 100644 --- a/src/KArtSell.Modules.ModelOperations/Scheduling/ScheduledModelOperationJob.cs +++ b/src/KArtSell.Modules.ModelOperations/Scheduling/ScheduledModelOperationJob.cs @@ -8,6 +8,18 @@ public sealed class ScheduledModelOperationJob( IModelOperationRequestService service, ILogger logger) { + private static readonly Action LogModelOperationNotCreated = + LoggerMessage.Define( + LogLevel.Warning, + new EventId(1, nameof(LogModelOperationNotCreated)), + "Model operation {OperationCode} for {ScopeKey} was not created because the approved frozen context is unavailable or the idempotency key already exists."); + + private static readonly Action LogModelOperationRequested = + LoggerMessage.Define( + LogLevel.Information, + new EventId(2, nameof(LogModelOperationRequested)), + "Requested model operation {OperationCode} for {ScopeKey} with model {ModelVersion} and dataset {DatasetId}. No model mutation is performed by this job."); + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Fail)] public async Task ExecuteAsync( Guid scheduleId, @@ -28,18 +40,16 @@ public sealed class ScheduledModelOperationJob( if (request is null) { - logger.LogWarning( - "Model operation {OperationCode} for {ScopeKey} was not created because the approved frozen context is unavailable or the idempotency key already exists.", - operationCode, - scopeKey); + LogModelOperationNotCreated(logger, operationCode, scopeKey, null); return; } - logger.LogInformation( - "Requested model operation {OperationCode} for {ScopeKey} with model {ModelVersion} and dataset {DatasetId}. No model mutation is performed by this job.", + LogModelOperationRequested( + logger, request.OperationCode, request.ScopeKey, request.Context.VersionSet.ModelVersion, - request.Context.VersionSet.DatasetId); + request.Context.VersionSet.DatasetId, + null); } }