Skip to content

Commit eb9167d

Browse files
Merge pull request #790 from TransactionProcessing/task/#777_add_retry_to_process_settlement
Refactor PolicyFactory and update SettlementDomainService
2 parents e2e4cc7 + a3b361d commit eb9167d

File tree

2 files changed

+164
-86
lines changed

2 files changed

+164
-86
lines changed

TransactionProcessor.BusinessLogic/Common/PolicyFactory.cs

Lines changed: 108 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,51 +14,129 @@
1414
namespace TransactionProcessor.BusinessLogic.Common;
1515

1616
[ExcludeFromCodeCoverage]
17-
public static class PolicyFactory{
18-
public static IAsyncPolicy<Result> CreatePolicy(Int32 retryCount=5, TimeSpan? retryDelay = null, String policyTag="", Boolean withFallBack=false) {
19-
20-
TimeSpan retryDelayValue = retryDelay.GetValueOrDefault(TimeSpan.FromSeconds(2));
17+
public static class PolicyFactory
18+
{
19+
private enum LogType
20+
{
21+
Retry,
22+
Final
23+
}
2124

22-
AsyncRetryPolicy<Result> retryPolicy = CreateRetryPolicy(retryCount, retryDelayValue, policyTag);
25+
public static IAsyncPolicy<Result> CreatePolicy(
26+
int retryCount = 5,
27+
TimeSpan? retryDelay = null,
28+
string policyTag = "",
29+
bool withFallBack = false)
30+
{
31+
TimeSpan delay = retryDelay.GetValueOrDefault(TimeSpan.FromSeconds(5));
32+
return CreateRetryPolicy(retryCount, delay, policyTag);
33+
}
2334

24-
return retryPolicy;
35+
public static IAsyncPolicy<Result<T>> CreatePolicy<T>(
36+
int retryCount = 5,
37+
TimeSpan? retryDelay = null,
38+
string policyTag = "",
39+
bool withFallBack = false)
40+
{
41+
TimeSpan delay = retryDelay.GetValueOrDefault(TimeSpan.FromSeconds(5));
42+
return CreateRetryPolicy<T>(retryCount, delay, policyTag);
2543
}
2644

27-
public static async Task<Result> ExecuteWithPolicyAsync(Func<Task<Result>> action, IAsyncPolicy<Result> policy, String policyTag = "")
45+
public static async Task<Result> ExecuteWithPolicyAsync(
46+
Func<Task<Result>> action,
47+
IAsyncPolicy<Result> policy,
48+
string policyTag = "")
2849
{
2950
var context = new Context();
30-
context["RetryCount"] = 0;
51+
Result result = await policy.ExecuteAsync(ctx => action(), context);
3152

32-
Result result = await policy.ExecuteAsync((ctx) => action(), context);
33-
34-
int retryCount = (int)context["RetryCount"];
35-
String message = result switch
36-
{
37-
{ IsSuccess: true } => "Success",
38-
{ IsSuccess: false, Message: not "" } => result.Message,
39-
{ IsSuccess: false, Message: "", Errors: var errors } when errors.Any() => string.Join(", ", errors),
40-
_ => "Unknown Error"
41-
};
42-
String retryMessage = retryCount > 0 ? $" after {retryCount} retries." : "";
43-
// Log success if no retries were required
53+
int retryCount = context.TryGetValue("RetryCount", out var retryObj) && retryObj is int r ? r : 0;
54+
LogResult(policyTag, result, retryCount, LogType.Final);
4455

45-
Logger.LogWarning($"{policyTag} - {message} {retryMessage}");
56+
return result;
57+
}
58+
59+
public static async Task<Result<T>> ExecuteWithPolicyAsync<T>(
60+
Func<Task<Result<T>>> action,
61+
IAsyncPolicy<Result<T>> policy,
62+
string policyTag = "")
63+
{
64+
var context = new Context();
65+
Result<T> result = await policy.ExecuteAsync(ctx => action(), context);
66+
67+
int retryCount = context.TryGetValue("RetryCount", out var retryObj) && retryObj is int r ? r : 0;
68+
LogResult(policyTag, result, retryCount, LogType.Final);
4669

4770
return result;
4871
}
4972

50-
private static AsyncRetryPolicy<Result> CreateRetryPolicy(int retryCount, TimeSpan retryDelay, String policyTag)
73+
private static AsyncRetryPolicy<Result> CreateRetryPolicy(
74+
int retryCount,
75+
TimeSpan retryDelay,
76+
string policyTag)
5177
{
5278
return Policy<Result>
53-
.HandleResult(result => !result.IsSuccess && String.Join("|",result.Errors).Contains("Append failed due to WrongExpectedVersion")) // Retry if the result is not successful
54-
.OrResult(result => !result.IsSuccess && String.Join("|", result.Errors).Contains("DeadlineExceeded")) // Retry if the result is not successful
55-
.WaitAndRetryAsync(retryCount,
56-
_ => retryDelay, // Fixed delay
57-
(result, timeSpan, retryCount, context) =>
79+
.HandleResult(ShouldRetry)
80+
.WaitAndRetryAsync(
81+
retryCount,
82+
_ => retryDelay,
83+
(result, timeSpan, attempt, context) =>
84+
{
85+
context["RetryCount"] = attempt;
86+
LogResult(policyTag, result.Result, attempt, LogType.Retry);
87+
});
88+
}
89+
90+
private static AsyncRetryPolicy<Result<T>> CreateRetryPolicy<T>(
91+
int retryCount,
92+
TimeSpan retryDelay,
93+
string policyTag)
94+
{
95+
return Policy<Result<T>>
96+
.HandleResult(ShouldRetry)
97+
.WaitAndRetryAsync(
98+
retryCount,
99+
_ => retryDelay,
100+
(result, timeSpan, attempt, context) =>
58101
{
59-
context["RetryCount"] = retryCount;
60-
Logger.LogWarning($"{policyTag} - Retry {retryCount} due to unsuccessful result {String.Join(".",result.Result.Errors)}. Waiting {timeSpan} before retrying...");
102+
context["RetryCount"] = attempt;
103+
LogResult(policyTag, result.Result, attempt, LogType.Retry);
61104
});
105+
}
106+
107+
private static bool ShouldRetry(ResultBase result)
108+
{
109+
return !result.IsSuccess && result.Errors.Any(e =>
110+
e.Contains("WrongExpectedVersion", StringComparison.OrdinalIgnoreCase) ||
111+
e.Contains("DeadlineExceeded", StringComparison.OrdinalIgnoreCase) ||
112+
e.Contains("Cancelled"));
113+
}
114+
115+
private static string FormatResultMessage(ResultBase result)
116+
{
117+
return result switch
118+
{
119+
{ IsSuccess: true } => "Success",
120+
{ IsSuccess: false, Message: not "" } => result.Message,
121+
{ IsSuccess: false, Errors: var errors } when errors?.Any() == true => string.Join(", ", errors),
122+
_ => "Unknown Error"
123+
};
124+
}
125+
126+
private static void LogResult(string policyTag, ResultBase result, int retryCount, LogType type)
127+
{
128+
string message = FormatResultMessage(result);
129+
130+
switch (type)
131+
{
132+
case LogType.Retry:
133+
Logger.LogWarning($"{policyTag} - Retry {retryCount} due to error: {message}. Waiting before retrying...");
134+
break;
62135

136+
case LogType.Final:
137+
string retryMessage = retryCount > 0 ? $" after {retryCount} retries." : "";
138+
Logger.LogWarning($"{policyTag} - {message}{retryMessage}");
139+
break;
140+
}
63141
}
64-
}
142+
}

TransactionProcessor.BusinessLogic/Services/SettlementDomainService.cs

Lines changed: 56 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,18 @@
77

88
namespace TransactionProcessor.BusinessLogic.Services
99
{
10-
using System;
11-
using System.Collections.Generic;
12-
using System.Linq;
13-
using System.Threading;
14-
using System.Threading.Tasks;
1510
using Common;
1611
using Models;
12+
using Polly;
1713
using Shared.DomainDrivenDesign.EventSourcing;
1814
using Shared.EventStore.Aggregate;
1915
using Shared.Exceptions;
2016
using Shared.Logger;
17+
using System;
18+
using System.Collections.Generic;
19+
using System.Linq;
20+
using System.Threading;
21+
using System.Threading.Tasks;
2122

2223
public interface ISettlementDomainService
2324
{
@@ -98,56 +99,53 @@ private async Task<Result> ApplyTransactionUpdates(Func<TransactionAggregate, Ta
9899
// TODO: Add in a Get Settlement
99100

100101
public async Task<Result<Guid>> ProcessSettlement(SettlementCommands.ProcessSettlementCommand command,
101-
CancellationToken cancellationToken)
102-
{
103-
Guid settlementAggregateId = Helpers.CalculateSettlementAggregateId(command.SettlementDate, command.MerchantId,command.EstateId);
104-
List<(Guid transactionId, Guid merchantId, CalculatedFee calculatedFee)> feesToBeSettled = new();
102+
CancellationToken cancellationToken) {
103+
IAsyncPolicy<Result<Guid>> retryPolicy = PolicyFactory.CreatePolicy<Guid>(policyTag: "SettlementDomainService - ProcessSettlement");
104+
105+
return await PolicyFactory.ExecuteWithPolicyAsync<Guid>(async () => {
106+
Guid settlementAggregateId = Helpers.CalculateSettlementAggregateId(command.SettlementDate, command.MerchantId, command.EstateId);
107+
List<(Guid transactionId, Guid merchantId, CalculatedFee calculatedFee)> feesToBeSettled = new();
108+
109+
Result settlementResult = await ApplySettlementUpdates(async (SettlementAggregate settlementAggregate) => {
110+
if (settlementAggregate.IsCreated == false) {
111+
Logger.LogInformation($"No pending settlement for {command.SettlementDate:yyyy-MM-dd}");
112+
// Not pending settlement for this date
113+
return Result.Success();
114+
}
115+
116+
Result<MerchantAggregate> getMerchantResult = await this.AggregateService.Get<MerchantAggregate>(command.MerchantId, cancellationToken);
117+
if (getMerchantResult.IsFailed)
118+
return ResultHelpers.CreateFailure(getMerchantResult);
119+
120+
MerchantAggregate merchant = getMerchantResult.Data;
121+
if (merchant.SettlementSchedule == SettlementSchedule.Immediate) {
122+
// Mark the settlement as completed
123+
settlementAggregate.StartProcessing(DateTime.Now);
124+
settlementAggregate.ManuallyComplete();
125+
Result result = await this.AggregateService.Save(settlementAggregate, cancellationToken);
126+
return result;
127+
}
128+
129+
feesToBeSettled = settlementAggregate.GetFeesToBeSettled();
130+
131+
if (feesToBeSettled.Any()) {
132+
// Record the process call
133+
settlementAggregate.StartProcessing(DateTime.Now);
134+
return await this.AggregateService.Save(settlementAggregate, cancellationToken);
135+
}
105136

106-
Result settlementResult = await ApplySettlementUpdates(async (SettlementAggregate settlementAggregate) => {
107-
if (settlementAggregate.IsCreated == false)
108-
{
109-
Logger.LogInformation($"No pending settlement for {command.SettlementDate:yyyy-MM-dd}");
110-
// Not pending settlement for this date
111137
return Result.Success();
112-
}
113138

114-
Result<MerchantAggregate> getMerchantResult = await this.AggregateService.Get<MerchantAggregate>(command.MerchantId, cancellationToken);
115-
if (getMerchantResult.IsFailed)
116-
return ResultHelpers.CreateFailure(getMerchantResult);
117-
118-
MerchantAggregate merchant = getMerchantResult.Data;
119-
if (merchant.SettlementSchedule == SettlementSchedule.Immediate)
120-
{
121-
// Mark the settlement as completed
122-
settlementAggregate.StartProcessing(DateTime.Now);
123-
settlementAggregate.ManuallyComplete();
124-
Result result = await this.AggregateService.Save(settlementAggregate, cancellationToken);
125-
return result;
126-
}
139+
}, settlementAggregateId, cancellationToken);
127140

128-
feesToBeSettled = settlementAggregate.GetFeesToBeSettled();
129-
130-
if (feesToBeSettled.Any())
131-
{
132-
// Record the process call
133-
settlementAggregate.StartProcessing(DateTime.Now);
134-
return await this.AggregateService.Save(settlementAggregate, cancellationToken);
135-
}
141+
if (settlementResult.IsFailed)
142+
return settlementResult;
136143

137-
return Result.Success();
138-
139-
}, settlementAggregateId, cancellationToken);
140-
141-
if (settlementResult.IsFailed)
142-
return settlementResult;
143-
144-
List<Result> failedResults = new();
145-
foreach ((Guid transactionId, Guid merchantId, CalculatedFee calculatedFee) feeToSettle in feesToBeSettled) {
146-
Result transactionResult = await ApplyTransactionUpdates(
147-
async (TransactionAggregate transactionAggregate) => {
144+
List<Result> failedResults = new();
145+
foreach ((Guid transactionId, Guid merchantId, CalculatedFee calculatedFee) feeToSettle in feesToBeSettled) {
146+
Result transactionResult = await ApplyTransactionUpdates(async (TransactionAggregate transactionAggregate) => {
148147
try {
149-
transactionAggregate.AddSettledFee(feeToSettle.calculatedFee, command.SettlementDate,
150-
settlementAggregateId);
148+
transactionAggregate.AddSettledFee(feeToSettle.calculatedFee, command.SettlementDate, settlementAggregateId);
151149
return Result.Success();
152150
}
153151
catch (Exception ex) {
@@ -156,15 +154,17 @@ public async Task<Result<Guid>> ProcessSettlement(SettlementCommands.ProcessSett
156154
}
157155
}, feeToSettle.transactionId, cancellationToken);
158156

159-
if (transactionResult.IsFailed) {
160-
failedResults.Add(transactionResult);
157+
if (transactionResult.IsFailed) {
158+
failedResults.Add(transactionResult);
159+
}
161160
}
162-
}
163161

164-
if (failedResults.Any()) {
165-
return Result.Failure($"Not all fees were processed successfully {failedResults.Count} have failed");
166-
}
167-
return Result.Success(settlementAggregateId);
162+
if (failedResults.Any()) {
163+
return Result.Failure($"Not all fees were processed successfully {failedResults.Count} have failed");
164+
}
165+
166+
return Result.Success(settlementAggregateId);
167+
}, retryPolicy, "SettlementDomainService - ProcessSettlement");
168168
}
169169

170170
public async Task<Result> AddMerchantFeePendingSettlement(SettlementCommands.AddMerchantFeePendingSettlementCommand command,

0 commit comments

Comments
 (0)