Skip to content

Commit d957975

Browse files
committed
Always dispose outside of lock. Use Interlocked.Exchange to clear
1 parent 2576877 commit d957975

File tree

1 file changed

+19
-15
lines changed

1 file changed

+19
-15
lines changed

src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -135,17 +135,17 @@ private async Task RefreshAsyncInternal()
135135
CacheStatus newCacheState;
136136
try
137137
{
138+
// Capture the existing change token registration, if any, so we can dispose it.
139+
// Disposing inside the lock can cause a deadlock since Dispose waits for any in-flight callback to complete,
140+
// but the callback may be waiting to acquire _lock.
138141
IDisposable? changeTokenRegistration;
139142
lock (_lock)
140143
{
141-
// Capture the existing change token registration, if any, so we can dispose it outside the lock.
142-
// Disposing inside the lock can cause a deadlock since Dispose waits for any in-flight callback to complete,
143-
// but the callback may be waiting to acquire _lock.
144144
changeTokenRegistration = _changeTokenRegistration;
145145
_changeTokenRegistration = null;
146146
}
147147

148-
// Dispose the existing change token registration, if any, outside the lock to avoid deadlock.
148+
// Dispose the existing change token registration, if any.
149149
changeTokenRegistration?.Dispose();
150150

151151
Log.ResolvingEndpoints(_logger, ServiceName);
@@ -159,6 +159,10 @@ private async Task RefreshAsyncInternal()
159159
var endpoints = builder.Build();
160160
newCacheState = CacheStatus.Valid;
161161

162+
// Capture the existing timer, if any, so we can dispose it outside the lock.
163+
// Disposing inside the lock can cause a deadlock.
164+
ITimer? pollingTimer = null;
165+
162166
lock (_lock)
163167
{
164168
// Check if we need to poll for updates or if we can register for change notification callbacks.
@@ -167,8 +171,7 @@ private async Task RefreshAsyncInternal()
167171
// Initiate a background refresh when the change token fires.
168172
_changeTokenRegistration = endpoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndpointWatcher)state!).RefreshAsync(force: false), this);
169173

170-
// Dispose the existing timer, if any, since we are reliant on change tokens for updates.
171-
_pollingTimer?.Dispose();
174+
pollingTimer = _pollingTimer;
172175
_pollingTimer = null;
173176
}
174177
else
@@ -180,6 +183,8 @@ private async Task RefreshAsyncInternal()
180183
newEndpoints = endpoints;
181184
newCacheState = CacheStatus.Valid;
182185
}
186+
187+
pollingTimer?.Dispose();
183188
}
184189
catch (Exception exception)
185190
{
@@ -236,16 +241,14 @@ private async Task RefreshAsyncInternal()
236241

237242
private void SchedulePollingTimer()
238243
{
244+
ITimer? timerToDispose = null;
239245
lock (_lock)
240246
{
241247
if (_disposalCancellation.IsCancellationRequested)
242248
{
243-
_pollingTimer?.Dispose();
244-
_pollingTimer = null;
245-
return;
249+
timerToDispose = Interlocked.Exchange(ref _pollingTimer, null);
246250
}
247-
248-
if (_pollingTimer is null)
251+
else if (_pollingTimer is null)
249252
{
250253
_pollingTimer = _timeProvider.CreateTimer(s_pollingAction, this, _options.RefreshPeriod, TimeSpan.Zero);
251254
}
@@ -254,6 +257,8 @@ private void SchedulePollingTimer()
254257
_pollingTimer.Change(_options.RefreshPeriod, TimeSpan.Zero);
255258
}
256259
}
260+
261+
timerToDispose?.Dispose();
257262
}
258263

259264
/// <inheritdoc/>
@@ -268,16 +273,15 @@ public async ValueTask DisposeAsync()
268273
_logger.LogError(exception, "Error cancelling disposal cancellation token.");
269274
}
270275

276+
// Capture the existing change token registration and polling timer so we can dispose them.
277+
// Disposing _changeTokenRegistration inside the lock can cause a deadlock since Dispose waits for any in-flight
278+
// callback to complete, but the callback may be waiting to acquire _lock.
271279
IDisposable? changeTokenRegistration;
272280
ITimer? pollingTimer;
273281
lock (_lock)
274282
{
275-
// Capture the existing change token registration and polling timer so we can dispose them outside the lock.
276-
// Disposing _changeTokenRegistration inside the lock can cause a deadlock since Dispose waits for any in-flight
277-
// callback to complete, but the callback may be waiting to acquire _lock.
278283
changeTokenRegistration = _changeTokenRegistration;
279284
_changeTokenRegistration = null;
280-
281285
pollingTimer = _pollingTimer;
282286
_pollingTimer = null;
283287
}

0 commit comments

Comments
 (0)