@@ -28,11 +28,21 @@ public class BatchingService : IDisposable
2828 TimeSpan . FromSeconds ( 15 )
2929 } ;
3030
31+ // Circuit breaker
32+ private readonly CircuitBreaker _circuitBreaker ;
33+
34+ // Rate limiting
35+ private readonly int _maxBatchesPerMinute ;
36+ private readonly Queue < DateTime > _batchSendTimestamps = new ( ) ;
37+ private readonly object _rateLimitLock = new ( ) ;
38+
3139 // Metrics for monitoring
3240 private long _totalItemsProcessed ;
3341 private long _totalBatchesSent ;
3442 private long _totalFailures ;
3543 private long _totalValidationFailures ;
44+ private long _totalRateLimitDelays ;
45+ private long _totalDropped ;
3646 private DateTime _lastSuccessfulSend = DateTime . UtcNow ;
3747
3848 // Debounce configuration
@@ -53,6 +63,11 @@ public BatchingService(
5363 _options = batchingOptions ? . Value ?? throw new ArgumentNullException ( nameof ( batchingOptions ) ) ;
5464 _settingsService = settingsService ?? throw new ArgumentNullException ( nameof ( settingsService ) ) ;
5565
66+ _maxBatchesPerMinute = _options . MaxBatchesPerMinute ;
67+ _circuitBreaker = new CircuitBreaker ( logger ,
68+ failureThreshold : _options . CircuitBreakerFailureThreshold ,
69+ cooldownPeriod : _options . CircuitBreakerCooldown ) ;
70+
5671 ValidateOptions ( ) ;
5772
5873 // Configure debounce delay from options if available, otherwise use defaults
@@ -62,8 +77,8 @@ public BatchingService(
6277 _maxWaitTime = _options . BatchInterval ;
6378 }
6479
65- _logger . LogInformation ( "BatchingService initialized with debounce: {DebounceDelay}ms, max wait: {MaxWait}s, max size: {MaxSize}, timeout: {Timeout}" ,
66- _debounceDelay . TotalMilliseconds , _maxWaitTime . TotalSeconds , _options . MaxBatchSize , _options . SendTimeout ) ;
80+ _logger . LogInformation ( "BatchingService initialized with debounce: {DebounceDelay}ms, max wait: {MaxWait}s, max size: {MaxSize}, timeout: {Timeout}, rate limit: {RateLimit}/min, max queue: {MaxQueue}, max retry queue: {MaxRetryQueue} " ,
81+ _debounceDelay . TotalMilliseconds , _maxWaitTime . TotalSeconds , _options . MaxBatchSize , _options . SendTimeout , _maxBatchesPerMinute , _options . MaxQueueSize , _options . MaxRetryQueueSize ) ;
6782 }
6883
6984 private void ValidateOptions ( )
@@ -92,6 +107,18 @@ public void AddData(LogLine data)
92107 return ;
93108 }
94109
110+ // Enforce queue size limit - drop oldest items if at capacity
111+ if ( _options . MaxQueueSize > 0 && _batchData . Count >= _options . MaxQueueSize )
112+ {
113+ var dropped = 0 ;
114+ while ( _batchData . Count >= _options . MaxQueueSize && _batchData . TryDequeue ( out _ ) )
115+ {
116+ dropped ++ ;
117+ }
118+ Interlocked . Add ( ref _totalDropped , dropped ) ;
119+ _logger . LogWarning ( "Queue at capacity ({MaxSize}), dropped {Dropped} oldest items" , _options . MaxQueueSize , dropped ) ;
120+ }
121+
95122 _batchData . Enqueue ( data ) ;
96123
97124 // Track when first log was added if this is the first in a batch
@@ -232,11 +259,48 @@ private async Task ProcessRetryQueueAsync()
232259 }
233260 }
234261
262+ private bool IsRateLimited ( )
263+ {
264+ lock ( _rateLimitLock )
265+ {
266+ var now = DateTime . UtcNow ;
267+ var windowStart = now . AddMinutes ( - 1 ) ;
268+
269+ // Remove timestamps outside the sliding window
270+ while ( _batchSendTimestamps . Count > 0 && _batchSendTimestamps . Peek ( ) < windowStart )
271+ {
272+ _batchSendTimestamps . Dequeue ( ) ;
273+ }
274+
275+ if ( _batchSendTimestamps . Count >= _maxBatchesPerMinute )
276+ {
277+ return true ;
278+ }
279+
280+ _batchSendTimestamps . Enqueue ( now ) ;
281+ return false ;
282+ }
283+ }
284+
235285 private async Task ProcessNewBatchAsync ( )
236286 {
237287 if ( _batchData . IsEmpty )
238288 return ;
239289
290+ // Check rate limit before extracting the batch
291+ if ( IsRateLimited ( ) )
292+ {
293+ Interlocked . Increment ( ref _totalRateLimitDelays ) ;
294+ _logger . LogWarning ( "Rate limit reached ({MaxBatchesPerMinute}/min), delaying batch send" , _maxBatchesPerMinute ) ;
295+ // Re-trigger after a short delay instead of dropping
296+ _ = Task . Run ( async ( ) =>
297+ {
298+ await Task . Delay ( TimeSpan . FromSeconds ( 2 ) , _cancellationTokenSource . Token ) . ConfigureAwait ( false ) ;
299+ ResetDebounceTimer ( ) ;
300+ } , _cancellationTokenSource . Token ) ;
301+ return ;
302+ }
303+
240304 var currentBatch = ExtractBatch ( ) ;
241305 if ( currentBatch . Count == 0 )
242306 return ;
@@ -259,9 +323,22 @@ private async Task ProcessNewBatchAsync()
259323
260324 if ( ! success )
261325 {
262- // Add to retry queue
326+ // Add to retry queue with size enforcement
263327 lock ( _retryLock )
264328 {
329+ if ( _options . MaxRetryQueueSize > 0 )
330+ {
331+ var retryItemCount = _retryQueue . Sum ( r => r . Data . Count ) ;
332+ if ( retryItemCount + currentBatch . Count > _options . MaxRetryQueueSize )
333+ {
334+ var dropped = currentBatch . Count ;
335+ Interlocked . Add ( ref _totalDropped , dropped ) ;
336+ _logger . LogWarning ( "Retry queue at capacity ({MaxSize} items), dropping batch of {Count} logs" ,
337+ _options . MaxRetryQueueSize , dropped ) ;
338+ return ;
339+ }
340+ }
341+
265342 var retryItem = new BatchItem
266343 {
267344 Data = currentBatch ,
@@ -329,6 +406,13 @@ private async Task<bool> SendBatchWithRetryAsync(List<LogLine> batch, int attemp
329406 if ( batch . Count == 0 )
330407 return true ;
331408
409+ // Check circuit breaker before attempting to send
410+ if ( ! _circuitBreaker . AllowRequest ( ) )
411+ {
412+ _logger . LogDebug ( "Circuit breaker is open, skipping batch send of {Count} items" , batch . Count ) ;
413+ return false ;
414+ }
415+
332416 try
333417 {
334418 _logger . LogDebug ( "Sending batch of {Count} log lines (attempt {Attempt})" , batch . Count , attemptNumber ) ;
@@ -341,6 +425,7 @@ private async Task<bool> SendBatchWithRetryAsync(List<LogLine> batch, int attemp
341425
342426 if ( response . IsSuccessStatusCode )
343427 {
428+ _circuitBreaker . RecordSuccess ( ) ;
344429 Interlocked . Add ( ref _totalItemsProcessed , batch . Count ) ;
345430 Interlocked . Increment ( ref _totalBatchesSent ) ;
346431 _lastSuccessfulSend = DateTime . UtcNow ;
@@ -350,13 +435,15 @@ private async Task<bool> SendBatchWithRetryAsync(List<LogLine> batch, int attemp
350435 }
351436 else
352437 {
438+ _circuitBreaker . RecordFailure ( ) ;
353439 _logger . LogWarning ( "HTTP error sending batch: {StatusCode} {ReasonPhrase}" ,
354440 response . StatusCode , response . ReasonPhrase ) ;
355441 return false ;
356442 }
357443 }
358444 catch ( TaskCanceledException ex )
359445 {
446+ _circuitBreaker . RecordFailure ( ) ;
360447 _logger . LogWarning ( ex , "Request cancelled sending batch (attempt {Attempt})" , attemptNumber ) ;
361448 return false ;
362449 }
@@ -367,17 +454,19 @@ private async Task<bool> SendBatchWithRetryAsync(List<LogLine> batch, int attemp
367454 }
368455 catch ( OperationCanceledException )
369456 {
457+ _circuitBreaker . RecordFailure ( ) ;
370458 _logger . LogWarning ( "Batch send timed out after {Timeout}" , _options . SendTimeout ) ;
371459 return false ;
372460 }
373461 catch ( HttpRequestException ex )
374462 {
463+ _circuitBreaker . RecordFailure ( ) ;
375464 _logger . LogWarning ( ex , "Network error sending batch (attempt {Attempt})" , attemptNumber ) ;
376465 return false ;
377466 }
378-
379467 catch ( Exception ex )
380468 {
469+ _circuitBreaker . RecordFailure ( ) ;
381470 _logger . LogError ( ex , "Unexpected error sending batch (attempt {Attempt})" , attemptNumber ) ;
382471 return false ;
383472 }
@@ -432,8 +521,12 @@ public BatchingServiceStats GetStats()
432521 TotalBatchesSent = _totalBatchesSent ,
433522 TotalFailures = _totalFailures ,
434523 TotalValidationFailures = _totalValidationFailures ,
524+ TotalRateLimitDelays = _totalRateLimitDelays ,
525+ TotalDropped = _totalDropped ,
435526 PendingRetries = _retryQueue . Count ,
436- LastSuccessfulSend = _lastSuccessfulSend
527+ LastSuccessfulSend = _lastSuccessfulSend ,
528+ CircuitBreakerState = _circuitBreaker . State . ToString ( ) ,
529+ CircuitBreakerFailures = _circuitBreaker . ConsecutiveFailures
437530 } ;
438531 }
439532 }
@@ -484,6 +577,11 @@ public class BatchingOptions
484577 public TimeSpan BatchInterval { get ; set ; } = TimeSpan . FromSeconds ( 2 ) ; // Now acts as max wait time
485578 public int MaxBatchSize { get ; set ; } = 100 ;
486579 public TimeSpan SendTimeout { get ; set ; } = TimeSpan . FromSeconds ( 30 ) ;
580+ public int MaxBatchesPerMinute { get ; set ; } = 60 ;
581+ public int MaxQueueSize { get ; set ; } = 10000 ;
582+ public int MaxRetryQueueSize { get ; set ; } = 5000 ;
583+ public int CircuitBreakerFailureThreshold { get ; set ; } = 5 ;
584+ public TimeSpan CircuitBreakerCooldown { get ; set ; } = TimeSpan . FromSeconds ( 30 ) ;
487585}
488586
489587public class BatchingServiceStats
@@ -493,8 +591,12 @@ public class BatchingServiceStats
493591 public long TotalBatchesSent { get ; set ; }
494592 public long TotalFailures { get ; set ; }
495593 public long TotalValidationFailures { get ; set ; }
594+ public long TotalRateLimitDelays { get ; set ; }
595+ public long TotalDropped { get ; set ; }
496596 public int PendingRetries { get ; set ; }
497597 public DateTime LastSuccessfulSend { get ; set ; }
598+ public string CircuitBreakerState { get ; set ; } = string . Empty ;
599+ public int CircuitBreakerFailures { get ; set ; }
498600}
499601
500602public class ApiSettings
0 commit comments