1+ using Microsoft . Extensions . Logging ;
2+ using MongoDB . Driver ;
3+ using Polly ;
4+ using Polly . CircuitBreaker ;
5+ using Polly . Retry ;
6+ using Polly . Timeout ;
7+
8+ namespace KeeperData . Core . Database . Resilience ;
9+
10+ /// <summary>
11+ /// Factory for creating Polly resilience pipelines for MongoDB operations.
12+ /// Handles retry logic, circuit breaker, and timeout policies for transient failures.
13+ /// </summary>
14+ public static class MongoResiliencePipelineFactory
15+ {
16+ /// <summary>
17+ /// Creates a resilience pipeline for MongoDB operations with retry, circuit breaker, and timeout.
18+ /// </summary>
19+ /// <typeparam name="T">The result type of the MongoDB operation</typeparam>
20+ /// <param name="config">Resilience configuration settings</param>
21+ /// <param name="logger">Logger for diagnostics</param>
22+ /// <param name="operationName">Name of the operation for logging context</param>
23+ /// <returns>Configured resilience pipeline</returns>
24+ public static ResiliencePipeline < T > Create < T > (
25+ Configuration . MongoResilienceConfig config ,
26+ ILogger logger ,
27+ string operationName )
28+ {
29+ var pipelineBuilder = new ResiliencePipelineBuilder < T > ( ) ;
30+
31+ // Add retry strategy for transient failures
32+ pipelineBuilder . AddRetry ( new RetryStrategyOptions < T >
33+ {
34+ MaxRetryAttempts = config . MaxRetryAttempts ,
35+ Delay = TimeSpan . FromMilliseconds ( config . InitialDelayMs ) ,
36+ BackoffType = DelayBackoffType . Exponential ,
37+ UseJitter = config . UseJitter ,
38+ ShouldHandle = new PredicateBuilder < T > ( )
39+ . Handle < MongoConnectionException > ( )
40+ . Handle < MongoExecutionTimeoutException > ( )
41+ . Handle < TimeoutException > ( )
42+ . Handle < MongoException > ( ex =>
43+ // Retry on network-related errors
44+ ex . Message . Contains ( "network" , StringComparison . OrdinalIgnoreCase ) ||
45+ ex . Message . Contains ( "connection pool" , StringComparison . OrdinalIgnoreCase ) ||
46+ ex . InnerException is System . Net . Sockets . SocketException ) ,
47+ OnRetry = args =>
48+ {
49+ // Log at Debug level to avoid log spam, Warning only on final failure
50+ var logLevel = args . AttemptNumber < config . MaxRetryAttempts ? LogLevel . Debug : LogLevel . Warning ;
51+
52+ if ( logLevel == LogLevel . Debug )
53+ {
54+ logger . LogDebug (
55+ args . Outcome . Exception ,
56+ "[MongoDB Resilience] {OperationName} failed (attempt {AttemptNumber}/{MaxRetryAttempts}). Retrying after {RetryDelay}ms" ,
57+ operationName ,
58+ args . AttemptNumber ,
59+ config . MaxRetryAttempts ,
60+ args . RetryDelay . TotalMilliseconds ) ;
61+ }
62+ else
63+ {
64+ logger . LogWarning (
65+ args . Outcome . Exception ,
66+ "[MongoDB Resilience] {OperationName} failed (attempt {AttemptNumber}/{MaxRetryAttempts}). Retrying after {RetryDelay}ms" ,
67+ operationName ,
68+ args . AttemptNumber ,
69+ config . MaxRetryAttempts ,
70+ args . RetryDelay . TotalMilliseconds ) ;
71+ }
72+
73+ return ValueTask . CompletedTask ;
74+ }
75+ } ) ;
76+
77+ // Add circuit breaker if enabled
78+ if ( config . EnableCircuitBreaker )
79+ {
80+ pipelineBuilder . AddCircuitBreaker ( new CircuitBreakerStrategyOptions < T >
81+ {
82+ FailureRatio = config . CircuitBreakerFailureRatio ,
83+ MinimumThroughput = config . CircuitBreakerMinimumThroughput ,
84+ SamplingDuration = TimeSpan . FromSeconds ( config . CircuitBreakerBreakDurationSeconds ) ,
85+ BreakDuration = TimeSpan . FromSeconds ( config . CircuitBreakerBreakDurationSeconds ) ,
86+ ShouldHandle = new PredicateBuilder < T > ( )
87+ . Handle < MongoConnectionException > ( )
88+ . Handle < MongoExecutionTimeoutException > ( )
89+ . Handle < TimeoutException > ( ) ,
90+ OnOpened = args =>
91+ {
92+ logger . LogError (
93+ "[MongoDB Resilience] Circuit breaker OPENED for {OperationName}. " +
94+ "Too many failures detected. Breaking for {BreakDuration}s" ,
95+ operationName ,
96+ config . CircuitBreakerBreakDurationSeconds ) ;
97+ return ValueTask . CompletedTask ;
98+ } ,
99+ OnClosed = args =>
100+ {
101+ logger . LogInformation (
102+ "[MongoDB Resilience] Circuit breaker CLOSED for {OperationName}. Resuming normal operations" ,
103+ operationName ) ;
104+ return ValueTask . CompletedTask ;
105+ } ,
106+ OnHalfOpened = args =>
107+ {
108+ logger . LogInformation (
109+ "[MongoDB Resilience] Circuit breaker HALF-OPEN for {OperationName}. Testing if service recovered" ,
110+ operationName ) ;
111+ return ValueTask . CompletedTask ;
112+ }
113+ } ) ;
114+ }
115+
116+ // Add timeout as the outer layer
117+ pipelineBuilder . AddTimeout ( TimeSpan . FromSeconds ( config . TimeoutSeconds ) ) ;
118+
119+ return pipelineBuilder . Build ( ) ;
120+ }
121+
122+ /// <summary>
123+ /// Creates a resilience pipeline for void/async Task MongoDB operations.
124+ /// </summary>
125+ public static ResiliencePipeline CreateForVoid (
126+ Configuration . MongoResilienceConfig config ,
127+ ILogger logger ,
128+ string operationName )
129+ {
130+ var pipelineBuilder = new ResiliencePipelineBuilder ( ) ;
131+
132+ // Add retry strategy
133+ pipelineBuilder . AddRetry ( new RetryStrategyOptions
134+ {
135+ MaxRetryAttempts = config . MaxRetryAttempts ,
136+ Delay = TimeSpan . FromMilliseconds ( config . InitialDelayMs ) ,
137+ BackoffType = DelayBackoffType . Exponential ,
138+ UseJitter = config . UseJitter ,
139+ ShouldHandle = new PredicateBuilder ( )
140+ . Handle < MongoConnectionException > ( )
141+ . Handle < MongoExecutionTimeoutException > ( )
142+ . Handle < TimeoutException > ( )
143+ . Handle < MongoException > ( ex =>
144+ ex . Message . Contains ( "network" , StringComparison . OrdinalIgnoreCase ) ||
145+ ex . Message . Contains ( "connection pool" , StringComparison . OrdinalIgnoreCase ) ||
146+ ex . InnerException is System . Net . Sockets . SocketException ) ,
147+ OnRetry = args =>
148+ {
149+ var logLevel = args . AttemptNumber < config . MaxRetryAttempts ? LogLevel . Debug : LogLevel . Warning ;
150+
151+ if ( logLevel == LogLevel . Debug )
152+ {
153+ logger . LogDebug (
154+ args . Outcome . Exception ,
155+ "[MongoDB Resilience] {OperationName} failed (attempt {AttemptNumber}/{MaxRetryAttempts}). Retrying after {RetryDelay}ms" ,
156+ operationName ,
157+ args . AttemptNumber ,
158+ config . MaxRetryAttempts ,
159+ args . RetryDelay . TotalMilliseconds ) ;
160+ }
161+ else
162+ {
163+ logger . LogWarning (
164+ args . Outcome . Exception ,
165+ "[MongoDB Resilience] {OperationName} failed (attempt {AttemptNumber}/{MaxRetryAttempts}). Retrying after {RetryDelay}ms" ,
166+ operationName ,
167+ args . AttemptNumber ,
168+ config . MaxRetryAttempts ,
169+ args . RetryDelay . TotalMilliseconds ) ;
170+ }
171+
172+ return ValueTask . CompletedTask ;
173+ }
174+ } ) ;
175+
176+ // Add circuit breaker if enabled
177+ if ( config . EnableCircuitBreaker )
178+ {
179+ pipelineBuilder . AddCircuitBreaker ( new CircuitBreakerStrategyOptions
180+ {
181+ FailureRatio = config . CircuitBreakerFailureRatio ,
182+ MinimumThroughput = config . CircuitBreakerMinimumThroughput ,
183+ SamplingDuration = TimeSpan . FromSeconds ( config . CircuitBreakerBreakDurationSeconds ) ,
184+ BreakDuration = TimeSpan . FromSeconds ( config . CircuitBreakerBreakDurationSeconds ) ,
185+ ShouldHandle = new PredicateBuilder ( )
186+ . Handle < MongoConnectionException > ( )
187+ . Handle < MongoExecutionTimeoutException > ( )
188+ . Handle < TimeoutException > ( ) ,
189+ OnOpened = args =>
190+ {
191+ logger . LogError (
192+ "[MongoDB Resilience] Circuit breaker OPENED for {OperationName}. Breaking for {BreakDuration}s" ,
193+ operationName ,
194+ config . CircuitBreakerBreakDurationSeconds ) ;
195+ return ValueTask . CompletedTask ;
196+ } ,
197+ OnClosed = args =>
198+ {
199+ logger . LogInformation (
200+ "[MongoDB Resilience] Circuit breaker CLOSED for {OperationName}" ,
201+ operationName ) ;
202+ return ValueTask . CompletedTask ;
203+ } ,
204+ OnHalfOpened = args =>
205+ {
206+ logger . LogInformation (
207+ "[MongoDB Resilience] Circuit breaker HALF-OPEN for {OperationName}" ,
208+ operationName ) ;
209+ return ValueTask . CompletedTask ;
210+ }
211+ } ) ;
212+ }
213+
214+ pipelineBuilder . AddTimeout ( TimeSpan . FromSeconds ( config . TimeoutSeconds ) ) ;
215+
216+ return pipelineBuilder . Build ( ) ;
217+ }
218+ }
0 commit comments