Skip to content

Commit 26bc93c

Browse files
committed
* Add lock object for disposal of Channels and Connections. Note: a SemaphoreSlim can't be used because it must be disposed as well, and that can't happen cleanly in a Dispose method.
1 parent 0b80ed3 commit 26bc93c

File tree

4 files changed

+103
-1
lines changed

4 files changed

+103
-1
lines changed

projects/RabbitMQ.Client/Impl/AutorecoveringChannel.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ internal sealed class AutorecoveringChannel : IChannel, IRecoverable
4747

4848
private AutorecoveringConnection _connection;
4949
private RecoveryAwareChannel _innerChannel;
50+
5051
private bool _disposedValue;
52+
private bool _isDisposing;
53+
private readonly object _isDisposingLock = new();
5154

5255
private ushort _prefetchCountConsumer;
5356
private ushort _prefetchCountGlobal;
@@ -269,6 +272,15 @@ public async ValueTask DisposeAsync()
269272
return;
270273
}
271274

275+
lock (_isDisposingLock)
276+
{
277+
if (_isDisposing)
278+
{
279+
return;
280+
}
281+
_isDisposing = true;
282+
}
283+
272284
try
273285
{
274286
if (IsOpen)
@@ -282,6 +294,7 @@ await this.AbortAsync()
282294
finally
283295
{
284296
_disposedValue = true;
297+
_isDisposing = false;
285298
}
286299
}
287300

projects/RabbitMQ.Client/Impl/Channel.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ internal partial class Channel : IChannel, IRecoverable
6161

6262
internal readonly IConsumerDispatcher ConsumerDispatcher;
6363

64-
private bool _disposedValue = false;
64+
private bool _disposedValue;
65+
private bool _isDisposing;
66+
private readonly object _isDisposingLock = new();
6567

6668
public Channel(ISession session, CreateChannelOptions createChannelOptions)
6769
{
@@ -546,6 +548,15 @@ protected virtual void Dispose(bool disposing)
546548

547549
if (disposing)
548550
{
551+
lock (_isDisposingLock)
552+
{
553+
if (_isDisposing)
554+
{
555+
return;
556+
}
557+
_isDisposing = true;
558+
}
559+
549560
try
550561
{
551562
if (IsOpen)
@@ -561,14 +572,29 @@ protected virtual void Dispose(bool disposing)
561572
finally
562573
{
563574
_disposedValue = true;
575+
_isDisposing = false;
564576
}
565577
}
566578
}
567579

568580
protected virtual async ValueTask DisposeAsyncCore(bool disposing)
569581
{
582+
if (_disposedValue)
583+
{
584+
return;
585+
}
586+
570587
if (disposing)
571588
{
589+
lock (_isDisposingLock)
590+
{
591+
if (_isDisposing)
592+
{
593+
return;
594+
}
595+
_isDisposing = true;
596+
}
597+
572598
try
573599
{
574600
if (IsOpen)
@@ -590,6 +616,7 @@ await _outstandingPublisherConfirmationsRateLimiter.DisposeAsync()
590616
finally
591617
{
592618
_disposedValue = true;
619+
_isDisposing = false;
593620
}
594621
}
595622
}

projects/RabbitMQ.Client/Impl/Connection.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ namespace RabbitMQ.Client.Framing
4646
internal sealed partial class Connection : IConnection
4747
{
4848
private bool _disposedValue;
49+
private bool _isDisposing;
50+
private readonly object _isDisposingLock = new();
51+
4952
private volatile bool _closed;
5053

5154
private readonly ConnectionConfig _config;
@@ -502,6 +505,15 @@ public async ValueTask DisposeAsync()
502505
return;
503506
}
504507

508+
lock (_isDisposingLock)
509+
{
510+
if (_isDisposing)
511+
{
512+
return;
513+
}
514+
_isDisposing = true;
515+
}
516+
505517
try
506518
{
507519
if (IsOpen)
@@ -523,6 +535,7 @@ await _channel0.DisposeAsync()
523535
finally
524536
{
525537
_disposedValue = true;
538+
_isDisposing = false;
526539
}
527540
}
528541

projects/Test/Integration/TestChannelShutdown.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
//---------------------------------------------------------------------------
3131

3232
using System;
33+
using System.Collections.Generic;
34+
using System.Threading;
3335
using System.Threading.Tasks;
3436
using RabbitMQ.Client;
3537
using RabbitMQ.Client.Impl;
@@ -61,5 +63,52 @@ public async Task TestConsumerDispatcherShutdown()
6163
await WaitAsync(tcs, TimeSpan.FromSeconds(5), "channel shutdown");
6264
Assert.True(autorecoveringChannel.ConsumerDispatcher.IsShutdown, "dispatcher should be shut down after CloseAsync");
6365
}
66+
67+
[Fact]
68+
public async Task TestConcurrentDisposeAsync_GH1749()
69+
{
70+
bool sawCallbackException = false;
71+
int channelShutdownCount = 0;
72+
73+
_channel.CallbackExceptionAsync += (channel, ea) =>
74+
{
75+
sawCallbackException = true;
76+
return Task.CompletedTask;
77+
};
78+
79+
_channel.ChannelShutdownAsync += (channel, args) =>
80+
{
81+
Interlocked.Increment(ref channelShutdownCount);
82+
return Task.CompletedTask;
83+
};
84+
85+
var disposeTasks = new List<ValueTask>
86+
{
87+
_channel.DisposeAsync(),
88+
_channel.DisposeAsync(),
89+
_channel.DisposeAsync()
90+
};
91+
92+
foreach (ValueTask vt in disposeTasks)
93+
{
94+
await vt;
95+
}
96+
97+
Assert.Equal(1, channelShutdownCount);
98+
Assert.False(sawCallbackException);
99+
100+
disposeTasks.Clear();
101+
disposeTasks.Add(_conn.DisposeAsync());
102+
disposeTasks.Add(_conn.DisposeAsync());
103+
disposeTasks.Add(_conn.DisposeAsync());
104+
105+
foreach (ValueTask vt in disposeTasks)
106+
{
107+
await vt;
108+
}
109+
110+
_channel = null;
111+
_conn = null;
112+
}
64113
}
65114
}

0 commit comments

Comments
 (0)