diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.netcore.cs index 7e35e5a122..059c632eb1 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.netcore.cs @@ -15,6 +15,17 @@ namespace Microsoft.Data.SqlClient { internal abstract partial class TdsParserStateObject { + private struct RuntimeHelpers + { + /// + /// This is a no-op in netcore version. Only needed for merging with netfx codebase. + /// + [Conditional("NETFRAMEWORK")] + internal static void PrepareConstrainedRegions() + { + } + } + private static readonly ContextCallback s_readAsyncCallbackComplete = ReadAsyncCallbackComplete; // Timeout variables @@ -76,34 +87,6 @@ internal TdsParserStateObject(TdsParser parser, TdsParserStateObject physicalCon get; } - private partial struct NullBitmap - { - internal bool TryInitialize(TdsParserStateObject stateObj, int columnsCount) - { - _columnsCount = columnsCount; - // 1-8 columns need 1 byte - // 9-16: 2 bytes, and so on - int bitmapArrayLength = (columnsCount + 7) / 8; - - // allow reuse of previously allocated bitmap - if (_nullBitmap == null || _nullBitmap.Length != bitmapArrayLength) - { - _nullBitmap = new byte[bitmapArrayLength]; - } - - // read the null bitmap compression information from TDS - if (!stateObj.TryReadByteArray(_nullBitmap, _nullBitmap.Length)) - { - return false; - } - - SqlClientEventSource.Log.TryAdvancedTraceEvent("TdsParserStateObject.NullBitmap.Initialize | INFO | ADV | State Object Id {0}, NBCROW bitmap received, column count = {1}", stateObj.ObjectID, columnsCount); - SqlClientEventSource.Log.TryAdvancedTraceBinEvent("TdsParserStateObject.NullBitmap.Initialize | INFO | ADV | State Object Id {0}, NBCROW bitmap data. Null Bitmap {1}, Null bitmap length: {2}", stateObj.ObjectID, _nullBitmap, (ushort)_nullBitmap.Length); - - return true; - } - } - ///////////////////// // General methods // ///////////////////// @@ -278,568 +261,6 @@ internal void StartSession(object cancellationOwner) _cancellationOwner.Target = cancellationOwner; } - ///////////////////////////////////////// - // Value Skip Logic // - ///////////////////////////////////////// - - - // Reads bytes from the buffer but doesn't return them, in effect simply deleting them. - // Does not handle plp fields, need to use SkipPlpBytesValue for those. - // Does not handle null values or NBC bitmask, ensure the value is not null before calling this method - internal bool TrySkipLongBytes(long num) - { - Debug.Assert(_syncOverAsync || !_asyncReadWithoutSnapshot, "This method is not safe to call when doing sync over async"); - - while (num > 0) - { - int cbSkip = (int)Math.Min(int.MaxValue, num); - if (!TryReadByteArray(Span.Empty, cbSkip)) - { - return false; - } - num -= cbSkip; - } - - return true; - } - - // Reads bytes from the buffer but doesn't return them, in effect simply deleting them. - internal bool TrySkipBytes(int num) - { - Debug.Assert(_syncOverAsync || !_asyncReadWithoutSnapshot, "This method is not safe to call when doing sync over async"); - return TryReadByteArray(Span.Empty, num); - } - - ///////////////////////////////////////// - // Network/Packet Reading & Processing // - ///////////////////////////////////////// - -#if DEBUG - private string _lastStack; -#endif - - internal bool TryReadNetworkPacket() - { -#if DEBUG - Debug.Assert(!_shouldHaveEnoughData || _attentionSent, "Caller said there should be enough data, but we are currently reading a packet"); -#endif - - if (_snapshot != null) - { - if (_snapshotReplay) - { -#if DEBUG - // in debug builds stack traces contain line numbers so if we want to be - // able to compare the stack traces they must all be created in the same - // location in the code - string stackTrace = Environment.StackTrace; -#endif - if (_snapshot.MoveNext()) - { -#if DEBUG - if (s_checkNetworkPacketRetryStacks) - { - _snapshot.CheckStack(stackTrace); - } -#endif - return true; - } -#if DEBUG - else - { - if (s_checkNetworkPacketRetryStacks) - { - _lastStack = stackTrace; - } - } -#endif - } - - // previous buffer is in snapshot - _inBuff = new byte[_inBuff.Length]; - } - - if (_syncOverAsync) - { - ReadSniSyncOverAsync(); - return true; - } - - ReadSni(new TaskCompletionSource()); - -#if DEBUG - if (s_failAsyncPends) - { - throw new InvalidOperationException("Attempted to pend a read when s_failAsyncPends test hook was enabled"); - } - if (s_forceSyncOverAsyncAfterFirstPend) - { - _syncOverAsync = true; - } -#endif - Debug.Assert((_snapshot != null) ^ _asyncReadWithoutSnapshot, "Must have either _snapshot set up or _asyncReadWithoutSnapshot enabled (but not both) to pend a read"); - - return false; - } - - internal void PrepareReplaySnapshot() - { - _networkPacketTaskSource = null; - _snapshot.MoveToStart(); - } - - internal void ReadSniSyncOverAsync() - { - if (_parser.State == TdsParserState.Broken || _parser.State == TdsParserState.Closed) - { - throw ADP.ClosedConnectionError(); - } - - PacketHandle readPacket = default; - - uint error; - - bool shouldDecrement = false; - try - { - Interlocked.Increment(ref _readingCount); - shouldDecrement = true; - - readPacket = ReadSyncOverAsync(GetTimeoutRemaining(), out error); - - Interlocked.Decrement(ref _readingCount); - shouldDecrement = false; - - if (_parser.MARSOn) - { // Only take reset lock on MARS and Async. - CheckSetResetConnectionState(error, CallbackType.Read); - } - - if (TdsEnums.SNI_SUCCESS == error) - { // Success - process results! - - Debug.Assert(!IsPacketEmpty(readPacket), "ReadNetworkPacket cannot be null in synchronous operation!"); - - ProcessSniPacket(readPacket, 0); -#if DEBUG - if (s_forcePendingReadsToWaitForUser) - { - _networkPacketTaskSource = new TaskCompletionSource(); - Interlocked.MemoryBarrier(); - _networkPacketTaskSource.Task.Wait(); - _networkPacketTaskSource = null; - } -#endif - } - else - { // Failure! - - Debug.Assert(!IsValidPacket(readPacket), "unexpected readPacket without corresponding SNIPacketRelease"); - - ReadSniError(this, error); - } - } - finally - { - if (shouldDecrement) - { - Interlocked.Decrement(ref _readingCount); - } - - if (!IsPacketEmpty(readPacket)) - { - ReleasePacket(readPacket); - } - - AssertValidState(); - } - } - - internal void OnConnectionClosed() - { - // the stateObj is not null, so the async invocation that registered this callback - // via the SqlReferenceCollection has not yet completed. We will look for a - // _networkPacketTaskSource and mark it faulted. If we don't find it, then - // TdsParserStateObject.ReadSni will abort when it does look to see if the parser - // is open. If we do, then when the call that created it completes and a continuation - // is registered, we will ensure the completion is called. - - // Note, this effort is necessary because when the app domain is being unloaded, - // we don't get callback from SNI. - - // first mark parser broken. This is to ensure that ReadSni will abort if it has - // not yet executed. - Parser.State = TdsParserState.Broken; - Parser.Connection.BreakConnection(); - - // Ensure that changing state occurs before checking _networkPacketTaskSource - Interlocked.MemoryBarrier(); - - // then check for networkPacketTaskSource - TaskCompletionSource taskSource = _networkPacketTaskSource; - if (taskSource != null) - { - taskSource.TrySetException(ADP.ExceptionWithStackTrace(ADP.ClosedConnectionError())); - } - - taskSource = _writeCompletionSource; - if (taskSource != null) - { - taskSource.TrySetException(ADP.ExceptionWithStackTrace(ADP.ClosedConnectionError())); - } - } - - public void SetTimeoutStateStopped() - { - Interlocked.Exchange(ref _timeoutState, TimeoutState.Stopped); - _timeoutIdentityValue = 0; - } - - public bool IsTimeoutStateExpired - { - get - { - int state = _timeoutState; - return state == TimeoutState.ExpiredAsync || state == TimeoutState.ExpiredSync; - } - } - - private void OnTimeoutAsync(object state) - { - if (_enforceTimeoutDelay) - { - Thread.Sleep(_enforcedTimeoutDelayInMilliSeconds); - } - - int currentIdentityValue = _timeoutIdentityValue; - TimeoutState timeoutState = (TimeoutState)state; - if (timeoutState.IdentityValue == _timeoutIdentityValue) - { - // the return value is not useful here because no choice is going to be made using it - // we only want to make this call to set the state knowing that it will be seen later - OnTimeoutCore(TimeoutState.Running, TimeoutState.ExpiredAsync); - } - else - { - Debug.WriteLine($"OnTimeoutAsync called with identity state={timeoutState.IdentityValue} but current identity is {currentIdentityValue} so it is being ignored"); - } - } - - private bool OnTimeoutSync(bool asyncClose = false) - { - return OnTimeoutCore(TimeoutState.Running, TimeoutState.ExpiredSync, asyncClose); - } - - /// - /// attempts to change the timout state from the expected state to the target state and if it succeeds - /// will setup the the stateobject into the timeout expired state - /// - /// the state that is the expected current state, state will change only if this is correct - /// the state that will be changed to if the expected state is correct - /// any close action to be taken by an async task to avoid deadlock. - /// boolean value indicating whether the call changed the timeout state - private bool OnTimeoutCore(int expectedState, int targetState, bool asyncClose = false) - { - Debug.Assert(targetState == TimeoutState.ExpiredAsync || targetState == TimeoutState.ExpiredSync, "OnTimeoutCore must have an expiry state as the targetState"); - - bool retval = false; - if (Interlocked.CompareExchange(ref _timeoutState, targetState, expectedState) == expectedState) - { - retval = true; - // lock protects against Close and Cancel - lock (this) - { - if (!_attentionSent) - { - AddError(new SqlError(TdsEnums.TIMEOUT_EXPIRED, 0x00, TdsEnums.MIN_ERROR_CLASS, _parser.Server, _parser.Connection.TimeoutErrorInternal.GetErrorMessage(), "", 0, TdsEnums.SNI_WAIT_TIMEOUT)); - - // Grab a reference to the _networkPacketTaskSource in case it becomes null while we are trying to use it - TaskCompletionSource source = _networkPacketTaskSource; - - if (_parser.Connection.IsInPool) - { - // We should never timeout if the connection is currently in the pool: the safest thing to do here is to doom the connection to avoid corruption - Debug.Assert(_parser.Connection.IsConnectionDoomed, "Timeout occurred while the connection is in the pool"); - _parser.State = TdsParserState.Broken; - _parser.Connection.BreakConnection(); - if (source != null) - { - source.TrySetCanceled(); - } - } - else if (_parser.State == TdsParserState.OpenLoggedIn) - { - try - { - SendAttention(mustTakeWriteLock: true, asyncClose); - } - catch (Exception e) - { - if (!ADP.IsCatchableExceptionType(e)) - { - throw; - } - // if unable to send attention, cancel the _networkPacketTaskSource to - // request the parser be broken. SNIWritePacket errors will already - // be in the _errors collection. - if (source != null) - { - source.TrySetCanceled(); - } - } - } - - // If we still haven't received a packet then we don't want to actually close the connection - // from another thread, so complete the pending operation as cancelled, informing them to break it - if (source != null) - { - Task.Delay(AttentionTimeoutSeconds * 1000).ContinueWith(_ => - { - // Only break the connection if the read didn't finish - if (!source.Task.IsCompleted) - { - int pendingCallback = IncrementPendingCallbacks(); - try - { - // If pendingCallback is at 3, then ReadAsyncCallback hasn't been called yet - // So it is safe for us to break the connection and cancel the Task (since we are not sure that ReadAsyncCallback will ever be called) - if ((pendingCallback == 3) && (!source.Task.IsCompleted)) - { - Debug.Assert(source == _networkPacketTaskSource, "_networkPacketTaskSource which is being timed is not the current task source"); - - // Try to throw the timeout exception and store it in the task - bool exceptionStored = false; - try - { - CheckThrowSNIException(); - } - catch (Exception ex) - { - if (source.TrySetException(ex)) - { - exceptionStored = true; - } - } - - // Ensure that the connection is no longer usable - // This is needed since the timeout error added above is non-fatal (and so throwing it won't break the connection) - _parser.State = TdsParserState.Broken; - _parser.Connection.BreakConnection(); - - // If we didn't get an exception (something else observed it?) then ensure that the task is cancelled - if (!exceptionStored) - { - source.TrySetCanceled(); - } - } - } - finally - { - DecrementPendingCallbacks(release: false); - } - } - }); - } - } - } - } - return retval; - } - - internal void ReadSni(TaskCompletionSource completion) - { - Debug.Assert(_networkPacketTaskSource == null || ((_asyncReadWithoutSnapshot) && (_networkPacketTaskSource.Task.IsCompleted)), "Pending async call or failed to replay snapshot when calling ReadSni"); - _networkPacketTaskSource = completion; - - // Ensure that setting the completion source is completed before checking the state - Interlocked.MemoryBarrier(); - - // We must check after assigning _networkPacketTaskSource to avoid races with - // SqlCommand.OnConnectionClosed - if (_parser.State == TdsParserState.Broken || _parser.State == TdsParserState.Closed) - { - throw ADP.ClosedConnectionError(); - } - -#if DEBUG - if (s_forcePendingReadsToWaitForUser) - { - _realNetworkPacketTaskSource = new TaskCompletionSource(); - } -#endif - - - PacketHandle readPacket = default; - - uint error = 0; - - try - { - Debug.Assert(completion != null, "Async on but null asyncResult passed"); - - // if the state is currently stopped then change it to running and allocate a new identity value from - // the identity source. The identity value is used to correlate timer callback events to the currently - // running timeout and prevents a late timer callback affecting a result it does not relate to - int previousTimeoutState = Interlocked.CompareExchange(ref _timeoutState, TimeoutState.Running, TimeoutState.Stopped); - Debug.Assert(previousTimeoutState == TimeoutState.Stopped, "previous timeout state was not Stopped"); - if (previousTimeoutState == TimeoutState.Stopped) - { - Debug.Assert(_timeoutIdentityValue == 0, "timer was previously stopped without resetting the _identityValue"); - _timeoutIdentityValue = Interlocked.Increment(ref _timeoutIdentitySource); - } - - _networkPacketTimeout?.Dispose(); - - _networkPacketTimeout = ADP.UnsafeCreateTimer( - _onTimeoutAsync, - new TimeoutState(_timeoutIdentityValue), - Timeout.Infinite, - Timeout.Infinite - ); - - - // -1 == Infinite - // 0 == Already timed out (NOTE: To simulate the same behavior as sync we will only timeout on 0 if we receive an IO Pending from SNI) - // >0 == Actual timeout remaining - int msecsRemaining = GetTimeoutRemaining(); - if (msecsRemaining > 0) - { - ChangeNetworkPacketTimeout(msecsRemaining, Timeout.Infinite); - } - - Interlocked.Increment(ref _readingCount); - - SessionHandle handle = SessionHandle; - if (!handle.IsNull) - { - IncrementPendingCallbacks(); - - readPacket = ReadAsync(handle, out error); - - if (!(TdsEnums.SNI_SUCCESS == error || TdsEnums.SNI_SUCCESS_IO_PENDING == error)) - { - DecrementPendingCallbacks(false); // Failure - we won't receive callback! - } - } - - Interlocked.Decrement(ref _readingCount); - - if (handle.IsNull) - { - throw ADP.ClosedConnectionError(); - } - - if (TdsEnums.SNI_SUCCESS == error) - { // Success - process results! - Debug.Assert(IsValidPacket(readPacket), "ReadNetworkPacket should not have been null on this async operation!"); - // Evaluate this condition for MANAGED_SNI. This may not be needed because the network call is happening Async and only the callback can receive a success. - ReadAsyncCallback(IntPtr.Zero, readPacket, 0); - - // Only release packet for Managed SNI as for Native SNI packet is released in finally block. - if (TdsParserStateObjectFactory.UseManagedSNI && !IsPacketEmpty(readPacket)) - { - ReleasePacket(readPacket); - } - } - else if (TdsEnums.SNI_SUCCESS_IO_PENDING != error) - { // FAILURE! - Debug.Assert(IsPacketEmpty(readPacket), "unexpected readPacket without corresponding SNIPacketRelease"); - - ReadSniError(this, error); -#if DEBUG - if ((s_forcePendingReadsToWaitForUser) && (_realNetworkPacketTaskSource != null)) - { - _realNetworkPacketTaskSource.TrySetResult(null); - } - else -#endif - { - _networkPacketTaskSource.TrySetResult(null); - } - // Disable timeout timer on error - SetTimeoutStateStopped(); - ChangeNetworkPacketTimeout(Timeout.Infinite, Timeout.Infinite); - } - else if (msecsRemaining == 0) - { - // Got IO Pending, but we have no time left to wait - // disable the timer and set the error state by calling OnTimeoutSync - ChangeNetworkPacketTimeout(Timeout.Infinite, Timeout.Infinite); - OnTimeoutSync(); - } - // DO NOT HANDLE PENDING READ HERE - which is TdsEnums.SNI_SUCCESS_IO_PENDING state. - // That is handled by user who initiated async read, or by ReadNetworkPacket which is sync over async. - } - finally - { - if (!TdsParserStateObjectFactory.UseManagedSNI) - { - if (!IsPacketEmpty(readPacket)) - { - // Be sure to release packet, otherwise it will be leaked by native. - ReleasePacket(readPacket); - } - } - AssertValidState(); - } - } - - /// - /// Checks to see if the underlying connection is still alive (used by connection pool resiliency) - /// NOTE: This is not safe to do on a connection that is currently in use - /// NOTE: This will mark the connection as broken if it is found to be dead - /// - /// If true then an exception will be thrown if the connection is found to be dead, otherwise no exception will be thrown - /// True if the connection is still alive, otherwise false - internal bool IsConnectionAlive(bool throwOnException) - { - Debug.Assert(_parser.Connection == null || _parser.Connection.Pool != null, "Shouldn't be calling IsConnectionAlive on non-pooled connections"); - bool isAlive = true; - - if (DateTime.UtcNow.Ticks - _lastSuccessfulIOTimer._value > CheckConnectionWindow) - { - if ((_parser == null) || ((_parser.State == TdsParserState.Broken) || (_parser.State == TdsParserState.Closed))) - { - isAlive = false; - if (throwOnException) - { - throw SQL.ConnectionDoomed(); - } - } - else if ((_pendingCallbacks > 1) || ((_parser.Connection != null) && (!_parser.Connection.IsInPool))) - { - // This connection is currently in use, assume that the connection is 'alive' - // NOTE: SNICheckConnection is not currently supported for connections that are in use - Debug.Assert(true, "Call to IsConnectionAlive while connection is in use"); - } - else - { - uint error; - SniContext = SniContext.Snix_Connect; - - error = CheckConnection(); - if ((error != TdsEnums.SNI_SUCCESS) && (error != TdsEnums.SNI_WAIT_TIMEOUT)) - { - // Connection is dead - SqlClientEventSource.Log.TryTraceEvent("TdsParserStateObject.IsConnectionAlive | Info | State Object Id {0}, received error {1} on idle connection", _objectID, (int)error); - isAlive = false; - if (throwOnException) - { - // Get the error from SNI so that we can throw the correct exception - AddError(_parser.ProcessSNIError(this)); - ThrowExceptionAndWarning(); - } - } - else - { - _lastSuccessfulIOTimer._value = DateTime.UtcNow.Ticks; - } - } - } - - return isAlive; - } - /// /// Checks to see if the underlying connection is still valid (used by idle connection resiliency - for active connections) /// NOTE: This is not safe to do on a connection that is currently in use @@ -875,6 +296,8 @@ internal bool ValidateSNIConnection() // This method should only be called by ReadSni! If not - it may have problems with timeouts! private void ReadSniError(TdsParserStateObject stateObj, uint error) { + TdsParser.ReliabilitySection.Assert("unreliable call to ReadSniSyncError"); // you need to setup for a thread abort somewhere before you call this method + if (TdsEnums.SNI_WAIT_TIMEOUT == error) { Debug.Assert(_syncOverAsync, "Should never reach here with async on!"); @@ -897,7 +320,7 @@ private void ReadSniError(TdsParserStateObject stateObj, uint error) stateObj.SendAttention(mustTakeWriteLock: true); PacketHandle syncReadPacket = default; - + RuntimeHelpers.PrepareConstrainedRegions(); bool shouldDecrement = false; try { @@ -1097,6 +520,7 @@ public void ReadAsyncCallback(IntPtr key, PacketHandle packet, uint error) return; } + RuntimeHelpers.PrepareConstrainedRegions(); bool processFinallyBlock = true; try { @@ -1318,6 +742,8 @@ public void WriteAsyncCallback(IntPtr key, PacketHandle packet, uint sniError) // internal void WriteSecureString(SecureString secureString) { + TdsParser.ReliabilitySection.Assert("unreliable call to WriteSecureString"); // you need to setup for a thread abort somewhere before you call this method + Debug.Assert(_securePasswords[0] == null || _securePasswords[1] == null, "There are more than two secure passwords"); int index = _securePasswords[0] != null ? 1 : 0; @@ -1392,6 +818,8 @@ internal Task WaitForAccumulatedWrites() // and then the buffer is re-initialized in flush() and then the byte is put in the buffer. internal void WriteByte(byte b) { + TdsParser.ReliabilitySection.Assert("unreliable call to WriteByte"); // you need to setup for a thread abort somewhere before you call this method + Debug.Assert(_outBytesUsed <= _outBuff.Length, "ERROR - TDSParser: _outBytesUsed > _outBuff.Length"); // check to make sure we haven't used the full amount of space available in the buffer, if so, flush it @@ -1427,6 +855,8 @@ private Task WriteBytes(ReadOnlySpan b, int len, int offsetBuffer, bool ca } try { + TdsParser.ReliabilitySection.Assert("unreliable call to WriteByteArray"); // you need to setup for a thread abort somewhere before you call this method + bool async = _parser._asyncWrite; // NOTE: We are capturing this now for the assert after the Task is returned, since WritePacket will turn off async if there is an exception Debug.Assert(async || _asyncWriteCount == 0); // Do we have to send out in packet size chunks, or can we rely on netlib layer to break it up? @@ -1789,6 +1219,7 @@ internal void SendAttention(bool mustTakeWriteLock = false, bool asyncClose = fa PacketHandle attnPacket = CreateAndSetAttentionPacket(); + RuntimeHelpers.PrepareConstrainedRegions(); try { // Dev11 #344723: SqlClient stress test suspends System_Data!Tcp::ReadSync via a call to SqlDataReader::Close diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.netfx.cs index 7c7b9933db..5d4a332665 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.netfx.cs @@ -16,8 +16,27 @@ namespace Microsoft.Data.SqlClient { + using PacketHandle = IntPtr; + + internal readonly ref struct SessionHandle + { + public readonly SNIHandle NativeHandle; + + public SessionHandle(SNIHandle nativeHandle) => NativeHandle = nativeHandle; + + public bool IsNull => NativeHandle is null; + } + internal partial class TdsParserStateObject { + private static class TdsParserStateObjectFactory + { + /// + /// Always false in case of netfx. Only needed for merging with netcore codebase. + /// + internal const bool UseManagedSNI = false; + } + private SNIHandle _sessionHandle = null; // the SNI handle we're to work on // SNI variables // multiple resultsets in one batch. @@ -50,6 +69,7 @@ internal TdsParserStateObject(TdsParser parser, SNIHandle physicalConnection, bo // Construct a MARS session Debug.Assert(null != parser, "no parser?"); _parser = parser; + _onTimeoutAsync = OnTimeoutAsync; SniContext = SniContext.Snix_GetMarsSession; Debug.Assert(null != _parser._physicalStateObj, "no physical session?"); @@ -106,33 +126,7 @@ internal uint Status } } - private partial struct NullBitmap - { - internal bool TryInitialize(TdsParserStateObject stateObj, int columnsCount) - { - _columnsCount = columnsCount; - // 1-8 columns need 1 byte - // 9-16: 2 bytes, and so on - int bitmapArrayLength = (columnsCount + 7) / 8; - - // allow reuse of previously allocated bitmap - if (_nullBitmap == null || _nullBitmap.Length != bitmapArrayLength) - { - _nullBitmap = new byte[bitmapArrayLength]; - } - - // read the null bitmap compression information from TDS - if (!stateObj.TryReadByteArray(_nullBitmap, _nullBitmap.Length)) - { - return false; - } - - SqlClientEventSource.Log.TryAdvancedTraceEvent("TdsParserStateObject.NullBitmap.Initialize | INFO | ADV | State Object Id {0}, NBCROW bitmap received, column count = {1}", stateObj.ObjectID, columnsCount); - SqlClientEventSource.Log.TryAdvancedTraceBinEvent("TdsParserStateObject.NullBitmap.Initialize | INFO | ADV | State Object Id {0}, NBCROW bitmap data. Null Bitmap {1}, Null bitmap length: {2}", stateObj.ObjectID, _nullBitmap, (ushort)_nullBitmap.Length); - - return true; - } - } + internal SessionHandle SessionHandle => new SessionHandle(Handle); ///////////////////// // General methods // @@ -282,6 +276,27 @@ private SNINativeMethodWrapper.ConsumerInfo CreateConsumerInfo(bool async) ipPreference, cachedDNSInfo, hostNameInCertificate); } + internal bool IsPacketEmpty(PacketHandle readPacket) => readPacket == default; + + internal PacketHandle ReadSyncOverAsync(int timeoutRemaining, out uint error) + { + SNIHandle handle = Handle ?? throw ADP.ClosedConnectionError(); + PacketHandle readPacket = default; + error = SNINativeMethodWrapper.SNIReadSyncOverAsync(handle, ref readPacket, timeoutRemaining); + return readPacket; + } + + internal PacketHandle ReadAsync(SessionHandle handle, out uint error) + { + PacketHandle readPacket = default; + error = SNINativeMethodWrapper.SNIReadAsync(handle.NativeHandle, ref readPacket); + return readPacket; + } + + internal uint CheckConnection() => SNINativeMethodWrapper.SNICheckConnection(Handle); + + internal void ReleasePacket(PacketHandle syncReadPacket) => SNINativeMethodWrapper.SNIPacketRelease(syncReadPacket); + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] internal int DecrementPendingCallbacks(bool release) { @@ -372,594 +387,6 @@ internal void StartSession(int objectID) _allowObjectID = objectID; } - ///////////////////////////////////////// - // Value Skip Logic // - ///////////////////////////////////////// - - - // Reads bytes from the buffer but doesn't return them, in effect simply deleting them. - // Does not handle plp fields, need to use SkipPlpBytesValue for those. - // Does not handle null values or NBC bitmask, ensure the value is not null before calling this method - internal bool TrySkipLongBytes(long num) - { - Debug.Assert(_syncOverAsync || !_asyncReadWithoutSnapshot, "This method is not safe to call when doing sync over async"); - - while (num > 0) - { - int cbSkip = (int)Math.Min(int.MaxValue, num); - if (!TryReadByteArray(Span.Empty, cbSkip)) - { - return false; - } - num -= cbSkip; - } - - return true; - } - - // Reads bytes from the buffer but doesn't return them, in effect simply deleting them. - internal bool TrySkipBytes(int num) - { - Debug.Assert(_syncOverAsync || !_asyncReadWithoutSnapshot, "This method is not safe to call when doing sync over async"); - return TryReadByteArray(Span.Empty, num); - } - - ///////////////////////////////////////// - // Network/Packet Reading & Processing // - ///////////////////////////////////////// - -#if DEBUG - string _lastStack; -#endif - - internal bool TryReadNetworkPacket() - { - TdsParser.ReliabilitySection.Assert("unreliable call to TryReadNetworkPacket"); // you need to setup for a thread abort somewhere before you call this method - -#if DEBUG - Debug.Assert(!_shouldHaveEnoughData || _attentionSent, "Caller said there should be enough data, but we are currently reading a packet"); -#endif - - if (_snapshot != null) - { - if (_snapshotReplay) - { -#if DEBUG - // in debug builds stack traces contain line numbers so if we want to be - // able to compare the stack traces they must all be created in the same - // location in the code - string stackTrace = Environment.StackTrace; -#endif - if (_snapshot.MoveNext()) - { -#if DEBUG - if (s_checkNetworkPacketRetryStacks) - { - _snapshot.CheckStack(stackTrace); - } -#endif - return true; - } -#if DEBUG - else - { - if (s_checkNetworkPacketRetryStacks) - { - _lastStack = stackTrace; - } - } -#endif - } - - // previous buffer is in snapshot - _inBuff = new byte[_inBuff.Length]; - } - - if (_syncOverAsync) - { - ReadSniSyncOverAsync(); - return true; - } - - ReadSni(new TaskCompletionSource()); - -#if DEBUG - if (s_failAsyncPends) - { - throw new InvalidOperationException("Attempted to pend a read when s_failAsyncPends test hook was enabled"); - } - if (s_forceSyncOverAsyncAfterFirstPend) - { - _syncOverAsync = true; - } -#endif - Debug.Assert((_snapshot != null) ^ _asyncReadWithoutSnapshot, "Must have either _snapshot set up or _asyncReadWithoutSnapshot enabled (but not both) to pend a read"); - - return false; - } - - internal void PrepareReplaySnapshot() - { - _networkPacketTaskSource = null; - _snapshot.MoveToStart(); - } - - internal void ReadSniSyncOverAsync() - { - if (_parser.State == TdsParserState.Broken || _parser.State == TdsParserState.Closed) - { - throw ADP.ClosedConnectionError(); - } - - IntPtr readPacket = IntPtr.Zero; - uint error; - - RuntimeHelpers.PrepareConstrainedRegions(); - bool shouldDecrement = false; - try - { - TdsParser.ReliabilitySection.Assert("unreliable call to ReadSniSync"); // you need to setup for a thread abort somewhere before you call this method - - Interlocked.Increment(ref _readingCount); - shouldDecrement = true; - - SNIHandle handle = Handle; - if (handle == null) - { - throw ADP.ClosedConnectionError(); - } - - error = SNINativeMethodWrapper.SNIReadSyncOverAsync(handle, ref readPacket, GetTimeoutRemaining()); - - Interlocked.Decrement(ref _readingCount); - shouldDecrement = false; - - if (_parser.MARSOn) - { // Only take reset lock on MARS and Async. - CheckSetResetConnectionState(error, CallbackType.Read); - } - - if (TdsEnums.SNI_SUCCESS == error) - { // Success - process results! - Debug.Assert(ADP.s_ptrZero != readPacket, "ReadNetworkPacket cannot be null in synchronous operation!"); - ProcessSniPacket(readPacket, 0); -#if DEBUG - if (s_forcePendingReadsToWaitForUser) - { - _networkPacketTaskSource = new TaskCompletionSource(); - Thread.MemoryBarrier(); - _networkPacketTaskSource.Task.Wait(); - _networkPacketTaskSource = null; - } -#endif - } - else - { // Failure! - - Debug.Assert(IntPtr.Zero == readPacket, "unexpected readPacket without corresponding SNIPacketRelease"); - - ReadSniError(this, error); - } - } - finally - { - if (shouldDecrement) - { - Interlocked.Decrement(ref _readingCount); - } - - if (readPacket != IntPtr.Zero) - { - // Be sure to release packet, otherwise it will be leaked by native. - SNINativeMethodWrapper.SNIPacketRelease(readPacket); - } - - AssertValidState(); - } - } - - internal void OnConnectionClosed() - { - // the stateObj is not null, so the async invocation that registered this callback - // via the SqlReferenceCollection has not yet completed. We will look for a - // _networkPacketTaskSource and mark it faulted. If we don't find it, then - // TdsParserStateObject.ReadSni will abort when it does look to see if the parser - // is open. If we do, then when the call that created it completes and a continuation - // is registered, we will ensure the completion is called. - - // Note, this effort is necessary because when the app domain is being unloaded, - // we don't get callback from SNI. - - // first mark parser broken. This is to ensure that ReadSni will abort if it has - // not yet executed. - Parser.State = TdsParserState.Broken; - Parser.Connection.BreakConnection(); - - // Ensure that changing state occurs before checking _networkPacketTaskSource - Thread.MemoryBarrier(); - - // then check for networkPacketTaskSource - TaskCompletionSource taskSource = _networkPacketTaskSource; - if (taskSource != null) - { - taskSource.TrySetException(ADP.ExceptionWithStackTrace(ADP.ClosedConnectionError())); - } - - taskSource = _writeCompletionSource; - if (taskSource != null) - { - taskSource.TrySetException(ADP.ExceptionWithStackTrace(ADP.ClosedConnectionError())); - } - } - - public void SetTimeoutStateStopped() - { - Interlocked.Exchange(ref _timeoutState, TimeoutState.Stopped); - _timeoutIdentityValue = 0; - } - - public bool IsTimeoutStateExpired - { - get - { - int state = _timeoutState; - return state == TimeoutState.ExpiredAsync || state == TimeoutState.ExpiredSync; - } - } - - private void OnTimeoutAsync(object state) - { - if (_enforceTimeoutDelay) - { - Thread.Sleep(_enforcedTimeoutDelayInMilliSeconds); - } - - int currentIdentityValue = _timeoutIdentityValue; - TimeoutState timeoutState = (TimeoutState)state; - if (timeoutState.IdentityValue == _timeoutIdentityValue) - { - // the return value is not useful here because no choice is going to be made using it - // we only want to make this call to set the state knowing that it will be seen later - OnTimeoutCore(TimeoutState.Running, TimeoutState.ExpiredAsync); - } - else - { - Debug.WriteLine($"OnTimeoutAsync called with identity state={timeoutState.IdentityValue} but current identity is {currentIdentityValue} so it is being ignored"); - } - } - - private bool OnTimeoutSync(bool asyncClose = false) - { - return OnTimeoutCore(TimeoutState.Running, TimeoutState.ExpiredSync, asyncClose); - } - - /// - /// attempts to change the timout state from the expected state to the target state and if it succeeds - /// will setup the the stateobject into the timeout expired state - /// - /// the state that is the expected current state, state will change only if this is correct - /// the state that will be changed to if the expected state is correct - /// any close action to be taken by an async task to avoid deadlock. - /// boolean value indicating whether the call changed the timeout state - private bool OnTimeoutCore(int expectedState, int targetState, bool asyncClose = false) - { - Debug.Assert(targetState == TimeoutState.ExpiredAsync || targetState == TimeoutState.ExpiredSync, "OnTimeoutCore must have an expiry state as the targetState"); - - bool retval = false; - if (Interlocked.CompareExchange(ref _timeoutState, targetState, expectedState) == expectedState) - { - retval = true; - // lock protects against Close and Cancel - lock (this) - { - if (!_attentionSent) - { - AddError(new SqlError(TdsEnums.TIMEOUT_EXPIRED, 0x00, TdsEnums.MIN_ERROR_CLASS, _parser.Server, _parser.Connection.TimeoutErrorInternal.GetErrorMessage(), "", 0, TdsEnums.SNI_WAIT_TIMEOUT)); - - // Grab a reference to the _networkPacketTaskSource in case it becomes null while we are trying to use it - TaskCompletionSource source = _networkPacketTaskSource; - - if (_parser.Connection.IsInPool) - { - // We should never timeout if the connection is currently in the pool: the safest thing to do here is to doom the connection to avoid corruption - Debug.Assert(_parser.Connection.IsConnectionDoomed, "Timeout occurred while the connection is in the pool"); - _parser.State = TdsParserState.Broken; - _parser.Connection.BreakConnection(); - if (source != null) - { - source.TrySetCanceled(); - } - } - else if (_parser.State == TdsParserState.OpenLoggedIn) - { - try - { - SendAttention(mustTakeWriteLock: true, asyncClose); - } - catch (Exception e) - { - if (!ADP.IsCatchableExceptionType(e)) - { - throw; - } - // if unable to send attention, cancel the _networkPacketTaskSource to - // request the parser be broken. SNIWritePacket errors will already - // be in the _errors collection. - if (source != null) - { - source.TrySetCanceled(); - } - } - } - - // If we still haven't received a packet then we don't want to actually close the connection - // from another thread, so complete the pending operation as cancelled, informing them to break it - if (source != null) - { - Task.Delay(AttentionTimeoutSeconds * 1000).ContinueWith(_ => - { - // Only break the connection if the read didn't finish - if (!source.Task.IsCompleted) - { - int pendingCallback = IncrementPendingCallbacks(); - RuntimeHelpers.PrepareConstrainedRegions(); - try - { - // If pendingCallback is at 3, then ReadAsyncCallback hasn't been called yet - // So it is safe for us to break the connection and cancel the Task (since we are not sure that ReadAsyncCallback will ever be called) - if ((pendingCallback == 3) && (!source.Task.IsCompleted)) - { - Debug.Assert(source == _networkPacketTaskSource, "_networkPacketTaskSource which is being timed is not the current task source"); - - // Try to throw the timeout exception and store it in the task - bool exceptionStored = false; - try - { - CheckThrowSNIException(); - } - catch (Exception ex) - { - if (source.TrySetException(ex)) - { - exceptionStored = true; - } - } - - // Ensure that the connection is no longer usable - // This is needed since the timeout error added above is non-fatal (and so throwing it won't break the connection) - _parser.State = TdsParserState.Broken; - _parser.Connection.BreakConnection(); - - // If we didn't get an exception (something else observed it?) then ensure that the task is cancelled - if (!exceptionStored) - { - source.TrySetCanceled(); - } - } - } - finally - { - DecrementPendingCallbacks(release: false); - } - } - }); - } - } - } - } - return retval; - } - - internal void ReadSni(TaskCompletionSource completion) - { - Debug.Assert(_networkPacketTaskSource == null || ((_asyncReadWithoutSnapshot) && (_networkPacketTaskSource.Task.IsCompleted)), "Pending async call or failed to replay snapshot when calling ReadSni"); - _networkPacketTaskSource = completion; - - // Ensure that setting the completion source is completed before checking the state - Thread.MemoryBarrier(); - - // We must check after assigning _networkPacketTaskSource to avoid races with - // SqlCommand.OnConnectionClosed - if (_parser.State == TdsParserState.Broken || _parser.State == TdsParserState.Closed) - { - throw ADP.ClosedConnectionError(); - } - -#if DEBUG - if (s_forcePendingReadsToWaitForUser) - { - _realNetworkPacketTaskSource = new TaskCompletionSource(); - } -#endif - - IntPtr readPacket = IntPtr.Zero; - uint error = 0; - - RuntimeHelpers.PrepareConstrainedRegions(); - try - { - Debug.Assert(completion != null, "Async on but null asyncResult passed"); - - // if the state is currently stopped then change it to running and allocate a new identity value from - // the identity source. The identity value is used to correlate timer callback events to the currently - // running timeout and prevents a late timer callback affecting a result it does not relate to - int previousTimeoutState = Interlocked.CompareExchange(ref _timeoutState, TimeoutState.Running, TimeoutState.Stopped); - Debug.Assert(previousTimeoutState == TimeoutState.Stopped, "previous timeout state was not Stopped"); - if (previousTimeoutState == TimeoutState.Stopped) - { - Debug.Assert(_timeoutIdentityValue == 0, "timer was previously stopped without resetting the _identityValue"); - _timeoutIdentityValue = Interlocked.Increment(ref _timeoutIdentitySource); - } - - _networkPacketTimeout?.Dispose(); - - _networkPacketTimeout = new Timer( - new TimerCallback(OnTimeoutAsync), - new TimeoutState(_timeoutIdentityValue), - Timeout.Infinite, - Timeout.Infinite - ); - - // -1 == Infinite - // 0 == Already timed out (NOTE: To simulate the same behavior as sync we will only timeout on 0 if we receive an IO Pending from SNI) - // >0 == Actual timeout remaining - int msecsRemaining = GetTimeoutRemaining(); - if (msecsRemaining > 0) - { - ChangeNetworkPacketTimeout(msecsRemaining, Timeout.Infinite); - } - - SNIHandle handle = null; - - RuntimeHelpers.PrepareConstrainedRegions(); - try - { } - finally - { - Interlocked.Increment(ref _readingCount); - - handle = Handle; - if (handle != null) - { - - IncrementPendingCallbacks(); - - error = SNINativeMethodWrapper.SNIReadAsync(handle, ref readPacket); - - if (!(TdsEnums.SNI_SUCCESS == error || TdsEnums.SNI_SUCCESS_IO_PENDING == error)) - { - DecrementPendingCallbacks(false); // Failure - we won't receive callback! - } - } - - Interlocked.Decrement(ref _readingCount); - } - - if (handle == null) - { - throw ADP.ClosedConnectionError(); - } - - if (TdsEnums.SNI_SUCCESS == error) - { // Success - process results! - Debug.Assert(ADP.s_ptrZero != readPacket, "ReadNetworkPacket should not have been null on this async operation!"); - ReadAsyncCallback(ADP.s_ptrZero, readPacket, 0); - } - else if (TdsEnums.SNI_SUCCESS_IO_PENDING != error) - { // FAILURE! - Debug.Assert(IntPtr.Zero == readPacket, "unexpected readPacket without corresponding SNIPacketRelease"); - ReadSniError(this, error); -#if DEBUG - if ((s_forcePendingReadsToWaitForUser) && (_realNetworkPacketTaskSource != null)) - { - _realNetworkPacketTaskSource.TrySetResult(null); - } - else -#endif - { - _networkPacketTaskSource.TrySetResult(null); - } - // Disable timeout timer on error - SetTimeoutStateStopped(); - ChangeNetworkPacketTimeout(Timeout.Infinite, Timeout.Infinite); - } - else if (msecsRemaining == 0) - { - // Got IO Pending, but we have no time left to wait - // disable the timer and set the error state by calling OnTimeoutSync - ChangeNetworkPacketTimeout(Timeout.Infinite, Timeout.Infinite); - OnTimeoutSync(); - } - // DO NOT HANDLE PENDING READ HERE - which is TdsEnums.SNI_SUCCESS_IO_PENDING state. - // That is handled by user who initiated async read, or by ReadNetworkPacket which is sync over async. - } - finally - { - if (readPacket != IntPtr.Zero) - { - // Be sure to release packet, otherwise it will be leaked by native. - SNINativeMethodWrapper.SNIPacketRelease(readPacket); - } - - AssertValidState(); - } - } - - /// - /// Checks to see if the underlying connection is still alive (used by connection pool resiliency) - /// NOTE: This is not safe to do on a connection that is currently in use - /// NOTE: This will mark the connection as broken if it is found to be dead - /// - /// If true then an exception will be thrown if the connection is found to be dead, otherwise no exception will be thrown - /// True if the connection is still alive, otherwise false - internal bool IsConnectionAlive(bool throwOnException) - { - Debug.Assert(_parser.Connection == null || _parser.Connection.Pool != null, "Shouldn't be calling IsConnectionAlive on non-pooled connections"); - bool isAlive = true; - - if (DateTime.UtcNow.Ticks - _lastSuccessfulIOTimer._value > CheckConnectionWindow) - { - if ((_parser == null) || ((_parser.State == TdsParserState.Broken) || (_parser.State == TdsParserState.Closed))) - { - isAlive = false; - if (throwOnException) - { - throw SQL.ConnectionDoomed(); - } - } - else if ((_pendingCallbacks > 1) || ((_parser.Connection != null) && (!_parser.Connection.IsInPool))) - { - // This connection is currently in use, assume that the connection is 'alive' - // NOTE: SNICheckConnection is not currently supported for connections that are in use - Debug.Assert(true, "Call to IsConnectionAlive while connection is in use"); - } - else - { - uint error; - IntPtr readPacket = IntPtr.Zero; - - RuntimeHelpers.PrepareConstrainedRegions(); - try - { - TdsParser.ReliabilitySection.Assert("unreliable call to IsConnectionAlive"); // you need to setup for a thread abort somewhere before you call this method - - - SniContext = SniContext.Snix_Connect; - error = SNINativeMethodWrapper.SNICheckConnection(Handle); - - if ((error != TdsEnums.SNI_SUCCESS) && (error != TdsEnums.SNI_WAIT_TIMEOUT)) - { - // Connection is dead - SqlClientEventSource.Log.TryTraceEvent("TdsParserStateObject.IsConnectionAlive | Info | State Object Id {0}, received error {1} on idle connection", _objectID, (int)error); - - isAlive = false; - if (throwOnException) - { - // Get the error from SNI so that we can throw the correct exception - AddError(_parser.ProcessSNIError(this)); - ThrowExceptionAndWarning(); - } - } - else - { - _lastSuccessfulIOTimer._value = DateTime.UtcNow.Ticks; - } - } - finally - { - if (readPacket != IntPtr.Zero) - { - // Be sure to release packet, otherwise it will be leaked by native. - SNINativeMethodWrapper.SNIPacketRelease(readPacket); - } - - } - } - } - - return isAlive; - } - /// /// Checks to see if the underlying connection is still valid (used by idle connection resiliency - for active connections) /// NOTE: This is not safe to do on a connection that is currently in use @@ -1022,7 +449,7 @@ private void ReadSniError(TdsParserStateObject stateObj, uint error) { stateObj.SendAttention(mustTakeWriteLock: true); - IntPtr syncReadPacket = IntPtr.Zero; + PacketHandle syncReadPacket = default; RuntimeHelpers.PrepareConstrainedRegions(); bool shouldDecrement = false; try @@ -1030,13 +457,7 @@ private void ReadSniError(TdsParserStateObject stateObj, uint error) Interlocked.Increment(ref _readingCount); shouldDecrement = true; - SNIHandle handle = Handle; - if (handle == null) - { - throw ADP.ClosedConnectionError(); - } - - error = SNINativeMethodWrapper.SNIReadSyncOverAsync(handle, ref syncReadPacket, stateObj.GetTimeoutRemaining()); + syncReadPacket = ReadSyncOverAsync(stateObj.GetTimeoutRemaining(), out error); Interlocked.Decrement(ref _readingCount); shouldDecrement = false; @@ -1049,7 +470,7 @@ private void ReadSniError(TdsParserStateObject stateObj, uint error) } else { - Debug.Assert(IntPtr.Zero == syncReadPacket, "unexpected syncReadPacket without corresponding SNIPacketRelease"); + Debug.Assert(!IsValidPacket(syncReadPacket), "unexpected syncReadPacket without corresponding SNIPacketRelease"); fail = true; // Subsequent read failed, time to give up. } } @@ -1060,10 +481,10 @@ private void ReadSniError(TdsParserStateObject stateObj, uint error) Interlocked.Decrement(ref _readingCount); } - if (syncReadPacket != IntPtr.Zero) + if (!IsPacketEmpty(syncReadPacket)) { // Be sure to release packet, otherwise it will be leaked by native. - SNINativeMethodWrapper.SNIPacketRelease(syncReadPacket); + ReleasePacket(syncReadPacket); } } } @@ -1101,7 +522,7 @@ private void ReadSniError(TdsParserStateObject stateObj, uint error) AssertValidState(); } - public void ProcessSniPacket(IntPtr packet, uint error) + public void ProcessSniPacket(PacketHandle packet, uint error) { if (error != 0) { @@ -1174,7 +595,7 @@ private void ChangeNetworkPacketTimeout(int dueTime, int period) } } - public void ReadAsyncCallback(IntPtr key, IntPtr packet, uint error) + public void ReadAsyncCallback(IntPtr key, PacketHandle packet, uint error) { // Key never used. // Note - it's possible that when native calls managed that an asynchronous exception @@ -1319,7 +740,7 @@ private void ReadAsyncCallbackCaptureException(TaskCompletionSource sour #pragma warning disable 420 // a reference to a volatile field will not be treated as volatile - public void WriteAsyncCallback(IntPtr key, IntPtr packet, uint sniError) + public void WriteAsyncCallback(IntPtr key, PacketHandle packet, uint sniError) { // Key never used. RemovePacketFromPendingList(packet); try @@ -1344,7 +765,7 @@ public void WriteAsyncCallback(IntPtr key, IntPtr packet, uint sniError) _delayedWriteAsyncCallbackException = e; // Ensure that _delayedWriteAsyncCallbackException is set before checking _writeCompletionSource - Thread.MemoryBarrier(); + Interlocked.MemoryBarrier(); // Double check that _writeCompletionSource hasn't been created in the meantime writeCompletionSource = _writeCompletionSource; @@ -1465,7 +886,7 @@ internal Task WaitForAccumulatedWrites() Task task = _writeCompletionSource.Task; // Ensure that _writeCompletionSource is set before checking state - Thread.MemoryBarrier(); + Interlocked.MemoryBarrier(); // Now that we have set _writeCompletionSource, check if parser is closed or broken if ((_parser.State == TdsParserState.Closed) || (_parser.State == TdsParserState.Broken)) @@ -1714,7 +1135,7 @@ private Task SNIWritePacket(SNIHandle handle, SNIPacket packet, out uint sniErro Task task = null; _writeCompletionSource = null; - IntPtr packetPointer = IntPtr.Zero; + PacketHandle packetPointer = EmptyReadPacket; bool sync = !_parser._asyncWrite; if (sync && _asyncWriteCount > 0) { // for example, SendAttention while there are writes pending @@ -1760,7 +1181,7 @@ private Task SNIWritePacket(SNIHandle handle, SNIPacket packet, out uint sniErro task = _writeCompletionSource.Task; // Ensure that setting _writeCompletionSource completes before checking _delayedWriteAsyncCallbackException - Thread.MemoryBarrier(); + Interlocked.MemoryBarrier(); // Check for a stored exception delayedException = Interlocked.Exchange(ref _delayedWriteAsyncCallbackException, null); @@ -1822,7 +1243,7 @@ private Task SNIWritePacket(SNIHandle handle, SNIPacket packet, out uint sniErro if (!sync) { // Since there will be no callback, remove the packet from the pending list - Debug.Assert(packetPointer != IntPtr.Zero, "Packet added to list has an invalid pointer, can not remove from pending list"); + Debug.Assert(IsValidPacket(packetPointer), "Packet added to list has an invalid pointer, can not remove from pending list"); RemovePacketFromPendingList(packetPointer); } } @@ -1837,7 +1258,9 @@ private Task SNIWritePacket(SNIHandle handle, SNIPacket packet, out uint sniErro return task; } -#pragma warning restore 420 +#pragma warning restore 420 + + internal bool IsValidPacket(PacketHandle packetPointer) => packetPointer != default; // Sends an attention signal - executing thread will consume attn. internal void SendAttention(bool mustTakeWriteLock = false, bool asyncClose = false) @@ -2210,6 +1633,8 @@ internal int WarningCount } } + protected PacketHandle EmptyReadPacket => default; + internal int PreAttentionErrorCount { get diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs index 8d16876824..1ded2eef3a 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs @@ -128,6 +128,31 @@ internal static Exception ExceptionWithStackTrace(Exception e) } } + internal static Timer UnsafeCreateTimer(TimerCallback callback, object state, int dueTime, int period) + { + // Don't capture the current ExecutionContext and its AsyncLocals onto + // a global timer causing them to live forever + bool restoreFlow = false; + try + { + if (!ExecutionContext.IsFlowSuppressed()) + { + ExecutionContext.SuppressFlow(); + restoreFlow = true; + } + + return new Timer(callback, state, dueTime, period); + } + finally + { + // Restore the current ExecutionContext + if (restoreFlow) + { + ExecutionContext.RestoreFlow(); + } + } + } + #region COM+ exceptions internal static ArgumentException Argument(string error) { @@ -1516,28 +1541,6 @@ internal static IntPtr IntPtrOffset(IntPtr pbase, int offset) #endregion #else #region netcore project only - internal static Timer UnsafeCreateTimer(TimerCallback callback, object state, int dueTime, int period) - { - // Don't capture the current ExecutionContext and its AsyncLocals onto - // a global timer causing them to live forever - bool restoreFlow = false; - try - { - if (!ExecutionContext.IsFlowSuppressed()) - { - ExecutionContext.SuppressFlow(); - restoreFlow = true; - } - - return new Timer(callback, state, dueTime, period); - } - finally - { - // Restore the current ExecutionContext - if (restoreFlow) - ExecutionContext.RestoreFlow(); - } - } // // COM+ exceptions diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs index 0e6655a8b2..805f0c75d9 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs @@ -13,6 +13,11 @@ namespace Microsoft.Data.SqlClient { +#if NETFRAMEWORK + using PacketHandle = IntPtr; + using RuntimeHelpers = System.Runtime.CompilerServices.RuntimeHelpers; +#endif + sealed internal class LastIOTimer { internal long _value; @@ -560,11 +565,36 @@ internal bool IsNullCompressionBitSet(int columnOrdinal) return _nullBitmapInfo.IsGuaranteedNull(columnOrdinal); } - private partial struct NullBitmap + private struct NullBitmap { private byte[] _nullBitmap; private int _columnsCount; // set to 0 if not used or > 0 for NBC rows + internal bool TryInitialize(TdsParserStateObject stateObj, int columnsCount) + { + _columnsCount = columnsCount; + // 1-8 columns need 1 byte + // 9-16: 2 bytes, and so on + int bitmapArrayLength = (columnsCount + 7) / 8; + + // allow reuse of previously allocated bitmap + if (_nullBitmap == null || _nullBitmap.Length != bitmapArrayLength) + { + _nullBitmap = new byte[bitmapArrayLength]; + } + + // read the null bitmap compression information from TDS + if (!stateObj.TryReadByteArray(_nullBitmap, _nullBitmap.Length)) + { + return false; + } + + SqlClientEventSource.Log.TryAdvancedTraceEvent("TdsParserStateObject.NullBitmap.Initialize | INFO | ADV | State Object Id {0}, NBCROW bitmap received, column count = {1}", stateObj.ObjectID, columnsCount); + SqlClientEventSource.Log.TryAdvancedTraceBinEvent("TdsParserStateObject.NullBitmap.Initialize | INFO | ADV | State Object Id {0}, NBCROW bitmap data. Null Bitmap {1}, Null bitmap length: {2}", stateObj.ObjectID, _nullBitmap, (ushort)_nullBitmap.Length); + + return true; + } + internal bool ReferenceEquals(NullBitmap obj) { return object.ReferenceEquals(_nullBitmap, obj._nullBitmap); @@ -1864,6 +1894,581 @@ internal bool TryReadPlpBytes(ref byte[] buff, int offset, int len, out int tota return true; } + ///////////////////////////////////////// + // Value Skip Logic // + ///////////////////////////////////////// + + // Reads bytes from the buffer but doesn't return them, in effect simply deleting them. + // Does not handle plp fields, need to use SkipPlpBytesValue for those. + // Does not handle null values or NBC bitmask, ensure the value is not null before calling this method + internal bool TrySkipLongBytes(long num) + { + Debug.Assert(_syncOverAsync || !_asyncReadWithoutSnapshot, "This method is not safe to call when doing sync over async"); + + while (num > 0) + { + int cbSkip = (int)Math.Min(int.MaxValue, num); + if (!TryReadByteArray(Span.Empty, cbSkip)) + { + return false; + } + num -= cbSkip; + } + + return true; + } + + // Reads bytes from the buffer but doesn't return them, in effect simply deleting them. + internal bool TrySkipBytes(int num) + { + Debug.Assert(_syncOverAsync || !_asyncReadWithoutSnapshot, "This method is not safe to call when doing sync over async"); + return TryReadByteArray(Span.Empty, num); + } + + ///////////////////////////////////////// + // Network/Packet Reading & Processing // + ///////////////////////////////////////// + +#if DEBUG + private string _lastStack; +#endif + + internal bool TryReadNetworkPacket() + { + TdsParser.ReliabilitySection.Assert("unreliable call to TryReadNetworkPacket"); // you need to setup for a thread abort somewhere before you call this method + +#if DEBUG + Debug.Assert(!_shouldHaveEnoughData || _attentionSent, "Caller said there should be enough data, but we are currently reading a packet"); +#endif + + if (_snapshot != null) + { + if (_snapshotReplay) + { +#if DEBUG + // in debug builds stack traces contain line numbers so if we want to be + // able to compare the stack traces they must all be created in the same + // location in the code + string stackTrace = Environment.StackTrace; +#endif + if (_snapshot.MoveNext()) + { +#if DEBUG + if (s_checkNetworkPacketRetryStacks) + { + _snapshot.CheckStack(stackTrace); + } +#endif + return true; + } +#if DEBUG + else + { + if (s_checkNetworkPacketRetryStacks) + { + _lastStack = stackTrace; + } + } +#endif + } + + // previous buffer is in snapshot + _inBuff = new byte[_inBuff.Length]; + } + + if (_syncOverAsync) + { + ReadSniSyncOverAsync(); + return true; + } + + ReadSni(new TaskCompletionSource()); + +#if DEBUG + if (s_failAsyncPends) + { + throw new InvalidOperationException("Attempted to pend a read when s_failAsyncPends test hook was enabled"); + } + if (s_forceSyncOverAsyncAfterFirstPend) + { + _syncOverAsync = true; + } +#endif + Debug.Assert((_snapshot != null) ^ _asyncReadWithoutSnapshot, "Must have either _snapshot set up or _asyncReadWithoutSnapshot enabled (but not both) to pend a read"); + + return false; + } + + internal void PrepareReplaySnapshot() + { + _networkPacketTaskSource = null; + _snapshot.MoveToStart(); + } + + internal void ReadSniSyncOverAsync() + { + if (_parser.State == TdsParserState.Broken || _parser.State == TdsParserState.Closed) + { + throw ADP.ClosedConnectionError(); + } + + PacketHandle readPacket = default; + + uint error; + + RuntimeHelpers.PrepareConstrainedRegions(); + bool shouldDecrement = false; + try + { + TdsParser.ReliabilitySection.Assert("unreliable call to ReadSniSync"); // you need to setup for a thread abort somewhere before you call this method + + Interlocked.Increment(ref _readingCount); + shouldDecrement = true; + + readPacket = ReadSyncOverAsync(GetTimeoutRemaining(), out error); + + Interlocked.Decrement(ref _readingCount); + shouldDecrement = false; + + if (_parser.MARSOn) + { // Only take reset lock on MARS and Async. + CheckSetResetConnectionState(error, CallbackType.Read); + } + + if (TdsEnums.SNI_SUCCESS == error) + { // Success - process results! + + Debug.Assert(!IsPacketEmpty(readPacket), "ReadNetworkPacket cannot be null in synchronous operation!"); + + ProcessSniPacket(readPacket, 0); +#if DEBUG + if (s_forcePendingReadsToWaitForUser) + { + _networkPacketTaskSource = new TaskCompletionSource(); + Interlocked.MemoryBarrier(); + _networkPacketTaskSource.Task.Wait(); + _networkPacketTaskSource = null; + } +#endif + } + else + { // Failure! + + Debug.Assert(!IsValidPacket(readPacket), "unexpected readPacket without corresponding SNIPacketRelease"); + + ReadSniError(this, error); + } + } + finally + { + if (shouldDecrement) + { + Interlocked.Decrement(ref _readingCount); + } + + if (!IsPacketEmpty(readPacket)) + { + ReleasePacket(readPacket); + } + + AssertValidState(); + } + } + + internal void OnConnectionClosed() + { + // the stateObj is not null, so the async invocation that registered this callback + // via the SqlReferenceCollection has not yet completed. We will look for a + // _networkPacketTaskSource and mark it faulted. If we don't find it, then + // TdsParserStateObject.ReadSni will abort when it does look to see if the parser + // is open. If we do, then when the call that created it completes and a continuation + // is registered, we will ensure the completion is called. + + // Note, this effort is necessary because when the app domain is being unloaded, + // we don't get callback from SNI. + + // first mark parser broken. This is to ensure that ReadSni will abort if it has + // not yet executed. + Parser.State = TdsParserState.Broken; + Parser.Connection.BreakConnection(); + + // Ensure that changing state occurs before checking _networkPacketTaskSource + Interlocked.MemoryBarrier(); + + // then check for networkPacketTaskSource + TaskCompletionSource taskSource = _networkPacketTaskSource; + if (taskSource != null) + { + taskSource.TrySetException(ADP.ExceptionWithStackTrace(ADP.ClosedConnectionError())); + } + + taskSource = _writeCompletionSource; + if (taskSource != null) + { + taskSource.TrySetException(ADP.ExceptionWithStackTrace(ADP.ClosedConnectionError())); + } + } + + public void SetTimeoutStateStopped() + { + Interlocked.Exchange(ref _timeoutState, TimeoutState.Stopped); + _timeoutIdentityValue = 0; + } + + public bool IsTimeoutStateExpired + { + get + { + int state = _timeoutState; + return state == TimeoutState.ExpiredAsync || state == TimeoutState.ExpiredSync; + } + } + + private void OnTimeoutAsync(object state) + { + if (_enforceTimeoutDelay) + { + Thread.Sleep(_enforcedTimeoutDelayInMilliSeconds); + } + + int currentIdentityValue = _timeoutIdentityValue; + TimeoutState timeoutState = (TimeoutState)state; + if (timeoutState.IdentityValue == _timeoutIdentityValue) + { + // the return value is not useful here because no choice is going to be made using it + // we only want to make this call to set the state knowing that it will be seen later + OnTimeoutCore(TimeoutState.Running, TimeoutState.ExpiredAsync); + } + else + { + Debug.WriteLine($"OnTimeoutAsync called with identity state={timeoutState.IdentityValue} but current identity is {currentIdentityValue} so it is being ignored"); + } + } + + private bool OnTimeoutSync(bool asyncClose = false) + { + return OnTimeoutCore(TimeoutState.Running, TimeoutState.ExpiredSync, asyncClose); + } + + /// + /// attempts to change the timout state from the expected state to the target state and if it succeeds + /// will setup the the stateobject into the timeout expired state + /// + /// the state that is the expected current state, state will change only if this is correct + /// the state that will be changed to if the expected state is correct + /// any close action to be taken by an async task to avoid deadlock. + /// boolean value indicating whether the call changed the timeout state + private bool OnTimeoutCore(int expectedState, int targetState, bool asyncClose = false) + { + Debug.Assert(targetState == TimeoutState.ExpiredAsync || targetState == TimeoutState.ExpiredSync, "OnTimeoutCore must have an expiry state as the targetState"); + + bool retval = false; + if (Interlocked.CompareExchange(ref _timeoutState, targetState, expectedState) == expectedState) + { + retval = true; + // lock protects against Close and Cancel + lock (this) + { + if (!_attentionSent) + { + AddError(new SqlError(TdsEnums.TIMEOUT_EXPIRED, 0x00, TdsEnums.MIN_ERROR_CLASS, _parser.Server, _parser.Connection.TimeoutErrorInternal.GetErrorMessage(), "", 0, TdsEnums.SNI_WAIT_TIMEOUT)); + + // Grab a reference to the _networkPacketTaskSource in case it becomes null while we are trying to use it + TaskCompletionSource source = _networkPacketTaskSource; + + if (_parser.Connection.IsInPool) + { + // We should never timeout if the connection is currently in the pool: the safest thing to do here is to doom the connection to avoid corruption + Debug.Assert(_parser.Connection.IsConnectionDoomed, "Timeout occurred while the connection is in the pool"); + _parser.State = TdsParserState.Broken; + _parser.Connection.BreakConnection(); + if (source != null) + { + source.TrySetCanceled(); + } + } + else if (_parser.State == TdsParserState.OpenLoggedIn) + { + try + { + SendAttention(mustTakeWriteLock: true, asyncClose); + } + catch (Exception e) + { + if (!ADP.IsCatchableExceptionType(e)) + { + throw; + } + // if unable to send attention, cancel the _networkPacketTaskSource to + // request the parser be broken. SNIWritePacket errors will already + // be in the _errors collection. + if (source != null) + { + source.TrySetCanceled(); + } + } + } + + // If we still haven't received a packet then we don't want to actually close the connection + // from another thread, so complete the pending operation as cancelled, informing them to break it + if (source != null) + { + Task.Delay(AttentionTimeoutSeconds * 1000).ContinueWith(_ => + { + // Only break the connection if the read didn't finish + if (!source.Task.IsCompleted) + { + int pendingCallback = IncrementPendingCallbacks(); + RuntimeHelpers.PrepareConstrainedRegions(); + try + { + // If pendingCallback is at 3, then ReadAsyncCallback hasn't been called yet + // So it is safe for us to break the connection and cancel the Task (since we are not sure that ReadAsyncCallback will ever be called) + if ((pendingCallback == 3) && (!source.Task.IsCompleted)) + { + Debug.Assert(source == _networkPacketTaskSource, "_networkPacketTaskSource which is being timed is not the current task source"); + + // Try to throw the timeout exception and store it in the task + bool exceptionStored = false; + try + { + CheckThrowSNIException(); + } + catch (Exception ex) + { + if (source.TrySetException(ex)) + { + exceptionStored = true; + } + } + + // Ensure that the connection is no longer usable + // This is needed since the timeout error added above is non-fatal (and so throwing it won't break the connection) + _parser.State = TdsParserState.Broken; + _parser.Connection.BreakConnection(); + + // If we didn't get an exception (something else observed it?) then ensure that the task is cancelled + if (!exceptionStored) + { + source.TrySetCanceled(); + } + } + } + finally + { + DecrementPendingCallbacks(release: false); + } + } + }); + } + } + } + } + return retval; + } + + internal void ReadSni(TaskCompletionSource completion) + { + Debug.Assert(_networkPacketTaskSource == null || ((_asyncReadWithoutSnapshot) && (_networkPacketTaskSource.Task.IsCompleted)), "Pending async call or failed to replay snapshot when calling ReadSni"); + _networkPacketTaskSource = completion; + + // Ensure that setting the completion source is completed before checking the state + Interlocked.MemoryBarrier(); + + // We must check after assigning _networkPacketTaskSource to avoid races with + // SqlCommand.OnConnectionClosed + if (_parser.State == TdsParserState.Broken || _parser.State == TdsParserState.Closed) + { + throw ADP.ClosedConnectionError(); + } + +#if DEBUG + if (s_forcePendingReadsToWaitForUser) + { + _realNetworkPacketTaskSource = new TaskCompletionSource(); + } +#endif + + PacketHandle readPacket = default; + + uint error = 0; + + RuntimeHelpers.PrepareConstrainedRegions(); + try + { + Debug.Assert(completion != null, "Async on but null asyncResult passed"); + + // if the state is currently stopped then change it to running and allocate a new identity value from + // the identity source. The identity value is used to correlate timer callback events to the currently + // running timeout and prevents a late timer callback affecting a result it does not relate to + int previousTimeoutState = Interlocked.CompareExchange(ref _timeoutState, TimeoutState.Running, TimeoutState.Stopped); + Debug.Assert(previousTimeoutState == TimeoutState.Stopped, "previous timeout state was not Stopped"); + if (previousTimeoutState == TimeoutState.Stopped) + { + Debug.Assert(_timeoutIdentityValue == 0, "timer was previously stopped without resetting the _identityValue"); + _timeoutIdentityValue = Interlocked.Increment(ref _timeoutIdentitySource); + } + + _networkPacketTimeout?.Dispose(); + + _networkPacketTimeout = ADP.UnsafeCreateTimer( + _onTimeoutAsync, + new TimeoutState(_timeoutIdentityValue), + Timeout.Infinite, + Timeout.Infinite + ); + + // -1 == Infinite + // 0 == Already timed out (NOTE: To simulate the same behavior as sync we will only timeout on 0 if we receive an IO Pending from SNI) + // >0 == Actual timeout remaining + int msecsRemaining = GetTimeoutRemaining(); + if (msecsRemaining > 0) + { + ChangeNetworkPacketTimeout(msecsRemaining, Timeout.Infinite); + } + + SessionHandle handle = default; + + RuntimeHelpers.PrepareConstrainedRegions(); + try + { } + finally + { + Interlocked.Increment(ref _readingCount); + + handle = SessionHandle; + if (!handle.IsNull) + { + IncrementPendingCallbacks(); + + readPacket = ReadAsync(handle, out error); + + if (!(TdsEnums.SNI_SUCCESS == error || TdsEnums.SNI_SUCCESS_IO_PENDING == error)) + { + DecrementPendingCallbacks(false); // Failure - we won't receive callback! + } + } + + Interlocked.Decrement(ref _readingCount); + } + + if (handle.IsNull) + { + throw ADP.ClosedConnectionError(); + } + + if (TdsEnums.SNI_SUCCESS == error) + { // Success - process results! + Debug.Assert(IsValidPacket(readPacket), "ReadNetworkPacket should not have been null on this async operation!"); + // Evaluate this condition for MANAGED_SNI. This may not be needed because the network call is happening Async and only the callback can receive a success. + ReadAsyncCallback(IntPtr.Zero, readPacket, 0); + + // Only release packet for Managed SNI as for Native SNI packet is released in finally block. + if (TdsParserStateObjectFactory.UseManagedSNI && !IsPacketEmpty(readPacket)) + { + ReleasePacket(readPacket); + } + } + else if (TdsEnums.SNI_SUCCESS_IO_PENDING != error) + { // FAILURE! + Debug.Assert(IsPacketEmpty(readPacket), "unexpected readPacket without corresponding SNIPacketRelease"); + + ReadSniError(this, error); +#if DEBUG + if ((s_forcePendingReadsToWaitForUser) && (_realNetworkPacketTaskSource != null)) + { + _realNetworkPacketTaskSource.TrySetResult(null); + } + else +#endif + { + _networkPacketTaskSource.TrySetResult(null); + } + // Disable timeout timer on error + SetTimeoutStateStopped(); + ChangeNetworkPacketTimeout(Timeout.Infinite, Timeout.Infinite); + } + else if (msecsRemaining == 0) + { + // Got IO Pending, but we have no time left to wait + // disable the timer and set the error state by calling OnTimeoutSync + ChangeNetworkPacketTimeout(Timeout.Infinite, Timeout.Infinite); + OnTimeoutSync(); + } + // DO NOT HANDLE PENDING READ HERE - which is TdsEnums.SNI_SUCCESS_IO_PENDING state. + // That is handled by user who initiated async read, or by ReadNetworkPacket which is sync over async. + } + finally + { + if (!TdsParserStateObjectFactory.UseManagedSNI) + { + if (!IsPacketEmpty(readPacket)) + { + // Be sure to release packet, otherwise it will be leaked by native. + ReleasePacket(readPacket); + } + } + AssertValidState(); + } + } + + /// + /// Checks to see if the underlying connection is still alive (used by connection pool resiliency) + /// NOTE: This is not safe to do on a connection that is currently in use + /// NOTE: This will mark the connection as broken if it is found to be dead + /// + /// If true then an exception will be thrown if the connection is found to be dead, otherwise no exception will be thrown + /// True if the connection is still alive, otherwise false + internal bool IsConnectionAlive(bool throwOnException) + { + Debug.Assert(_parser.Connection == null || _parser.Connection.Pool != null, "Shouldn't be calling IsConnectionAlive on non-pooled connections"); + bool isAlive = true; + + if (DateTime.UtcNow.Ticks - _lastSuccessfulIOTimer._value > CheckConnectionWindow) + { + if ((_parser == null) || ((_parser.State == TdsParserState.Broken) || (_parser.State == TdsParserState.Closed))) + { + isAlive = false; + if (throwOnException) + { + throw SQL.ConnectionDoomed(); + } + } + else if ((_pendingCallbacks > 1) || ((_parser.Connection != null) && (!_parser.Connection.IsInPool))) + { + SqlClientEventSource.Log.TryTraceEvent("TdsParserStateObject.IsConnectionAlive | Info | State Object Id {0}, This connection is currently in use, assume that the connection is 'alive'", _objectID); + // NOTE: SNICheckConnection is not currently supported for connections that are in use + Debug.Assert(true, "Call to IsConnectionAlive while connection is in use"); + } + else + { + TdsParser.ReliabilitySection.Assert("unreliable call to IsConnectionAlive"); // you need to setup for a thread abort somewhere before you call this method + + SniContext = SniContext.Snix_Connect; + + uint error = CheckConnection(); + if ((error != TdsEnums.SNI_SUCCESS) && (error != TdsEnums.SNI_WAIT_TIMEOUT)) + { + // Connection is dead + SqlClientEventSource.Log.TryTraceEvent("TdsParserStateObject.IsConnectionAlive | Info | State Object Id {0}, received error {1} on idle connection", _objectID, (int)error); + isAlive = false; + if (throwOnException) + { + // Get the error from SNI so that we can throw the correct exception + AddError(_parser.ProcessSNIError(this)); + ThrowExceptionAndWarning(); + } + } + else + { + _lastSuccessfulIOTimer._value = DateTime.UtcNow.Ticks; + } + } + } + + return isAlive; + } + /* // leave this in. comes handy if you have to do Console.WriteLine style debugging ;)