From b13294343fa8faf0756d3b23fabf689cf0f0b1a5 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Wed, 2 Apr 2025 20:46:11 +0100 Subject: [PATCH] Port PR 499 --- .../Microsoft/Data/SqlClient/SqlDataReader.cs | 62 ++++++++++++++++--- .../Data/SqlClient/SqlInternalConnection.cs | 3 +- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlDataReader.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlDataReader.cs index 645ba1edc7..71a2b49613 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlDataReader.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlDataReader.cs @@ -30,7 +30,7 @@ namespace Microsoft.Data.SqlClient /// public class SqlDataReader : DbDataReader, IDataReader, IDbColumnSchemaGenerator { - private enum ALTROWSTATUS + internal enum ALTROWSTATUS { Null = 0, // default and after Done AltRow, // after calling NextResult and the first AltRow is available for read @@ -97,9 +97,6 @@ internal class SharedState private SqlSequentialStream _currentStream; private SqlSequentialTextReader _currentTextReader; - private IsDBNullAsyncCallContext _cachedIsDBNullContext; - private ReadAsyncCallContext _cachedReadAsyncContext; - internal SqlDataReader(SqlCommand command, CommandBehavior behavior) { SqlConnection.VerifyExecutePermission(); @@ -4387,6 +4384,10 @@ internal TdsOperationStatus TryReadColumnInternal(int i, bool readHeaderOnly/* = { // reset snapshot to save memory use. We can safely do that here because all SqlDataReader values are stable. // The retry logic can use the current values to get back to the right state. + if (_connection?.InnerConnection is SqlInternalConnection sqlInternalConnection && sqlInternalConnection.CachedDataReaderSnapshot is null) + { + sqlInternalConnection.CachedDataReaderSnapshot = _snapshot; + } _snapshot = null; PrepareAsyncInvocation(useSnapshot: true); } @@ -5318,7 +5319,15 @@ public override Task ReadAsync(CancellationToken cancellationToken) return source.Task; } - var context = Interlocked.Exchange(ref _cachedReadAsyncContext, null) ?? new ReadAsyncCallContext(); + ReadAsyncCallContext context = null; + if (_connection?.InnerConnection is SqlInternalConnection sqlInternalConnection) + { + context = Interlocked.Exchange(ref sqlInternalConnection.CachedDataReaderReadAsyncContext, null); + } + if (context is null) + { + context = new ReadAsyncCallContext(); + } Debug.Assert(context.Reader == null && context.Source == null && context.Disposable == default, "cached ReadAsyncCallContext was not properly disposed"); @@ -5358,6 +5367,10 @@ private static Task ReadAsyncExecute(Task task, object state) if (!hasReadRowToken) { hasReadRowToken = true; + if (reader.Connection?.InnerConnection is SqlInternalConnection sqlInternalConnection && sqlInternalConnection.CachedDataReaderSnapshot is null) + { + sqlInternalConnection.CachedDataReaderSnapshot = reader._snapshot; + } reader._snapshot = null; reader.PrepareAsyncInvocation(useSnapshot: true); } @@ -5377,7 +5390,10 @@ private static Task ReadAsyncExecute(Task task, object state) private void SetCachedReadAsyncCallContext(ReadAsyncCallContext instance) { - Interlocked.CompareExchange(ref _cachedReadAsyncContext, instance, null); + if (_connection?.InnerConnection is SqlInternalConnection sqlInternalConnection) + { + Interlocked.CompareExchange(ref sqlInternalConnection.CachedDataReaderReadAsyncContext, instance, null); + } } /// @@ -5479,7 +5495,15 @@ override public Task IsDBNullAsync(int i, CancellationToken cancellationTo registrationHolder.Set(cancellationToken.Register(SqlCommand.s_cancelIgnoreFailure, _command)); } - IsDBNullAsyncCallContext context = Interlocked.Exchange(ref _cachedIsDBNullContext, null) ?? new IsDBNullAsyncCallContext(); + IsDBNullAsyncCallContext context = null; + if (_connection?.InnerConnection is SqlInternalConnection sqlInternalConnection) + { + context = Interlocked.Exchange(ref sqlInternalConnection.CachedDataReaderIsDBNullContext, null); + } + if (context is null) + { + context = new IsDBNullAsyncCallContext(); + } Debug.Assert(context.Reader == null && context.Source == null && context.Disposable == default, "cached ISDBNullAsync context not properly disposed"); @@ -5517,7 +5541,10 @@ private static Task IsDBNullAsyncExecute(Task task, object state) private void SetCachedIDBNullAsyncCallContext(IsDBNullAsyncCallContext instance) { - Interlocked.CompareExchange(ref _cachedIsDBNullContext, instance, null); + if (_connection?.InnerConnection is SqlInternalConnection sqlInternalConnection) + { + Interlocked.CompareExchange(ref sqlInternalConnection.CachedDataReaderIsDBNullContext, instance, null); + } } /// @@ -6019,7 +6046,7 @@ private void CompleteAsyncCall(Task task, SqlDataReaderBaseAsyncCallContex } } - private sealed class Snapshot + internal sealed class Snapshot { public bool _dataReady; public bool _haltRead; @@ -6051,7 +6078,14 @@ private void PrepareAsyncInvocation(bool useSnapshot) if (_snapshot == null) { - _snapshot = new Snapshot(); + if (_connection?.InnerConnection is SqlInternalConnection sqlInternalConnection) + { + _snapshot = Interlocked.Exchange(ref sqlInternalConnection.CachedDataReaderSnapshot, null) ?? new Snapshot(); + } + else + { + _snapshot = new Snapshot(); + } _snapshot._dataReady = _sharedState._dataReady; _snapshot._haltRead = _haltRead; @@ -6124,6 +6158,10 @@ private void CleanupAfterAsyncInvocationInternal(TdsParserStateObject stateObj, stateObj._permitReplayStackTraceToDiffer = false; #endif + if (_connection?.InnerConnection is SqlInternalConnection sqlInternalConnection && sqlInternalConnection.CachedDataReaderSnapshot is null) + { + sqlInternalConnection.CachedDataReaderSnapshot = _snapshot; + } // We are setting this to null inside the if-statement because stateObj==null means that the reader hasn't been initialized or has been closed (either way _snapshot should already be null) _snapshot = null; } @@ -6162,6 +6200,10 @@ private void SwitchToAsyncWithoutSnapshot() Debug.Assert(_snapshot != null, "Should currently have a snapshot"); Debug.Assert(_stateObj != null && !_stateObj._asyncReadWithoutSnapshot, "Already in async without snapshot"); + if (_connection?.InnerConnection is SqlInternalConnection sqlInternalConnection && sqlInternalConnection.CachedDataReaderSnapshot is null) + { + sqlInternalConnection.CachedDataReaderSnapshot = _snapshot; + } _snapshot = null; _stateObj.ResetSnapshot(); _stateObj._asyncReadWithoutSnapshot = true; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs index d8950280ed..a39c83d831 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs @@ -31,11 +31,10 @@ internal abstract class SqlInternalConnection : DbConnectionInternal internal SqlCommand.ExecuteReaderAsyncCallContext CachedCommandExecuteReaderAsyncContext; internal SqlCommand.ExecuteNonQueryAsyncCallContext CachedCommandExecuteNonQueryAsyncContext; internal SqlCommand.ExecuteXmlReaderAsyncCallContext CachedCommandExecuteXmlReaderAsyncContext; - +#endif internal SqlDataReader.Snapshot CachedDataReaderSnapshot; internal SqlDataReader.IsDBNullAsyncCallContext CachedDataReaderIsDBNullContext; internal SqlDataReader.ReadAsyncCallContext CachedDataReaderReadAsyncContext; -#endif // if connection is not open: null // if connection is open: currently active database