@@ -105,6 +105,9 @@ internal void Validate()
105
105
case { Values . Count : 0 } :
106
106
throw new ArgumentException ( "Values must be provided when Filter is set" ) ;
107
107
}
108
+
109
+ FlowControl ??= new FlowControl ( ) ;
110
+
108
111
}
109
112
110
113
internal bool IsFiltering => ConsumerFilter is { Values . Count : > 0 } ;
@@ -177,7 +180,7 @@ private RawConsumer(Client client, RawConsumerConfig config, ILogger logger = nu
177
180
ProcessChunks ( ) ;
178
181
}
179
182
180
- // if a user specify a custom offset
183
+ // if a user specifies a custom offset,
181
184
// the _client must filter messages
182
185
// and dispatch only the messages starting from the
183
186
// user offset.
@@ -196,17 +199,12 @@ protected override string GetStream()
196
199
return _config . Stream ;
197
200
}
198
201
199
- public async Task StoreOffset ( ulong offset )
200
- {
201
- await _client . StoreOffset ( _config . Reference , _config . Stream , offset ) . ConfigureAwait ( false ) ;
202
- }
203
-
204
202
////// *********************
205
203
// IsPromotedAsActive is needed to understand if the consumer is active or not
206
204
// by default is active
207
205
// in case of single active consumer can be not active
208
206
// it is important to skip the messages in the chunk that
209
- // it is in progress. In this way the promotion will be faster
207
+ // it is in progress. In this way, the promotion will be faster
210
208
// avoiding to block the consumer handler if the user put some
211
209
// long task
212
210
private bool IsPromotedAsActive { get ; set ; }
@@ -219,14 +217,14 @@ public async Task StoreOffset(ulong offset)
219
217
220
218
/// <summary>
221
219
/// MaybeLockDispatch locks the dispatch of the messages
222
- /// it is needed only when the consumer is single active consumer
223
- /// MaybeLockDispatch is an optimization to avoid to lock the dispatch
220
+ /// it is needed only when the consumer is single active consumer.
221
+ /// MaybeLockDispatch is an optimization to avoid lock the dispatch
224
222
/// when the consumer is not single active consumer
225
223
/// </summary>
226
224
private async Task MaybeLockDispatch ( )
227
225
{
228
226
if ( _config . IsSingleActiveConsumer )
229
- await PromotionLock . WaitAsync ( Token ) . ConfigureAwait ( false ) ;
227
+ await PromotionLock . WaitAsync ( TimeSpan . FromSeconds ( 5 ) , Token ) . ConfigureAwait ( false ) ;
230
228
}
231
229
232
230
/// <summary>
@@ -309,9 +307,14 @@ async Task DispatchMessage(Message message, ulong i)
309
307
{
310
308
if ( ! Token . IsCancellationRequested )
311
309
{
310
+ // we need to lock the dispatch only if the consumer is single active consumer
311
+ await MaybeLockDispatch ( ) . ConfigureAwait ( false ) ;
312
+ var lockedIsPromotedAsActive = IsPromotedAsActive ;
313
+ MaybeReleaseLock ( ) ;
314
+
312
315
// it is usually active
313
316
// it is useful only in single active consumer
314
- if ( IsPromotedAsActive )
317
+ if ( lockedIsPromotedAsActive )
315
318
{
316
319
if ( _status != EntityStatus . Open )
317
320
{
@@ -423,15 +426,7 @@ await _config.MessageHandler(this,
423
426
for ( ulong z = 0 ; z < subEntryChunk . NumRecordsInBatch ; z ++ )
424
427
{
425
428
var message = MessageFromSequence ( ref unCompressedData , ref compressOffset ) ;
426
- await MaybeLockDispatch ( ) . ConfigureAwait ( false ) ;
427
- try
428
- {
429
- await DispatchMessage ( message , messageOffset ++ ) . ConfigureAwait ( false ) ;
430
- }
431
- finally
432
- {
433
- MaybeReleaseLock ( ) ;
434
- }
429
+ await DispatchMessage ( message , messageOffset ++ ) . ConfigureAwait ( false ) ;
435
430
}
436
431
437
432
numRecords -= subEntryChunk . NumRecordsInBatch ;
@@ -479,13 +474,23 @@ await _chunksBuffer.Reader.WaitToReadAsync(Token).ConfigureAwait(false)) //
479
474
{
480
475
if ( Token . IsCancellationRequested )
481
476
break ;
482
- await _client . Credit ( EntityId , 1 ) . ConfigureAwait ( false ) ;
477
+ // Request the credit to the server
478
+ if ( _config . FlowControl . Strategy ==
479
+ ConsumerFlowStrategy . CreditsBeforeParseChunk )
480
+ {
481
+ // Request the credit before processing the chunk
482
+ // this is the default behavior
483
+ // it is useful to keep the network busy
484
+ // and avoid to wait for the next chunk
485
+ await _client . Credit ( EntityId , 1 )
486
+ . ConfigureAwait ( false ) ;
487
+ }
483
488
}
484
489
catch ( InvalidOperationException )
485
490
{
486
491
// The client has been closed
487
492
// Suppose a scenario where the client is closed and the ProcessChunks task is still running
488
- // we remove the the subscriber from the client and we close the client
493
+ // we remove the subscriber from the client and we close the client
489
494
// The ProcessChunks task will try to send the credit to the server
490
495
// The client will throw an InvalidOperationException
491
496
// since the connection is closed
@@ -514,6 +519,14 @@ await _chunksBuffer.Reader.WaitToReadAsync(Token).ConfigureAwait(false)) //
514
519
case ChunkAction . TryToProcess :
515
520
// That's what happens most of the time, and this is the default action
516
521
await ParseChunk ( chunk ) . ConfigureAwait ( false ) ;
522
+
523
+ if ( _config . FlowControl . Strategy == ConsumerFlowStrategy . CreditsAfterParseChunk )
524
+ {
525
+ // it avoids flooding the network with credits
526
+ await _client . Credit ( EntityId , 1 )
527
+ . ConfigureAwait ( false ) ;
528
+ }
529
+
517
530
break ;
518
531
default :
519
532
throw new ArgumentOutOfRangeException ( ) ;
@@ -598,7 +611,7 @@ private async Task Init()
598
611
chunkConsumed ++ ;
599
612
// Send the chunk to the _chunksBuffer
600
613
// in this way the chunks are processed in a separate thread
601
- // this wont' block the socket thread
614
+ // this won't block the socket thread
602
615
// introduced https://github.com/rabbitmq/rabbitmq-stream-dotnet-client/pull/250
603
616
if ( Token . IsCancellationRequested )
604
617
{
@@ -642,7 +655,7 @@ await _chunksBuffer.Writer.WriteAsync((deliver.Chunk, chunkAction), Token)
642
655
{
643
656
// The consumer is closing from the user but some chunks are still in the buffer
644
657
// simply skip the chunk since the Token.IsCancellationRequested is true
645
- // the catch is needed to avoid to propagate the exception to the socket thread.
658
+ // the catch is needed to avoid propagating the exception to the socket thread.
646
659
Logger ? . LogWarning (
647
660
"OperationCanceledException. {EntityInfo} has been closed while consuming messages. " +
648
661
"Token.IsCancellationRequested: {IsCancellationRequested}" ,
@@ -719,7 +732,7 @@ private ClientParameters.MetadataUpdateHandler OnMetadataUpdate() =>
719
732
// at this point the server has removed the consumer from the list
720
733
// and the unsubscribe is not needed anymore (ignoreIfClosed = true)
721
734
// we call the Close to re-enter to the standard behavior
722
- // ignoreIfClosed is an optimization to avoid to send the unsubscribe
735
+ // ignoreIfClosed is an optimization to avoid sending the unsubscribe
723
736
_config . Pool . RemoveConsumerEntityFromStream ( _client . ClientId , EntityId , _config . Stream ) ;
724
737
await Shutdown ( _config , true ) . ConfigureAwait ( false ) ;
725
738
_config . MetadataHandler ? . Invoke ( metaDataUpdate ) ;
@@ -770,7 +783,7 @@ protected override async Task<ResponseCode> DeleteEntityFromTheServer(bool ignor
770
783
public override async Task < ResponseCode > Close ( )
771
784
{
772
785
// when the consumer is closed we must be sure that the
773
- // the subscription is completed to avoid problems with the connection
786
+ // subscription is completed to avoid problems with the connection
774
787
// It could happen when the closing is called just after the creation
775
788
_completeSubscription . Task . Wait ( ) ;
776
789
return await Shutdown ( _config ) . ConfigureAwait ( false ) ;
@@ -790,5 +803,36 @@ public void Dispose()
790
803
}
791
804
792
805
public ConsumerInfo Info { get ; }
806
+
807
+ public async Task StoreOffset ( ulong offset )
808
+ {
809
+ await _client . StoreOffset ( _config . Reference , _config . Stream , offset ) . ConfigureAwait ( false ) ;
810
+ }
811
+
812
+ /// <summary>
813
+ /// Request credits from the server.
814
+ /// Valid only if the ConsumerFlowStrategy is set to ConsumerFlowStrategy.ConsumerCredits.
815
+ /// </summary>
816
+ public async Task Credits ( )
817
+ {
818
+ await Credits ( 1 ) . ConfigureAwait ( false ) ;
819
+ }
820
+
821
+ private async Task Credits ( ushort credits )
822
+ {
823
+ if ( credits < 1 )
824
+ {
825
+ throw new ArgumentException (
826
+ $ "Credits must be greater than 0") ;
827
+ }
828
+
829
+ if ( _config . FlowControl . Strategy != ConsumerFlowStrategy . ConsumerCredits )
830
+ {
831
+ throw new InvalidOperationException (
832
+ "RequestCredits can be used only with ConsumerFlowStrategy.ManualRequestCredit." ) ;
833
+ }
834
+
835
+ await _client . Credit ( EntityId , credits ) . ConfigureAwait ( false ) ;
836
+ }
793
837
}
794
838
}
0 commit comments