Skip to content

Commit 4233f94

Browse files
committed
Add tests. Clean up diff.
1 parent 87a1129 commit 4233f94

File tree

2 files changed

+133
-49
lines changed

2 files changed

+133
-49
lines changed

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

Lines changed: 12 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,6 @@ public DbConnectionPoolState State
110110

111111
// Counts the total number of open connections tracked by the pool.
112112
private volatile int _numConnections;
113-
114-
// Counts the number of connections currently sitting idle in the pool.
115-
private volatile int _idleCount;
116113
#endregion
117114

118115

@@ -140,7 +137,7 @@ internal ChannelDbConnectionPool(
140137

141138
_connections = new DbConnectionInternal[MaxPoolSize];
142139

143-
// We enforce Max Pool Size, so no need to to create a bounded channel (which is less efficient)
140+
// We enforce Max Pool Size, so no need to create a bounded channel (which is less efficient)
144141
// On the consuming side, we have the multiplexing write loop but also non-multiplexing Rents
145142
// On the producing side, we have connections being released back into the pool (both multiplexing and not)
146143
var idleChannel = Channel.CreateUnbounded<DbConnectionInternal?>();
@@ -198,6 +195,11 @@ public bool TryGetConnection(DbConnection owningObject, TaskCompletionSource<DbC
198195
{
199196
if (taskCompletionSource is not null)
200197
{
198+
// This is ugly, but async anti-patterns further up the stack necessitate a fresh task to be created.
199+
// Ideally we would just return a Task<DbConnectionInternal> and let the caller await it as needed,
200+
// but we need to signal to the provided TaskCompletionSource when the connection is established.
201+
// This pattern has implications for connection open retry logic that are intricate enough to merit
202+
// dedicated work.
201203
Task.Run(async () =>
202204
{
203205
//TODO: use same timespan everywhere and tick down for queueuing and actual connection opening work
@@ -224,7 +226,7 @@ public bool TryGetConnection(DbConnection owningObject, TaskCompletionSource<DbC
224226
}
225227
}
226228

227-
internal async Task<DbConnectionInternal> GetInternalConnection(DbConnection owningConnection, DbConnectionOptions userOptions, TimeSpan timeout, bool async, CancellationToken cancellationToken)
229+
private async Task<DbConnectionInternal> GetInternalConnection(DbConnection owningConnection, DbConnectionOptions userOptions, TimeSpan timeout, bool async, CancellationToken cancellationToken)
228230
{
229231
DbConnectionInternal? connection = null;
230232

@@ -282,7 +284,8 @@ internal async Task<DbConnectionInternal> GetInternalConnection(DbConnection own
282284
}
283285
}
284286

285-
if (connection != null && CheckIdleConnection(connection))
287+
// TODO: check if connection is still valid
288+
if (connection != null)
286289
{
287290
//TODO: set connection internal state
288291
//PrepareConnection(owningConnection, connection, transaction);
@@ -332,42 +335,16 @@ internal async Task<DbConnectionInternal> GetInternalConnection(DbConnection own
332335
[MethodImpl(MethodImplOptions.AggressiveInlining)]
333336
private DbConnectionInternal? GetIdleConnection()
334337
{
335-
336338
while (_idleConnectionReader.TryRead(out DbConnectionInternal? connection))
337339
{
338-
if (CheckIdleConnection(connection))
339-
{
340-
return connection;
341-
}
340+
// TODO: check if connection is still valid
341+
// if (CheckIdleConnection(connection))
342+
return connection;
342343
}
343344

344345
return null;
345346
}
346347

347-
/// <summary>
348-
/// Checks that the provided connection is live and unexpired and closes it if needed.
349-
/// Decrements the idle count as long as the connection is not null.
350-
/// </summary>
351-
/// <param name="connection">The connection to be checked.</param>
352-
/// <returns>Returns true if the connection is live and unexpired, otherwise returns false.</returns>
353-
/// TODO: profile the inlining to see if it's necessary
354-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
355-
private bool CheckIdleConnection(DbConnectionInternal? connection)
356-
{
357-
if (connection is null)
358-
{
359-
return false;
360-
}
361-
362-
// Only decrement when the connection has a value.
363-
Interlocked.Decrement(ref _idleCount);
364-
365-
//TODO: check if the connection is expired
366-
//return CheckConnection(connection);
367-
368-
return true;
369-
}
370-
371348
/// <inheritdoc/>
372349
internal Task<DbConnectionInternal?> OpenNewInternalConnection(DbConnection? owningConnection, DbConnectionOptions userOptions, TimeSpan timeout, bool async, CancellationToken cancellationToken)
373350
{

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

Lines changed: 121 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public class ChannelDbConnectionPoolTest
2626
public ChannelDbConnectionPoolTest()
2727
{
2828
// Use a stubbed connection factory to avoid network code
29-
connectionFactory = new ConnectionFactoryStub();
29+
connectionFactory = new SuccessfulConnectionFactory();
3030
identity = DbConnectionPoolIdentity.NoIdentity;
3131
connectionPoolProviderInfo = new DbConnectionPoolProviderInfo();
3232
poolGroupOptions = new DbConnectionPoolGroupOptions(
@@ -54,20 +54,21 @@ public ChannelDbConnectionPoolTest()
5454
[InlineData(1)]
5555
[InlineData(5)]
5656
[InlineData(10)]
57-
public async Task TestGetConnectionFromEmptyPoolSync_ShouldCreateNewConnection(int numConnections)
57+
public void TestGetConnectionFromEmptyPoolSync_ShouldCreateNewConnection(int numConnections)
5858
{
5959
// Act
6060
for (int i = 0; i < numConnections; i++)
6161
{
62-
var internalConnection = await pool.GetInternalConnection(
62+
DbConnectionInternal internalConnection = null;
63+
var completed = pool.TryGetConnection(
6364
new SqlConnection(),
65+
taskCompletionSource: null,
6466
new DbConnectionOptions("", null),
65-
TimeSpan.Zero,
66-
async: false,
67-
CancellationToken.None
67+
out internalConnection
6868
);
6969

7070
// Assert
71+
Assert.True(completed);
7172
Assert.NotNull(internalConnection);
7273
}
7374

@@ -85,23 +86,29 @@ public async Task TestGetConnectionFromEmptyPoolAsync_ShouldCreateNewConnection(
8586
// Act
8687
for (int i = 0; i < numConnections; i++)
8788
{
88-
var internalConnection = await pool.GetInternalConnection(
89+
var tcs = new TaskCompletionSource<DbConnectionInternal>();
90+
DbConnectionInternal internalConnection = null;
91+
var completed = pool.TryGetConnection(
8992
new SqlConnection(),
93+
tcs,
9094
new DbConnectionOptions("", null),
91-
TimeSpan.Zero,
92-
async: false,
93-
CancellationToken.None
95+
out internalConnection
9496
);
9597

9698
// Assert
97-
Assert.NotNull(internalConnection);
99+
Assert.False(completed);
100+
Assert.Null(internalConnection);
101+
Assert.NotNull(await tcs.Task);
98102
}
99103

100104

101105
// Assert
102106
Assert.Equal(numConnections, pool.Count);
103107
}
104108

109+
// Test that requests to get connection from the pool fails when max pool size is reached
110+
111+
105112
#region Property Tests
106113

107114
[Fact]
@@ -236,15 +243,53 @@ public void TestTransactionEnded()
236243
{
237244
Assert.Throws<NotImplementedException>(() => pool.TransactionEnded(null!, null!));
238245
}
246+
[Fact]
247+
public void TestGetConnectionFailsWhenMaxPoolSizeReached()
248+
{
249+
// Arrange
250+
for (int i = 0; i < poolGroupOptions.MaxPoolSize; i++)
251+
{
252+
DbConnectionInternal internalConnection = null;
253+
var completed = pool.TryGetConnection(
254+
new SqlConnection(),
255+
taskCompletionSource: null,
256+
new DbConnectionOptions("", null),
257+
out internalConnection
258+
);
259+
260+
Assert.True(completed);
261+
Assert.NotNull(internalConnection);
262+
}
239263

264+
try
265+
{
266+
// Act
267+
DbConnectionInternal extraConnection = null;
268+
var exceeded = pool.TryGetConnection(
269+
//TODO: set timeout to make this faster
270+
new SqlConnection(),
271+
taskCompletionSource: null,
272+
new DbConnectionOptions("", null),
273+
out extraConnection
274+
);
275+
} catch (Exception ex)
276+
{
277+
// Assert
278+
Assert.IsType<InvalidOperationException>(ex);
279+
Assert.Equal("Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.", ex.Message);
280+
}
281+
282+
// Assert
283+
Assert.Equal(poolGroupOptions.MaxPoolSize, pool.Count);
284+
}
240285
#endregion
241286

242287
#region Test classes
243-
internal class ConnectionFactoryStub : DbConnectionFactory
288+
internal class SuccessfulConnectionFactory : DbConnectionFactory
244289
{
245290
protected override DbConnectionInternal CreateConnection(DbConnectionOptions options, DbConnectionPoolKey poolKey, object poolGroupProviderInfo, IDbConnectionPool pool, DbConnection owningConnection)
246291
{
247-
return new TestDbConnectionInternal();
292+
return new SuccessfulDbConnectionInternal();
248293
}
249294

250295
#region Not Implemented Members
@@ -302,7 +347,7 @@ internal override void SetInnerConnectionTo(DbConnection owningObject, DbConnect
302347
#endregion
303348
}
304349

305-
internal class TestDbConnectionInternal : DbConnectionInternal
350+
internal class SuccessfulDbConnectionInternal : DbConnectionInternal
306351
{
307352
#region Not Implemented Members
308353
public override string ServerVersion => throw new NotImplementedException();
@@ -328,6 +373,68 @@ protected override void Deactivate()
328373
}
329374
#endregion
330375
}
376+
377+
internal class TimeoutConnectionFactory : DbConnectionFactory
378+
{
379+
protected override DbConnectionInternal CreateConnection(DbConnectionOptions options, DbConnectionPoolKey poolKey, object poolGroupProviderInfo, IDbConnectionPool pool, DbConnection owningConnection)
380+
{
381+
return new SuccessfulDbConnectionInternal();
382+
}
383+
384+
#region Not Implemented Members
385+
public override DbProviderFactory ProviderFactory => throw new NotImplementedException();
386+
387+
protected override DbConnectionOptions CreateConnectionOptions(string connectionString, DbConnectionOptions previous)
388+
{
389+
throw new NotImplementedException();
390+
}
391+
392+
protected override DbConnectionPoolGroupOptions CreateConnectionPoolGroupOptions(DbConnectionOptions options)
393+
{
394+
throw new NotImplementedException();
395+
}
396+
397+
protected override int GetObjectId(DbConnection connection)
398+
{
399+
throw new NotImplementedException();
400+
}
401+
402+
internal override DbConnectionPoolGroup GetConnectionPoolGroup(DbConnection connection)
403+
{
404+
throw new NotImplementedException();
405+
}
406+
407+
internal override DbConnectionInternal GetInnerConnection(DbConnection connection)
408+
{
409+
throw new NotImplementedException();
410+
}
411+
412+
internal override void PermissionDemand(DbConnection outerConnection)
413+
{
414+
throw new NotImplementedException();
415+
}
416+
417+
internal override void SetConnectionPoolGroup(DbConnection outerConnection, DbConnectionPoolGroup poolGroup)
418+
{
419+
throw new NotImplementedException();
420+
}
421+
422+
internal override void SetInnerConnectionEvent(DbConnection owningObject, DbConnectionInternal to)
423+
{
424+
throw new NotImplementedException();
425+
}
426+
427+
internal override bool SetInnerConnectionFrom(DbConnection owningObject, DbConnectionInternal to, DbConnectionInternal from)
428+
{
429+
throw new NotImplementedException();
430+
}
431+
432+
internal override void SetInnerConnectionTo(DbConnection owningObject, DbConnectionInternal to)
433+
{
434+
throw new NotImplementedException();
435+
}
436+
#endregion
437+
}
331438
#endregion
332439
}
333440
}

0 commit comments

Comments
 (0)