11using System ;
22using System . Collections . Generic ;
3- using System . Linq ;
43using System . Text . Json ;
54using System . Threading ;
65using System . Threading . Channels ;
@@ -34,6 +33,7 @@ public sealed class KafkaMessageConsumer<TData> : IMessageConsumer<TData>, IDisp
3433 private readonly IMetricFamily < ISummary > ? _consumerLagSummary ;
3534 private readonly ILogger < KafkaMessageConsumer < TData > > _logger ;
3635 private readonly IHostApplicationLifetime _applicationLifetime ;
36+ private readonly AsyncPolicy < ProcessedMessageStatus > _retryPolicy ;
3737 private IConsumer < string ? , byte [ ] > ? _consumer ;
3838 private readonly CancellationTokenSource _internalCts = new ( ) ;
3939
@@ -69,6 +69,13 @@ CloudEventFormatter cloudEventFormatter
6969 _options . MaxConcurrentMessages
7070 ) ;
7171 _timer = new Timer ( HandleCommitTimer ) ;
72+
73+ _retryPolicy = Policy
74+ . HandleResult < ProcessedMessageStatus > ( status => status == ProcessedMessageStatus . TemporaryFailure )
75+ . WaitAndRetryAsync (
76+ _options . RetriesOnTemporaryFailure ,
77+ retryAttempt => _options . RetryBasePeriod * Math . Pow ( 2 , retryAttempt )
78+ ) ;
7279 }
7380
7481 public Func <
@@ -160,36 +167,41 @@ private void WriteLog(LogMessage logMessage)
160167 case SyslogLevel . Alert :
161168 case SyslogLevel . Critical :
162169 _logger . LogCritical (
163- $ "{ logMessage . Message } -(Facility: {{facility}}, Name: {{name}})",
170+ "{Message} -(Facility: {Facility}, Name: {Name})" ,
171+ logMessage . Message ,
164172 logMessage . Facility ,
165173 logMessage . Name
166174 ) ;
167175 break ;
168176 case SyslogLevel . Error :
169177 _logger . LogError (
170- $ "{ logMessage . Message } -(Facility: {{facility}}, Name: {{name}})",
178+ "{Message} -(Facility: {Facility}, Name: {Name})" ,
179+ logMessage . Message ,
171180 logMessage . Facility ,
172181 logMessage . Name
173182 ) ;
174183 break ;
175184 case SyslogLevel . Warning :
176185 _logger . LogWarning (
177- $ "{ logMessage . Message } -(Facility: {{facility}}, Name: {{name}})",
186+ "{Message} -(Facility: {Facility}, Name: {Name})" ,
187+ logMessage . Message ,
178188 logMessage . Facility ,
179189 logMessage . Name
180190 ) ;
181191 break ;
182192 case SyslogLevel . Notice :
183193 case SyslogLevel . Info :
184194 _logger . LogInformation (
185- $ "{ logMessage . Message } -(Facility: {{facility}}, Name: {{name}})",
195+ "{Message} -(Facility: {Facility}, Name: {Name})" ,
196+ logMessage . Message ,
186197 logMessage . Facility ,
187198 logMessage . Name
188199 ) ;
189200 break ;
190201 case SyslogLevel . Debug :
191202 _logger . LogDebug (
192- $ "{ logMessage . Message } -(Facility: {{facility}}, Name: {{name}})",
203+ "{Message} -(Facility: {Facility}, Name: {Name})" ,
204+ logMessage . Message ,
193205 logMessage . Facility ,
194206 logMessage . Name
195207 ) ;
@@ -201,26 +213,37 @@ private void WriteLog(LogMessage logMessage)
201213
202214 private void WriteStatistics ( string json )
203215 {
204- var partitionConsumerLags = JsonSerializer
205- . Deserialize < KafkaStatistics > ( json )
206- ? . Topics ? . Select ( t => t . Value )
207- . SelectMany ( t => t . Partitions ?? new Dictionary < string , KafkaStatisticsPartition > ( ) )
208- . Select ( t => ( Parition : t . Key . ToString ( ) , t . Value . ConsumerLag ) ) ;
209- if ( partitionConsumerLags is null )
216+ using var document = JsonDocument . Parse ( json ) ;
217+ var root = document . RootElement ;
218+
219+ if ( ! root . TryGetProperty ( "topics" , out var topics ) )
210220 {
211221 return ;
212222 }
213223
214- foreach ( var ( partition , consumerLag ) in partitionConsumerLags )
224+ foreach ( var topic in topics . EnumerateObject ( ) )
215225 {
216- var lag = consumerLag ;
217- if ( lag == - 1 )
226+ if ( ! topic . Value . TryGetProperty ( "partitions" , out var partitions ) )
218227 {
219- lag = 0 ;
228+ continue ;
220229 }
221230
222- _consumerLagSummary ? . WithLabels ( _options . Topic , partition ) ? . Observe ( lag ) ;
223- _consumerLagGauge ? . WithLabels ( _options . Topic , partition ) ? . Set ( lag ) ;
231+ foreach ( var partition in partitions . EnumerateObject ( ) )
232+ {
233+ if ( ! partition . Value . TryGetProperty ( "consumer_lag" , out var consumerLagElement ) )
234+ {
235+ continue ;
236+ }
237+
238+ var lag = consumerLagElement . GetInt64 ( ) ;
239+ if ( lag == - 1 )
240+ {
241+ lag = 0 ;
242+ }
243+
244+ _consumerLagSummary ? . WithLabels ( _options . Topic , partition . Name ) ? . Observe ( lag ) ;
245+ _consumerLagGauge ? . WithLabels ( _options . Topic , partition . Name ) ? . Set ( lag ) ;
246+ }
224247 }
225248 }
226249
@@ -241,13 +264,7 @@ CancellationToken token
241264 ) ;
242265 var cloudEvent = KafkaMessageToCloudEvent ( msg . Message ) ;
243266
244- var retryPolicy = Policy
245- . HandleResult < ProcessedMessageStatus > ( status => status == ProcessedMessageStatus . TemporaryFailure )
246- . WaitAndRetryAsync (
247- _options . RetriesOnTemporaryFailure ,
248- retryAttempt => _options . RetryBasePeriod * Math . Pow ( 2 , retryAttempt )
249- ) ;
250- var status = await retryPolicy . ExecuteAsync (
267+ var status = await _retryPolicy . ExecuteAsync (
251268 ( cancellationToken ) => ConsumeCallbackAsync ! . Invoke ( cloudEvent , cancellationToken ) ,
252269 token
253270 ) ;
@@ -272,6 +289,7 @@ CancellationToken token
272289 private readonly Timer _timer ;
273290 private readonly object _commitLock = new ( ) ;
274291 private bool _pendingCommit ;
292+ private int _messagesSinceLastCommit ;
275293
276294 private async Task ExecuteCommitLoopAsync ( CancellationToken cancellationToken )
277295 {
@@ -281,7 +299,7 @@ private async Task ExecuteCommitLoopAsync(CancellationToken cancellationToken)
281299 {
282300 try
283301 {
284- var result = await PeekAndAwaitProcessedMessages ( cancellationToken ) ;
302+ var result = await ReadAndAwaitProcessedMessage ( cancellationToken ) ;
285303
286304 if ( IsIrrecoverableFailure ( result . ProcessedMessageStatus ) )
287305 {
@@ -290,16 +308,16 @@ private async Task ExecuteCommitLoopAsync(CancellationToken cancellationToken)
290308 break ;
291309 }
292310
293- // Remove message from channel, when Task is successfully completed
294- await _processedMessages . Reader . ReadAsync ( cancellationToken ) ;
295-
296311 lock ( _commitLock )
297312 {
298313 _consumer ? . StoreOffset ( result . ConsumeResult ) ;
299314 _pendingCommit = true ;
315+ _messagesSinceLastCommit ++ ;
300316 }
301317
302- if ( ( result . ConsumeResult . Offset . Value + 1 ) % _options . CommitPeriod == 0 )
318+ // Use message count since last commit instead of offset-based check.
319+ // This works correctly across multiple partitions with non-contiguous offsets.
320+ if ( _messagesSinceLastCommit >= _options . CommitPeriod )
303321 {
304322 Commit ( ) ;
305323 RestartCommitTimer ( ) ;
@@ -314,17 +332,11 @@ private async Task ExecuteCommitLoopAsync(CancellationToken cancellationToken)
314332 StopCommitTimer ( ) ;
315333 }
316334
317- private async Task < ConsumeResultAndProcessedMessageStatus > PeekAndAwaitProcessedMessages (
335+ private async Task < ConsumeResultAndProcessedMessageStatus > ReadAndAwaitProcessedMessage (
318336 CancellationToken cancellationToken
319337 )
320338 {
321- await _processedMessages . Reader . WaitToReadAsync ( cancellationToken ) ;
322-
323- if ( ! _processedMessages . Reader . TryPeek ( out var consumeAndProcessTask ) )
324- {
325- throw new InvalidOperationException ( "Awaited channel data has been removed by another consumer" ) ;
326- }
327-
339+ var consumeAndProcessTask = await _processedMessages . Reader . ReadAsync ( cancellationToken ) ;
328340 return await consumeAndProcessTask ;
329341 }
330342
@@ -338,6 +350,7 @@ private void Commit()
338350 }
339351
340352 _pendingCommit = false ;
353+ _messagesSinceLastCommit = 0 ;
341354 try
342355 {
343356 _consumer ? . Commit ( ) ;
@@ -386,7 +399,7 @@ private bool IsIrrecoverableFailure(ProcessedMessageStatus status)
386399 default :
387400 _logger . LogCritical (
388401 LogEvents . UnknownProcessedMessageStatus ,
389- "Unknown processed message status {status }" ,
402+ "Unknown processed message status {Status }" ,
390403 status
391404 ) ;
392405 return true ;
0 commit comments