@@ -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