Skip to content

Commit b21442d

Browse files
authored
[6.1] Fix 3640 | Remove extra connection deactivation. (#3776)
1 parent a67126b commit b21442d

File tree

7 files changed

+165
-6
lines changed

7 files changed

+165
-6
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -977,8 +977,8 @@ protected override void InternalDeactivate()
977977
}
978978
}
979979

980-
[SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters")] // copied from Triaged.cs
981-
private void ResetConnection()
980+
/// <inheritdoc/>
981+
internal override void ResetConnection()
982982
{
983983
// For implicit pooled connections, if connection reset behavior is specified,
984984
// reset the database and language properties back to default. It is important

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -983,8 +983,8 @@ protected override void InternalDeactivate()
983983
}
984984
}
985985

986-
[SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters")] // copied from Triaged.cs
987-
private void ResetConnection()
986+
/// <inheritdoc/>
987+
internal override void ResetConnection()
988988
{
989989
// For implicit pooled connections, if connection reset behavior is specified,
990990
// reset the database and language properties back to default. It is important

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

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

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

3738
public override void EnlistTransaction(System.Transactions.Transaction transaction) => throw ADP.ClosedConnectionError();
@@ -43,6 +44,9 @@ protected internal override DataTable GetSchema(DbConnectionFactory factory, DbC
4344

4445
internal override bool TryOpenConnection(DbConnection outerConnection, DbConnectionFactory connectionFactory, TaskCompletionSource<DbConnectionInternal> retry, DbConnectionOptions userOptions)
4546
=> base.TryOpenConnectionInternal(outerConnection, connectionFactory, retry, userOptions);
47+
48+
/// <inheritdoc/>
49+
internal override void ResetConnection() => throw ADP.ClosedConnectionError();
4650
}
4751

4852
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
@@ -797,6 +797,13 @@ internal void PrePush(object expectedOwner)
797797
internal void RemoveWeakReference(object value) =>
798798
ReferenceCollection?.Remove(value);
799799

800+
/// <summary>
801+
/// Idempotently resets the connection so that it may be recycled without leaking state.
802+
/// May preserve transaction state if the connection is enlisted in a distributed transaction.
803+
/// Should be called before the first action is taken on a recycled connection.
804+
/// </summary>
805+
internal abstract void ResetConnection();
806+
800807
internal void SetInStasis()
801808
{
802809
IsTxRootWaitingForTxEnd = true;
@@ -834,6 +841,11 @@ internal virtual bool TryReplaceConnection(
834841

835842
#region Protected Methods
836843

844+
/// <summary>
845+
/// Activates the connection, preparing it for active use.
846+
/// An activated connection has an owner and is checked out from the connection pool (if pooling is enabled).
847+
/// </summary>
848+
/// <param name="transaction">The transaction in which the connection should enlist.</param>
837849
protected abstract void Activate(Transaction transaction);
838850

839851
/// <summary>
@@ -850,6 +862,11 @@ protected virtual DbReferenceCollection CreateReferenceCollection()
850862
throw ADP.InternalError(ADP.InternalErrorCode.AttemptingToConstructReferenceCollectionOnStaticObject);
851863
}
852864

865+
/// <summary>
866+
/// Deactivates the connection, cleaning up any state as necessary.
867+
/// A deactivated connection is one that is no longer in active use and does not have an owner.
868+
/// A deactivated connection may be open (connected to a server) and is checked into the connection pool (if pooling is enabled).
869+
/// </summary>
853870
protected abstract void Deactivate();
854871

855872
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
@@ -1682,8 +1682,6 @@ public void PutObjectFromTransactedPool(DbConnectionInternal obj)
16821682
Debug.Assert(obj != null, "null pooledObject?");
16831683
Debug.Assert(obj.EnlistedTransaction == null, "pooledObject is still enlisted?");
16841684

1685-
obj.DeactivateConnection();
1686-
16871685
// called by the transacted connection pool , once it's removed the
16881686
// connection from it's list. We put the connection back in general
16891687
// circulation.
@@ -1696,6 +1694,7 @@ public void PutObjectFromTransactedPool(DbConnectionInternal obj)
16961694

16971695
if (State is Running && obj.CanBePooled)
16981696
{
1697+
obj.ResetConnection();
16991698
PutNewObject(obj);
17001699
}
17011700
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
@@ -301,6 +301,7 @@ override protected DbReferenceCollection CreateReferenceCollection()
301301
return new SqlReferenceCollection();
302302
}
303303

304+
/// <inheritdoc/>
304305
override protected void Deactivate()
305306
{
306307
TdsParser bestEffortCleanupTarget = null;

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

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Diagnostics;
66
using System.Reflection;
7+
using System.Threading.Tasks;
78
using System.Transactions;
89
using Xunit;
910

@@ -177,6 +178,143 @@ public void StasisCounters_Functional()
177178
Assert.Equal(0, SqlClientEventSourceProps.StasisConnections);
178179
}
179180

181+
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))]
182+
public void TransactedConnectionPool_VerifyActiveConnectionCounters()
183+
{
184+
// This test verifies that the active connection count metric never goes negative
185+
// when connections are returned to the pool while enlisted in a transaction.
186+
// This is a regression test for issue #3640 where an extra DeactivateConnection
187+
// call was causing the active connection count to go negative.
188+
189+
// Arrange
190+
var stringBuilder = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString)
191+
{
192+
Pooling = true,
193+
Enlist = false,
194+
MinPoolSize = 0,
195+
MaxPoolSize = 10
196+
};
197+
198+
// Clear pools to start fresh
199+
ClearConnectionPools();
200+
201+
long initialActiveSoftConnections = SqlClientEventSourceProps.ActiveSoftConnections;
202+
long initialActiveHardConnections = SqlClientEventSourceProps.ActiveHardConnections;
203+
long initialActiveConnections = SqlClientEventSourceProps.ActiveConnections;
204+
205+
// Act and Assert
206+
// Verify counters at each step in the lifecycle of a transacted connection
207+
using (var txScope = new TransactionScope())
208+
{
209+
using (var conn = new SqlConnection(stringBuilder.ToString()))
210+
{
211+
conn.Open();
212+
conn.EnlistTransaction(System.Transactions.Transaction.Current);
213+
214+
if (SupportsActiveConnectionCounters)
215+
{
216+
// Connection should be active
217+
Assert.Equal(initialActiveSoftConnections + 1, SqlClientEventSourceProps.ActiveSoftConnections);
218+
Assert.Equal(initialActiveHardConnections + 1, SqlClientEventSourceProps.ActiveHardConnections);
219+
Assert.Equal(initialActiveConnections + 1, SqlClientEventSourceProps.ActiveConnections);
220+
}
221+
222+
conn.Close();
223+
224+
// Connection is returned to pool but still in transaction (stasis)
225+
if (SupportsActiveConnectionCounters)
226+
{
227+
// Connection should be deactivated (returned to pool)
228+
Assert.Equal(initialActiveSoftConnections, SqlClientEventSourceProps.ActiveSoftConnections);
229+
Assert.Equal(initialActiveHardConnections + 1, SqlClientEventSourceProps.ActiveHardConnections);
230+
Assert.Equal(initialActiveConnections, SqlClientEventSourceProps.ActiveConnections);
231+
}
232+
}
233+
234+
// Completing the transaction after the connection is closed ensures that the connection
235+
// is in the transacted pool at the time the transaction ends. This verifies that the
236+
// transition from the transacted pool back to the main pool properly updates the counters.
237+
txScope.Complete();
238+
}
239+
240+
if (SupportsActiveConnectionCounters)
241+
{
242+
Assert.Equal(initialActiveSoftConnections, SqlClientEventSourceProps.ActiveSoftConnections);
243+
Assert.Equal(initialActiveHardConnections + 1, SqlClientEventSourceProps.ActiveHardConnections);
244+
Assert.Equal(initialActiveConnections, SqlClientEventSourceProps.ActiveConnections);
245+
}
246+
}
247+
248+
#if NET
249+
// Note: DbConnection.CloseAsync is not available in .NET Framework
250+
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))]
251+
public async Task TransactedConnectionPool_VerifyActiveConnectionCounters_Async()
252+
{
253+
// This test verifies that the active connection count metric never goes negative
254+
// when connections are returned to the pool while enlisted in a transaction.
255+
// This is a regression test for issue #3640 where an extra DeactivateConnection
256+
// call was causing the active connection count to go negative.
257+
258+
// Arrange
259+
var stringBuilder = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString)
260+
{
261+
Pooling = true,
262+
Enlist = false,
263+
MinPoolSize = 0,
264+
MaxPoolSize = 10
265+
};
266+
267+
// Clear pools to start fresh
268+
ClearConnectionPools();
269+
270+
long initialActiveSoftConnections = SqlClientEventSourceProps.ActiveSoftConnections;
271+
long initialActiveHardConnections = SqlClientEventSourceProps.ActiveHardConnections;
272+
long initialActiveConnections = SqlClientEventSourceProps.ActiveConnections;
273+
274+
// Act and Assert
275+
// Verify counters at each step in the lifecycle of a transacted connection
276+
using (var txScope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
277+
{
278+
using (var conn = new SqlConnection(stringBuilder.ToString()))
279+
{
280+
await conn.OpenAsync();
281+
conn.EnlistTransaction(System.Transactions.Transaction.Current);
282+
283+
if (SupportsActiveConnectionCounters)
284+
{
285+
// Connection should be active
286+
Assert.Equal(initialActiveSoftConnections + 1, SqlClientEventSourceProps.ActiveSoftConnections);
287+
Assert.Equal(initialActiveHardConnections + 1, SqlClientEventSourceProps.ActiveHardConnections);
288+
Assert.Equal(initialActiveConnections + 1, SqlClientEventSourceProps.ActiveConnections);
289+
}
290+
291+
await conn.CloseAsync();
292+
293+
// Connection is returned to pool but still in transaction (stasis)
294+
if (SupportsActiveConnectionCounters)
295+
{
296+
// Connection should be deactivated (returned to pool)
297+
Assert.Equal(initialActiveSoftConnections, SqlClientEventSourceProps.ActiveSoftConnections);
298+
Assert.Equal(initialActiveHardConnections + 1, SqlClientEventSourceProps.ActiveHardConnections);
299+
Assert.Equal(initialActiveConnections, SqlClientEventSourceProps.ActiveConnections);
300+
}
301+
}
302+
303+
// Completing the transaction after the connection is closed ensures that the connection
304+
// is in the transacted pool at the time the transaction ends. This verifies that the
305+
// transition from the transacted pool back to the main pool properly updates the counters.
306+
txScope.Complete();
307+
}
308+
309+
if (SupportsActiveConnectionCounters)
310+
{
311+
Assert.Equal(initialActiveSoftConnections, SqlClientEventSourceProps.ActiveSoftConnections);
312+
Assert.Equal(initialActiveHardConnections + 1, SqlClientEventSourceProps.ActiveHardConnections);
313+
Assert.Equal(initialActiveConnections, SqlClientEventSourceProps.ActiveConnections);
314+
}
315+
}
316+
#endif
317+
180318
[ActiveIssue("https://github.com/dotnet/SqlClient/issues/3031")]
181319
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))]
182320
public void ReclaimedConnectionsCounter_Functional()

0 commit comments

Comments
 (0)