Skip to content

Commit 37dde3f

Browse files
authored
Fix 3640 | Remove extra connection deactivation. (#3758)
1 parent a325e04 commit 37dde3f

File tree

9 files changed

+106
-5
lines changed

9 files changed

+106
-5
lines changed

src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionClosed.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ internal override void CloseConnection(DbConnection owningObject, SqlConnectionF
3333
// not much to do here...
3434
}
3535

36+
/// <inheritdoc/>
3637
protected override void Deactivate() => ADP.ClosedConnectionError();
3738

3839
public override void EnlistTransaction(System.Transactions.Transaction transaction) => throw ADP.ClosedConnectionError();
@@ -55,6 +56,9 @@ internal override bool TryOpenConnection(
5556
TaskCompletionSource<DbConnectionInternal> retry,
5657
DbConnectionOptions userOptions) =>
5758
TryOpenConnectionInternal(outerConnection, connectionFactory, retry, userOptions);
59+
60+
/// <inheritdoc/>
61+
internal override void ResetConnection() => throw ADP.ClosedConnectionError();
5862
}
5963

6064
internal abstract class DbConnectionBusy : DbConnectionClosed

src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/DbConnectionInternal.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,13 @@ internal void PrePush(DbConnection expectedOwner)
767767
internal void RemoveWeakReference(object value) =>
768768
ReferenceCollection?.Remove(value);
769769

770+
/// <summary>
771+
/// Idempotently resets the connection so that it may be recycled without leaking state.
772+
/// May preserve transaction state if the connection is enlisted in a distributed transaction.
773+
/// Should be called before the first action is taken on a recycled connection.
774+
/// </summary>
775+
internal abstract void ResetConnection();
776+
770777
internal void SetInStasis()
771778
{
772779
IsTxRootWaitingForTxEnd = true;
@@ -804,6 +811,11 @@ internal virtual bool TryReplaceConnection(
804811

805812
#region Protected Methods
806813

814+
/// <summary>
815+
/// Activates the connection, preparing it for active use.
816+
/// An activated connection has an owner and is checked out from the connection pool (if pooling is enabled).
817+
/// </summary>
818+
/// <param name="transaction">The transaction in which the connection should enlist.</param>
807819
protected abstract void Activate(Transaction transaction);
808820

809821
/// <summary>
@@ -820,6 +832,11 @@ protected virtual DbReferenceCollection CreateReferenceCollection()
820832
throw ADP.InternalError(ADP.InternalErrorCode.AttemptingToConstructReferenceCollectionOnStaticObject);
821833
}
822834

835+
/// <summary>
836+
/// Deactivates the connection, cleaning up any state as necessary.
837+
/// A deactivated connection is one that is no longer in active use and does not have an owner.
838+
/// A deactivated connection may be open (connected to a server) and is checked into the connection pool (if pooling is enabled).
839+
/// </summary>
823840
protected abstract void Deactivate();
824841

825842
protected internal void DoNotPoolThisConnection()

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1379,8 +1379,6 @@ public void PutObjectFromTransactedPool(DbConnectionInternal obj)
13791379
Debug.Assert(obj != null, "null pooledObject?");
13801380
Debug.Assert(obj.EnlistedTransaction == null, "pooledObject is still enlisted?");
13811381

1382-
obj.DeactivateConnection();
1383-
13841382
// called by the transacted connection pool , once it's removed the
13851383
// connection from it's list. We put the connection back in general
13861384
// circulation.
@@ -1393,6 +1391,7 @@ public void PutObjectFromTransactedPool(DbConnectionInternal obj)
13931391

13941392
if (State is Running && obj.CanBePooled)
13951393
{
1394+
obj.ResetConnection();
13961395
PutNewObject(obj);
13971396
}
13981397
else

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnection.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ override protected DbReferenceCollection CreateReferenceCollection()
246246
return new SqlReferenceCollection();
247247
}
248248

