@@ -53,7 +53,9 @@ public record ClientParameters
5353 public string VirtualHost { get ; set ; } = "/" ;
5454 public EndPoint Endpoint { get ; set ; } = new IPEndPoint ( IPAddress . Loopback , 5552 ) ;
5555
56- public Action < MetaDataUpdate > MetadataHandler { get ; set ; } = _ => { } ;
56+ public delegate void MetadataUpdateHandler ( MetaDataUpdate update ) ;
57+
58+ public event MetadataUpdateHandler OnMetadataUpdate ;
5759 public Action < Exception > UnhandledExceptionHandler { get ; set ; } = _ => { } ;
5860 public TimeSpan Heartbeat { get ; set ; } = TimeSpan . FromMinutes ( 1 ) ;
5961
@@ -71,6 +73,11 @@ public string ClientProvidedName
7173 public AddressResolver AddressResolver { get ; set ; } = null ;
7274
7375 public AuthMechanism AuthMechanism { get ; set ; } = AuthMechanism . Plain ;
76+
77+ internal void FireMetadataUpdate ( MetaDataUpdate metaDataUpdate )
78+ {
79+ OnMetadataUpdate ? . Invoke ( metaDataUpdate ) ;
80+ }
7481 }
7582
7683 internal readonly struct OutgoingMsg : ICommand
@@ -213,7 +220,8 @@ await client
213220 . ConfigureAwait ( false ) ;
214221 logger ? . LogDebug ( "Sasl mechanism: {Mechanisms}" , saslHandshakeResponse . Mechanisms ) ;
215222
216- var isValid = saslHandshakeResponse . Mechanisms . Contains ( parameters . AuthMechanism . ToString ( ) . ToUpperInvariant ( ) ,
223+ var isValid = saslHandshakeResponse . Mechanisms . Contains (
224+ parameters . AuthMechanism . ToString ( ) . ToUpperInvariant ( ) ,
217225 StringComparer . OrdinalIgnoreCase ) ;
218226 if ( ! isValid )
219227 {
@@ -225,7 +233,8 @@ await client
225233 var authResponse =
226234 await client
227235 . Request < SaslAuthenticateRequest , SaslAuthenticateResponse > ( corr =>
228- new SaslAuthenticateRequest ( corr , parameters . AuthMechanism . ToString ( ) . ToUpperInvariant ( ) , saslData ) )
236+ new SaslAuthenticateRequest ( corr , parameters . AuthMechanism . ToString ( ) . ToUpperInvariant ( ) ,
237+ saslData ) )
229238 . ConfigureAwait ( false ) ;
230239 ClientExceptions . MaybeThrowException ( authResponse . ResponseCode , parameters . UserName ) ;
231240
@@ -322,22 +331,28 @@ public ValueTask<bool> Publish<T>(T msg) where T : struct, ICommand
322331 return ( publisherId , response ) ;
323332 }
324333
325- public async Task < DeletePublisherResponse > DeletePublisher ( byte publisherId )
334+ public async Task < DeletePublisherResponse > DeletePublisher ( byte publisherId ,
335+ bool ignoreIfAlreadyRemoved = false )
326336 {
327337 await _poolSemaphore . WaitAsync ( ) . ConfigureAwait ( false ) ;
328338 try
329339 {
330- var result =
331- await Request < DeletePublisherRequest , DeletePublisherResponse > ( corr =>
332- new DeletePublisherRequest ( corr , publisherId ) ) . ConfigureAwait ( false ) ;
340+ if ( ! ignoreIfAlreadyRemoved )
341+ {
342+ var result =
343+ await Request < DeletePublisherRequest , DeletePublisherResponse > ( corr =>
344+ new DeletePublisherRequest ( corr , publisherId ) ) . ConfigureAwait ( false ) ;
333345
334- return result ;
346+ return result ;
347+ }
335348 }
336349 finally
337350 {
338351 publishers . Remove ( publisherId ) ;
339352 _poolSemaphore . Release ( ) ;
340353 }
354+
355+ return new DeletePublisherResponse ( ) ;
341356 }
342357
343358 public async Task < ( byte , SubscribeResponse ) > Subscribe ( string stream , IOffsetType offsetType ,
@@ -386,20 +401,24 @@ await Request<DeletePublisherRequest, DeletePublisherResponse>(corr =>
386401 return ( subscriptionId , response ) ;
387402 }
388403
389- public async Task < UnsubscribeResponse > Unsubscribe ( byte subscriptionId )
404+ public async Task < UnsubscribeResponse > Unsubscribe ( byte subscriptionId , bool ignoreIfAlreadyRemoved = false )
390405 {
391406 await _poolSemaphore . WaitAsync ( ) . ConfigureAwait ( false ) ;
392407 try
393408 {
394- // here we reduce a bit the timeout to avoid waiting too much
395- // if the client is busy with read operations it can take time to process the unsubscribe
396- // but the subscribe is removed.
397- var result =
398- await Request < UnsubscribeRequest , UnsubscribeResponse > ( corr =>
399- new UnsubscribeRequest ( corr , subscriptionId ) , TimeSpan . FromSeconds ( 5 ) ) . ConfigureAwait ( false ) ;
400- _logger . LogDebug ( "Unsubscribe request : {SubscriptionId}" , subscriptionId ) ;
401-
402- return result ;
409+ if ( ! ignoreIfAlreadyRemoved )
410+ {
411+ // here we reduce a bit the timeout to avoid waiting too much
412+ // if the client is busy with read operations it can take time to process the unsubscribe
413+ // but the subscribe is removed.
414+ var result =
415+ await Request < UnsubscribeRequest , UnsubscribeResponse > ( corr =>
416+ new UnsubscribeRequest ( corr , subscriptionId ) ,
417+ TimeSpan . FromSeconds ( 5 ) ) . ConfigureAwait ( false ) ;
418+ _logger . LogDebug ( "Unsubscribe request : {SubscriptionId}" , subscriptionId ) ;
419+
420+ return result ;
421+ }
403422 }
404423 finally
405424 {
@@ -408,6 +427,8 @@ await Request<UnsubscribeRequest, UnsubscribeResponse>(corr =>
408427 consumers . Remove ( subscriptionId ) ;
409428 _poolSemaphore . Release ( ) ;
410429 }
430+
431+ return new UnsubscribeResponse ( ) ;
411432 }
412433
413434 public async Task < PartitionsQueryResponse > QueryPartition ( string superStream )
@@ -477,12 +498,25 @@ private async Task HandleIncoming(Memory<byte> frameMemory)
477498 case PublishConfirm . Key :
478499 PublishConfirm . Read ( frame , out var confirm ) ;
479500 confirmFrames += 1 ;
480- var ( confirmCallback , _) = publishers [ confirm . PublisherId ] ;
481- confirmCallback ( confirm . PublishingIds ) ;
482- if ( MemoryMarshal . TryGetArray ( confirm . PublishingIds , out var confirmSegment ) )
501+ if ( publishers . TryGetValue ( confirm . PublisherId , out var publisherConf ) )
483502 {
484- if ( confirmSegment . Array != null )
485- ArrayPool < ulong > . Shared . Return ( confirmSegment . Array ) ;
503+ var ( confirmCallback , _) = publisherConf ;
504+ confirmCallback ( confirm . PublishingIds ) ;
505+ if ( MemoryMarshal . TryGetArray ( confirm . PublishingIds , out var confirmSegment ) )
506+ {
507+ if ( confirmSegment . Array != null )
508+ ArrayPool < ulong > . Shared . Return ( confirmSegment . Array ) ;
509+ }
510+ }
511+ else
512+ {
513+ // the producer is not found, this can happen when the producer is closing
514+ // and there are still confirmation on the wire
515+ // we can ignore the error since the producer does not exists anymore
516+ _logger ? . LogDebug (
517+ "Could not find stream producer {ID} or producer is closing." +
518+ "A possible cause it that the producer was closed and the are still confirmation on the wire. " ,
519+ confirm . PublisherId ) ;
486520 }
487521
488522 break ;
@@ -507,12 +541,26 @@ private async Task HandleIncoming(Memory<byte> frameMemory)
507541 break ;
508542 case PublishError . Key :
509543 PublishError . Read ( frame , out var error ) ;
510- var ( _, errorCallback ) = publishers [ error . PublisherId ] ;
511- errorCallback ( error . PublishingErrors ) ;
544+ if ( publishers . TryGetValue ( error . PublisherId , out var publisher ) )
545+ {
546+ var ( _, errorCallback ) = publisher ;
547+ errorCallback ( error . PublishingErrors ) ;
548+ }
549+ else
550+ {
551+ // the producer is not found, this can happen when the producer is closing
552+ // and there are still confirmation on the wire
553+ // we can ignore the error since the producer does not exists anymore
554+ _logger ? . LogDebug (
555+ "Could not find stream producer {ID} or producer is closing." +
556+ "A possible cause it that the producer was closed and the are still confirmation on the wire. " ,
557+ error . PublisherId ) ;
558+ }
559+
512560 break ;
513561 case MetaDataUpdate . Key :
514562 MetaDataUpdate . Read ( frame , out var metaDataUpdate ) ;
515- Parameters . MetadataHandler ( metaDataUpdate ) ;
563+ Parameters . FireMetadataUpdate ( metaDataUpdate ) ;
516564 break ;
517565 case TuneResponse . Key :
518566 TuneResponse . Read ( frame , out var tuneResponse ) ;
0 commit comments