missing cleanup of CancellationTokenSource#4009
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds missing Dispose() calls on CancellationTokenSource instances across multiple files in the SQL client library. Undisposed CTS objects can cause minor resource leaks (they hold internal kernel event handles on some platforms), so cleaning them up is a good practice.
Changes:
- Disposes
_disposalTokenSourceinSqlSequentialTextReaderandSqlSequentialStreamduring theirDispose(bool)methods - Disposes
_cancelAsyncOnCloseTokenSourceinSqlDataReader.Close()andCloseReaderFromConnection()after cancelling it - Disposes reconnection CTS in
SqlConnection.Close()andReconnectAsync(), with field nulling for safety - Disposes
timeoutCtsin all continuation paths (success, failure, cancellation) inSqlCommand.Reader.csandSqlCommand.NonQuery.cs - Converts local CTS to a
usingdeclaration inSsrpClient.netcore.cs
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
SqlSequentialTextReader.cs |
Adds _disposalTokenSource.Dispose() in Dispose(bool) after SetClosed() |
SqlSequentialStream.cs |
Adds _disposalTokenSource.Dispose() in Dispose(bool) after SetClosed() |
SqlDataReader.cs |
Adds _cancelAsyncOnCloseTokenSource.Dispose() in Close() and CloseReaderFromConnection() |
SqlConnection.cs |
Adds CTS disposal in Close() and ReconnectAsync() with null-field pattern |
SqlCommand.Reader.cs |
Disposes timeoutCts in all continuation paths of reconnect setup |
SqlCommand.NonQuery.cs |
Disposes timeoutCts in all continuation paths of reconnect setup |
SsrpClient.netcore.cs |
Changes local CTS to a using declaration for automatic disposal |
|
This pull request has been marked as stale due to inactivity for more than 30 days. If you would like to keep this pull request open, please provide an update or respond to any comments. Otherwise, it will be closed automatically in 7 days. |
|
/azp run |
|
Azure Pipelines successfully started running 2 pipeline(s). |
Use Interlocked.Exchange to null out and safely dispose the _cancelAsyncOnCloseTokenSource in SqlDataReader, avoiding races and null/dispose issues during concurrent close operations. In SqlSequentialStream and SqlSequentialTextReader, guard calls to SetClosed (check for null reader / IsClosed) before invoking it and still dispose the disposal token source. These changes prevent potential NREs or double-dispose scenarios during teardown.
|
/azp run |
|
Azure Pipelines successfully started running 2 pipeline(s). |
…disposal - SqlConnection.Close(): only cancel the reconnect CTS and let ReconnectAsync's finally dispose it, avoiding ObjectDisposedException races with the in-flight reconnect task. - SqlCommand.Reader/NonQuery reconnect continuations: cancel timeoutCts before disposing on the completed/onFailure/onCancellation paths so the Task.Delay timer is released promptly. - SqlSequentialStream/SqlSequentialTextReader.Dispose(): keep the existing IsClosed/_reader guard (which prevents Cancel() on a disposed CTS) and rely on the idempotent CancellationTokenSource.Dispose(); no behavior change, double-dispose remains safe.
|
/azp run sqclient-pr |
|
No pipelines are associated with this pull request. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSequentialTextReader.cs:290
- Disposing
_disposalTokenSourceinDispose(bool)makes later internal cleanup (SqlDataReader.CloseActiveSequentialStreamAndTextReader()callingSetClosed()) able to throwObjectDisposedExceptionif the consumer disposes theSqlSequentialTextReaderbefore the parentSqlDataReaderis closed.SetClosed()currently calls_disposalTokenSource.Cancel()unconditionally, which will throw once the CTS has been disposed.
_disposalTokenSource.Dispose();
}
base.Dispose(disposing);
}
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
…posed CTS - SqlSequentialStream/SqlSequentialTextReader.SetClosed(): make idempotent by returning early when already closed. SqlDataReader.CloseActiveSequentialStreamAndTextReader() can call SetClosed() after the consumer has already disposed the stream/reader (which disposes _disposalTokenSource), so the second Cancel() would throw ObjectDisposedException. - SqlConnection.Close(): guard the reconnect-cancellation Cancel() with try/catch (ObjectDisposedException) to tolerate ReconnectAsync's finally disposing the same CancellationTokenSource concurrently.
|
/azp run sqlclient-pr |
|
Azure Pipelines successfully started running 1 pipeline(s). |
Adds ManualTests covering the dispose-then-close paths fixed in this PR: - Disposing a sequential Stream/TextReader before the owning SqlDataReader closes (and advancing past a result set) must not throw ObjectDisposedException via SetClosed(). - Double-dispose of the sequential Stream/TextReader is safe. - Repeated SqlDataReader.Close()/Dispose() is safe after the close-cancellation CancellationTokenSource is disposed.
|
/azp run sqlclient-pr |
|
Azure Pipelines successfully started running 1 pipeline(s). |
Summary
Several
CancellationTokenSource(CTS) instances across the driver were created but never disposed, leaking the timer/registration resources they hold. This PR disposes those token sources at the correct point in their lifecycle, and adds the guards needed so that the new disposal does not introduceObjectDisposedExceptionfaults when an object is closed/disposed more than once or concurrently.Changes
Managed SNI —
SsrpClient.netcore.csCancellationTokenSourceused for parallel SSRP resolution (MultiSubnetFailover) in ausingso it is disposed when resolution completes.SqlDataReader.csClose()andCloseReaderFromConnection()now dispose_cancelAsyncOnCloseTokenSourceafter cancelling it.Interlocked.Exchange(ref _cancelAsyncOnCloseTokenSource, null)so it is cancelled/disposed exactly once. This keeps repeatedClose()/Dispose()calls (and the broken-connection cleanup path) safe — a second close seesnulland does nothing instead of callingCancel()on a disposed source.SqlConnection.cs— reconnect cancellationReconnectAsyncnow owns itsCancellationTokenSource: it is created before thetryand disposed (and the_reconnectionCancellationSourcefield cleared) in thefinally, once the token is no longer in use.Close()only cancels the reconnect source and letsReconnectAsync'sfinallydispose it, avoiding disposal while the reconnect task is still using the token. TheCancel()call is wrapped intry/catch (ObjectDisposedException)to tolerate the race whereReconnectAsyncdisposes the source concurrently.SqlCommand.NonQuery.cs/SqlCommand.Reader.cs— reconnect timeouttimeoutCtsis now disposed on every path (already-completed, success, failure, and cancellation), not just the success path.Cancel()is called beforeDispose()so theTask.Delay(timeout, timeoutCts.Token)scheduled bySetTimeoutExceptionis released promptly instead of lingering until the timeout elapses.SqlSequentialStream.cs/SqlSequentialTextReader.csDispose(bool)now disposes_disposalTokenSource.SetClosed()is made idempotent (early-return when already closed).SqlDataReader.CloseActiveSequentialStreamAndTextReader()may callSetClosed()after the consumer has already disposed the stream/reader; without the guard the secondCancel()would throwObjectDisposedExceptionon the now-disposed source. The first close already cancels and waits for any pending task, so the later call is safely a no-op.Notes
CancellationTokenSource.Dispose()is idempotent (per theIDisposablecontract), so repeatedDispose()calls are safe; onlyCancel()/Tokenthrow after disposal, which is what the added guards protect against.Checklist
ObjectDisposedExceptionon double-close/concurrent pathsCancellationTokenSourceDisposalTest— dispose-then-close and double-close paths)