249+
/// <inheritdoc/>
249250
override protected void Deactivate()
250251
{
251252
try

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3387,9 +3387,8 @@ private void OpenLoginEnlist(
33873387
#endif
33883388
}
33893389

3390-
// @TODO: Is this suppression still required
3391-
[SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters")] // copied from Triaged.cs
3392-
private void ResetConnection()
3390+
/// <inheritdoc/>
3391+
internal override void ResetConnection()
33933392
{
33943393
// For implicit pooled connections, if connection reset behavior is specified, reset
33953394
// the database and language properties back to default. It is important to do this on

src/Microsoft.Data.SqlClient/tests/ManualTests/TracingTests/MetricsTest.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,73 @@ public void StasisCounters_Functional()
177177
Assert.Equal(0, SqlClientEventSourceProps.StasisConnections);
178178
}
179179

180+
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))]
181+
public void TransactedConnectionPool_VerifyActiveConnectionCounters()
182+
{
183+
// This test verifies that the active connection count metric never goes negative
184+
// when connections are returned to the pool while enlisted in a transaction.
185+
// This is a regression test for issue #3640 where an extra DeactivateConnection
186+
// call was causing the active connection count to go negative.
187+
188+
// Arrange
189+
var stringBuilder = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString)
190+
{
191+
Pooling = true,
192+
Enlist = false,
193+
MinPoolSize = 0,
194+
MaxPoolSize = 10
195+
};
196+
197+
// Clear pools to start fresh
198+
ClearConnectionPools();
199+
200+
long initialActiveSoftConnections = SqlClientEventSourceProps.ActiveSoftConnections;
201+
long initialActiveHardConnections = SqlClientEventSourceProps.ActiveHardConnections;
202+
long initialActiveConnections = SqlClientEventSourceProps.ActiveConnections;
203+
204+
// Act and Assert
205+
// Verify counters at each step in the lifecycle of a transacted connection
206+
using (var txScope = new TransactionScope())
207+
{
208+
using (var conn = new SqlConnection(stringBuilder.ToString()))
209+
{
210+
conn.Open();
211+
conn.EnlistTransaction(System.Transactions.Transaction.Current);
212+
213+
if (SupportsActiveConnectionCounters)
214+
{
215+
// Connection should be active
216+
Assert.Equal(initialActiveSoftConnections + 1, SqlClientEventSourceProps.ActiveSoftConnections);
217+
Assert.Equal(initialActiveHardConnections + 1, SqlClientEventSourceProps.ActiveHardConnections);
218+
Assert.Equal(initialActiveConnections + 1, SqlClientEventSourceProps.ActiveConnections);
219+
}
220+
221+
conn.Close();
222+
223+
// Connection is returned to pool but still in transaction (stasis)
224+
if (SupportsActiveConnectionCounters)
225+
{
226+
// Connection should be deactivated (returned to pool)
227+
Assert.Equal(initialActiveSoftConnections, SqlClientEventSourceProps.ActiveSoftConnections);
228+
Assert.Equal(initialActiveHardConnections + 1, SqlClientEventSourceProps.ActiveHardConnections);
229+
Assert.Equal(initialActiveConnections, SqlClientEventSourceProps.ActiveConnections);
230+
}
231+
}
232+
233+
// Completing the transaction after the connection is closed ensures that the connection
234+
// is in the transacted pool at the time the transaction ends. This verifies that the
235+
// transition from the transacted pool back to the main pool properly updates the counters.
236+
txScope.Complete();
237+
}
238+
239+
if (SupportsActiveConnectionCounters)
240+
{
241+
Assert.Equal(initialActiveSoftConnections, SqlClientEventSourceProps.ActiveSoftConnections);
242+
Assert.Equal(initialActiveHardConnections+1, SqlClientEventSourceProps.ActiveHardConnections);
243+
Assert.Equal(initialActiveConnections, SqlClientEventSourceProps.ActiveConnections);
244+
}
245+
}
246+
180247
[ActiveIssue("https://github.com/dotnet/SqlClient/issues/3031")]
181248
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))]
182249
public void ReclaimedConnectionsCounter_Functional()

src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/ChannelDbConnectionPoolTest.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,11 @@ protected override void Deactivate()
777777
{
778778
return;
779779
}
780+
781+
internal override void ResetConnection()
782+
{
783+
return;
784+
}
780785
#endregion
781786
}
782787
#endregion

src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/ConnectionPoolSlotsTest.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ protected override void Deactivate()
4343
{
4444
// Mock implementation - do nothing
4545
}
46+
47+
internal override void ResetConnection()
48+
{
49+
// Mock implementation - do nothing
50+
}
4651
}
4752

4853
[Fact]

src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/TransactedConnectionPoolTest.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,10 @@ protected override void Deactivate()
731731
// Mock implementation - do nothing
732732
}
733733

734+
internal override void ResetConnection()
735+
{
736+
// Mock implementation - do nothing
737+
}
734738
public override string ToString() => $"MockConnection_{MockId}";
735739
}
736740

0 commit comments

Comments
 (0)