From a3b361d2a837782f5d2cf86b9016b1779f3fc769 Mon Sep 17 00:00:00 2001 From: Stuart Ferguson Date: Tue, 29 Apr 2025 15:28:14 +0100 Subject: [PATCH] Refactor PolicyFactory and update SettlementDomainService Refactored the PolicyFactory class for improved structure and functionality, enhancing retry policy creation and logging. Updated the ExecuteWithPolicyAsync methods for better readability and maintainability, introducing new methods for retry logic and logging. In SettlementDomainService, modified the ProcessSettlement method to utilize the new policy execution methods, improving error handling and resilience. Enhanced method structure and logging practices for better clarity and flow tracking. --- .../Common/PolicyFactory.cs | 138 ++++++++++++++---- .../Services/SettlementDomainService.cs | 112 +++++++------- 2 files changed, 164 insertions(+), 86 deletions(-) diff --git a/TransactionProcessor.BusinessLogic/Common/PolicyFactory.cs b/TransactionProcessor.BusinessLogic/Common/PolicyFactory.cs index b9f36322..a986c87a 100644 --- a/TransactionProcessor.BusinessLogic/Common/PolicyFactory.cs +++ b/TransactionProcessor.BusinessLogic/Common/PolicyFactory.cs @@ -14,51 +14,129 @@ namespace TransactionProcessor.BusinessLogic.Common; [ExcludeFromCodeCoverage] -public static class PolicyFactory{ - public static IAsyncPolicy CreatePolicy(Int32 retryCount=5, TimeSpan? retryDelay = null, String policyTag="", Boolean withFallBack=false) { - - TimeSpan retryDelayValue = retryDelay.GetValueOrDefault(TimeSpan.FromSeconds(2)); +public static class PolicyFactory +{ + private enum LogType + { + Retry, + Final + } - AsyncRetryPolicy retryPolicy = CreateRetryPolicy(retryCount, retryDelayValue, policyTag); + public static IAsyncPolicy CreatePolicy( + int retryCount = 5, + TimeSpan? retryDelay = null, + string policyTag = "", + bool withFallBack = false) + { + TimeSpan delay = retryDelay.GetValueOrDefault(TimeSpan.FromSeconds(5)); + return CreateRetryPolicy(retryCount, delay, policyTag); + } - return retryPolicy; + public static IAsyncPolicy> CreatePolicy( + int retryCount = 5, + TimeSpan? retryDelay = null, + string policyTag = "", + bool withFallBack = false) + { + TimeSpan delay = retryDelay.GetValueOrDefault(TimeSpan.FromSeconds(5)); + return CreateRetryPolicy(retryCount, delay, policyTag); } - public static async Task ExecuteWithPolicyAsync(Func> action, IAsyncPolicy policy, String policyTag = "") + public static async Task ExecuteWithPolicyAsync( + Func> action, + IAsyncPolicy policy, + string policyTag = "") { var context = new Context(); - context["RetryCount"] = 0; + Result result = await policy.ExecuteAsync(ctx => action(), context); - Result result = await policy.ExecuteAsync((ctx) => action(), context); - - int retryCount = (int)context["RetryCount"]; - String message = result switch - { - { IsSuccess: true } => "Success", - { IsSuccess: false, Message: not "" } => result.Message, - { IsSuccess: false, Message: "", Errors: var errors } when errors.Any() => string.Join(", ", errors), - _ => "Unknown Error" - }; - String retryMessage = retryCount > 0 ? $" after {retryCount} retries." : ""; - // Log success if no retries were required + int retryCount = context.TryGetValue("RetryCount", out var retryObj) && retryObj is int r ? r : 0; + LogResult(policyTag, result, retryCount, LogType.Final); - Logger.LogWarning($"{policyTag} - {message} {retryMessage}"); + return result; + } + + public static async Task> ExecuteWithPolicyAsync( + Func>> action, + IAsyncPolicy> policy, + string policyTag = "") + { + var context = new Context(); + Result result = await policy.ExecuteAsync(ctx => action(), context); + + int retryCount = context.TryGetValue("RetryCount", out var retryObj) && retryObj is int r ? r : 0; + LogResult(policyTag, result, retryCount, LogType.Final); return result; } - private static AsyncRetryPolicy CreateRetryPolicy(int retryCount, TimeSpan retryDelay, String policyTag) + private static AsyncRetryPolicy CreateRetryPolicy( + int retryCount, + TimeSpan retryDelay, + string policyTag) { return Policy - .HandleResult(result => !result.IsSuccess && String.Join("|",result.Errors).Contains("Append failed due to WrongExpectedVersion")) // Retry if the result is not successful - .OrResult(result => !result.IsSuccess && String.Join("|", result.Errors).Contains("DeadlineExceeded")) // Retry if the result is not successful - .WaitAndRetryAsync(retryCount, - _ => retryDelay, // Fixed delay - (result, timeSpan, retryCount, context) => + .HandleResult(ShouldRetry) + .WaitAndRetryAsync( + retryCount, + _ => retryDelay, + (result, timeSpan, attempt, context) => + { + context["RetryCount"] = attempt; + LogResult(policyTag, result.Result, attempt, LogType.Retry); + }); + } + + private static AsyncRetryPolicy> CreateRetryPolicy( + int retryCount, + TimeSpan retryDelay, + string policyTag) + { + return Policy> + .HandleResult(ShouldRetry) + .WaitAndRetryAsync( + retryCount, + _ => retryDelay, + (result, timeSpan, attempt, context) => { - context["RetryCount"] = retryCount; - Logger.LogWarning($"{policyTag} - Retry {retryCount} due to unsuccessful result {String.Join(".",result.Result.Errors)}. Waiting {timeSpan} before retrying..."); + context["RetryCount"] = attempt; + LogResult(policyTag, result.Result, attempt, LogType.Retry); }); + } + + private static bool ShouldRetry(ResultBase result) + { + return !result.IsSuccess && result.Errors.Any(e => + e.Contains("WrongExpectedVersion", StringComparison.OrdinalIgnoreCase) || + e.Contains("DeadlineExceeded", StringComparison.OrdinalIgnoreCase) || + e.Contains("Cancelled")); + } + + private static string FormatResultMessage(ResultBase result) + { + return result switch + { + { IsSuccess: true } => "Success", + { IsSuccess: false, Message: not "" } => result.Message, + { IsSuccess: false, Errors: var errors } when errors?.Any() == true => string.Join(", ", errors), + _ => "Unknown Error" + }; + } + + private static void LogResult(string policyTag, ResultBase result, int retryCount, LogType type) + { + string message = FormatResultMessage(result); + + switch (type) + { + case LogType.Retry: + Logger.LogWarning($"{policyTag} - Retry {retryCount} due to error: {message}. Waiting before retrying..."); + break; + case LogType.Final: + string retryMessage = retryCount > 0 ? $" after {retryCount} retries." : ""; + Logger.LogWarning($"{policyTag} - {message}{retryMessage}"); + break; + } } -} \ No newline at end of file +} diff --git a/TransactionProcessor.BusinessLogic/Services/SettlementDomainService.cs b/TransactionProcessor.BusinessLogic/Services/SettlementDomainService.cs index 390c03b7..3c260189 100644 --- a/TransactionProcessor.BusinessLogic/Services/SettlementDomainService.cs +++ b/TransactionProcessor.BusinessLogic/Services/SettlementDomainService.cs @@ -7,17 +7,18 @@ namespace TransactionProcessor.BusinessLogic.Services { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; using Common; using Models; + using Polly; using Shared.DomainDrivenDesign.EventSourcing; using Shared.EventStore.Aggregate; using Shared.Exceptions; using Shared.Logger; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; public interface ISettlementDomainService { @@ -98,56 +99,53 @@ private async Task ApplyTransactionUpdates(Func> ProcessSettlement(SettlementCommands.ProcessSettlementCommand command, - CancellationToken cancellationToken) - { - Guid settlementAggregateId = Helpers.CalculateSettlementAggregateId(command.SettlementDate, command.MerchantId,command.EstateId); - List<(Guid transactionId, Guid merchantId, CalculatedFee calculatedFee)> feesToBeSettled = new(); + CancellationToken cancellationToken) { + IAsyncPolicy> retryPolicy = PolicyFactory.CreatePolicy(policyTag: "SettlementDomainService - ProcessSettlement"); + + return await PolicyFactory.ExecuteWithPolicyAsync(async () => { + Guid settlementAggregateId = Helpers.CalculateSettlementAggregateId(command.SettlementDate, command.MerchantId, command.EstateId); + List<(Guid transactionId, Guid merchantId, CalculatedFee calculatedFee)> feesToBeSettled = new(); + + Result settlementResult = await ApplySettlementUpdates(async (SettlementAggregate settlementAggregate) => { + if (settlementAggregate.IsCreated == false) { + Logger.LogInformation($"No pending settlement for {command.SettlementDate:yyyy-MM-dd}"); + // Not pending settlement for this date + return Result.Success(); + } + + Result getMerchantResult = await this.AggregateService.Get(command.MerchantId, cancellationToken); + if (getMerchantResult.IsFailed) + return ResultHelpers.CreateFailure(getMerchantResult); + + MerchantAggregate merchant = getMerchantResult.Data; + if (merchant.SettlementSchedule == SettlementSchedule.Immediate) { + // Mark the settlement as completed + settlementAggregate.StartProcessing(DateTime.Now); + settlementAggregate.ManuallyComplete(); + Result result = await this.AggregateService.Save(settlementAggregate, cancellationToken); + return result; + } + + feesToBeSettled = settlementAggregate.GetFeesToBeSettled(); + + if (feesToBeSettled.Any()) { + // Record the process call + settlementAggregate.StartProcessing(DateTime.Now); + return await this.AggregateService.Save(settlementAggregate, cancellationToken); + } - Result settlementResult = await ApplySettlementUpdates(async (SettlementAggregate settlementAggregate) => { - if (settlementAggregate.IsCreated == false) - { - Logger.LogInformation($"No pending settlement for {command.SettlementDate:yyyy-MM-dd}"); - // Not pending settlement for this date return Result.Success(); - } - Result getMerchantResult = await this.AggregateService.Get(command.MerchantId, cancellationToken); - if (getMerchantResult.IsFailed) - return ResultHelpers.CreateFailure(getMerchantResult); - - MerchantAggregate merchant = getMerchantResult.Data; - if (merchant.SettlementSchedule == SettlementSchedule.Immediate) - { - // Mark the settlement as completed - settlementAggregate.StartProcessing(DateTime.Now); - settlementAggregate.ManuallyComplete(); - Result result = await this.AggregateService.Save(settlementAggregate, cancellationToken); - return result; - } + }, settlementAggregateId, cancellationToken); - feesToBeSettled = settlementAggregate.GetFeesToBeSettled(); - - if (feesToBeSettled.Any()) - { - // Record the process call - settlementAggregate.StartProcessing(DateTime.Now); - return await this.AggregateService.Save(settlementAggregate, cancellationToken); - } + if (settlementResult.IsFailed) + return settlementResult; - return Result.Success(); - - }, settlementAggregateId, cancellationToken); - - if (settlementResult.IsFailed) - return settlementResult; - - List failedResults = new(); - foreach ((Guid transactionId, Guid merchantId, CalculatedFee calculatedFee) feeToSettle in feesToBeSettled) { - Result transactionResult = await ApplyTransactionUpdates( - async (TransactionAggregate transactionAggregate) => { + List failedResults = new(); + foreach ((Guid transactionId, Guid merchantId, CalculatedFee calculatedFee) feeToSettle in feesToBeSettled) { + Result transactionResult = await ApplyTransactionUpdates(async (TransactionAggregate transactionAggregate) => { try { - transactionAggregate.AddSettledFee(feeToSettle.calculatedFee, command.SettlementDate, - settlementAggregateId); + transactionAggregate.AddSettledFee(feeToSettle.calculatedFee, command.SettlementDate, settlementAggregateId); return Result.Success(); } catch (Exception ex) { @@ -156,15 +154,17 @@ public async Task> ProcessSettlement(SettlementCommands.ProcessSett } }, feeToSettle.transactionId, cancellationToken); - if (transactionResult.IsFailed) { - failedResults.Add(transactionResult); + if (transactionResult.IsFailed) { + failedResults.Add(transactionResult); + } } - } - if (failedResults.Any()) { - return Result.Failure($"Not all fees were processed successfully {failedResults.Count} have failed"); - } - return Result.Success(settlementAggregateId); + if (failedResults.Any()) { + return Result.Failure($"Not all fees were processed successfully {failedResults.Count} have failed"); + } + + return Result.Success(settlementAggregateId); + }, retryPolicy, "SettlementDomainService - ProcessSettlement"); } public async Task AddMerchantFeePendingSettlement(SettlementCommands.AddMerchantFeePendingSettlementCommand command,