@@ -43,7 +43,9 @@ internal abstract partial class RetryCallBase<TRequest, TResponse> : IGrpcCall<T
43
43
private readonly TaskCompletionSource < IGrpcCall < TRequest , TResponse > > _commitedCallTcs ;
44
44
private RetryCallBaseClientStreamReader < TRequest , TResponse > ? _retryBaseClientStreamReader ;
45
45
private RetryCallBaseClientStreamWriter < TRequest , TResponse > ? _retryBaseClientStreamWriter ;
46
- private CancellationTokenRegistration ? _ctsRegistration ;
46
+
47
+ // Internal for unit testing.
48
+ internal CancellationTokenRegistration ? _ctsRegistration ;
47
49
48
50
protected object Lock { get ; } = new object ( ) ;
49
51
protected ILogger Logger { get ; }
@@ -52,14 +54,14 @@ internal abstract partial class RetryCallBase<TRequest, TResponse> : IGrpcCall<T
52
54
protected int MaxRetryAttempts { get ; }
53
55
protected CancellationTokenSource CancellationTokenSource { get ; }
54
56
protected TaskCompletionSource < IGrpcCall < TRequest , TResponse > ? > ? NewActiveCallTcs { get ; set ; }
55
- protected bool Disposed { get ; private set ; }
56
57
57
58
public GrpcChannel Channel { get ; }
58
59
public Task < IGrpcCall < TRequest , TResponse > > CommitedCallTask => _commitedCallTcs . Task ;
59
60
public IAsyncStreamReader < TResponse > ? ClientStreamReader => _retryBaseClientStreamReader ??= new RetryCallBaseClientStreamReader < TRequest , TResponse > ( this ) ;
60
61
public IClientStreamWriter < TRequest > ? ClientStreamWriter => _retryBaseClientStreamWriter ??= new RetryCallBaseClientStreamWriter < TRequest , TResponse > ( this ) ;
61
62
public WriteOptions ? ClientStreamWriteOptions { get ; internal set ; }
62
63
public bool ClientStreamComplete { get ; set ; }
64
+ public bool Disposed { get ; private set ; }
63
65
64
66
protected int AttemptCount { get ; private set ; }
65
67
protected List < ReadOnlyMemory < byte > > BufferedMessages { get ; }
@@ -345,6 +347,16 @@ protected void CommitCall(IGrpcCall<TRequest, TResponse> call, CommitReason comm
345
347
346
348
NewActiveCallTcs ? . SetResult ( null ) ;
347
349
_commitedCallTcs . SetResult ( call ) ;
350
+
351
+ // If the commited call has finished and cleaned up then it is safe for
352
+ // the wrapping retry call to clean up. This is required to unregister
353
+ // from the cancellation token and avoid a memory leak.
354
+ //
355
+ // A commited call that has already cleaned up is likely a StatusGrpcCall.
356
+ if ( call . Disposed )
357
+ {
358
+ Cleanup ( ) ;
359
+ }
348
360
}
349
361
}
350
362
}
@@ -406,18 +418,24 @@ protected virtual void Dispose(bool disposing)
406
418
407
419
if ( disposing )
408
420
{
409
- _ctsRegistration ? . Dispose ( ) ;
410
- CancellationTokenSource . Cancel ( ) ;
411
-
412
421
if ( CommitedCallTask . IsCompletedSuccessfully ( ) )
413
422
{
414
423
CommitedCallTask . Result . Dispose ( ) ;
415
424
}
416
425
417
- ClearRetryBuffer ( ) ;
426
+ Cleanup ( ) ;
418
427
}
419
428
}
420
429
430
+ protected void Cleanup ( )
431
+ {
432
+ _ctsRegistration ? . Dispose ( ) ;
433
+ _ctsRegistration = null ;
434
+ CancellationTokenSource . Cancel ( ) ;
435
+
436
+ ClearRetryBuffer ( ) ;
437
+ }
438
+
421
439
internal bool TryAddToRetryBuffer ( ReadOnlyMemory < byte > message )
422
440
{
423
441
lock ( Lock )
0 commit comments