Skip to content

Commit 97f659f

Browse files
committed
Reap connections more frequently. Fixes #442
If ConnectionIdleTimeout is set to a low value for a particular connection pool, clean up connections in that pool more frequently. (Since most applications have just one connection string, switching from a global reaper Task to a per-pool Task won't create extra background tasks for most applications.) Since the reaper interval is now set per connection pool, the DebugOnlyTests are no longer necessary (and can be executed with the other ConnectionPool tests).
1 parent e0ad406 commit 97f659f

File tree

6 files changed

+61
-85
lines changed

6 files changed

+61
-85
lines changed

.ci/test.ps1

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,6 @@ dotnet xunit -c Release
3232
if ($LASTEXITCODE -ne 0){
3333
exit $LASTEXITCODE;
3434
}
35-
echo "Executing Debug Only tests"
36-
dotnet xunit -c Debug -class SideBySide.DebugOnlyTests
37-
if ($LASTEXITCODE -ne 0){
38-
exit $LASTEXITCODE;
39-
}
4035

4136
echo "Executing tests with Compression, No SSL"
4237
Copy-Item -Force ..\..\.ci\config\config.compression.json config.json

.travis.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ script:
3535
- dotnet build SideBySide.csproj -c Release -f netcoreapp2.0
3636
- echo 'Executing netcoreapp1.1.2 tests with No Compression, No SSL' && ../../.ci/use-config.sh config.json 172.17.0.1 3307 $NAME $OMIT_FEATURES && time dotnet xunit -c Release -f netcoreapp1.1.2
3737
- echo 'Executing netcoreapp2.0 tests with No Compression, No SSL' && ../../.ci/use-config.sh config.json 172.17.0.1 3307 $NAME $OMIT_FEATURES && time dotnet xunit -c Release -f netcoreapp2.0
38-
- echo 'Executing netcoreapp2.0 Debug Only tests' && time dotnet xunit -c Debug -f netcoreapp2.0 -class SideBySide.DebugOnlyTests
3938
- echo 'Executing netcoreapp2.0 tests with Compression, No SSL' && ../../.ci/use-config.sh config.compression.json 172.17.0.1 3307 $NAME $OMIT_FEATURES && time dotnet xunit -c Release -f netcoreapp2.0
4039
- echo 'Executing netcoreapp1.1.2 tests with No Compression, SSL' && ../../.ci/use-config.sh config.ssl.json 172.17.0.1 3307 $NAME $OMIT_FEATURES && time dotnet xunit -c Release -f netcoreapp1.1.2
4140
- echo 'Executing netcoreapp2.0 tests with No Compression, SSL' && ../../.ci/use-config.sh config.ssl.json 172.17.0.1 3307 $NAME $OMIT_FEATURES && time dotnet xunit -c Release -f netcoreapp2.0

src/MySqlConnector/Core/ConnectionPool.cs

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,6 @@ public async Task ReapAsync(IOBehavior ioBehavior, CancellationToken cancellatio
178178
{
179179
Log.Debug("{0} reaping connection pool", m_logArguments);
180180
RecoverLeakedSessions();
181-
if (ConnectionSettings.ConnectionIdleTimeout == 0)
182-
return;
183181
await CleanPoolAsync(ioBehavior, session => (DateTime.UtcNow - session.LastReturnedUtc).TotalSeconds >= ConnectionSettings.ConnectionIdleTimeout, true, cancellationToken).ConfigureAwait(false);
184182
}
185183

@@ -398,12 +396,6 @@ public static async Task ClearPoolsAsync(IOBehavior ioBehavior, CancellationToke
398396
await pool.ClearAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
399397
}
400398

401-
public static async Task ReapPoolsAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
402-
{
403-
foreach (var pool in GetAllPools())
404-
await pool.ReapAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
405-
}
406-
407399
private static IReadOnlyList<ConnectionPool> GetAllPools()
408400
{
409401
var pools = new List<ConnectionPool>(s_pools.Count);
@@ -430,6 +422,7 @@ private ConnectionPool(ConnectionSettings cs)
430422
foreach (var hostName in cs.HostNames)
431423
m_hostSessions[hostName] = 0;
432424
}
425+
433426
m_loadBalancer = cs.ConnectionType != ConnectionType.Tcp ? null :
434427
cs.HostNames.Count == 1 || cs.LoadBalance == MySqlLoadBalance.FailOver ? FailOverLoadBalancer.Instance :
435428
cs.LoadBalance == MySqlLoadBalance.Random ? RandomLoadBalancer.Instance :
@@ -440,6 +433,28 @@ private ConnectionPool(ConnectionSettings cs)
440433
m_logArguments = new object[] { "Pool{0}".FormatInvariant(Id) };
441434
if (Log.IsInfoEnabled())
442435
Log.Info("{0} creating new connection pool for {1}", m_logArguments[0], cs.ConnectionStringBuilder.GetConnectionString(includePassword: false));
436+
437+
if (cs.ConnectionIdleTimeout > 0)
438+
{
439+
var reaperInterval = TimeSpan.FromSeconds(Math.Max(1, Math.Min(60, cs.ConnectionIdleTimeout / 2)));
440+
m_reaperTask = Task.Run(async () =>
441+
{
442+
while (true)
443+
{
444+
var task = Task.Delay(reaperInterval);
445+
try
446+
{
447+
using (var source = new CancellationTokenSource(reaperInterval))
448+
await ReapAsync(IOBehavior.Asynchronous, source.Token).ConfigureAwait(false);
449+
}
450+
catch
451+
{
452+
// do nothing; we'll try to reap again
453+
}
454+
await task.ConfigureAwait(false);
455+
}
456+
});
457+
}
443458
}
444459

445460
private void AdjustHostConnectionCount(ServerSession session, int delta)
@@ -478,26 +493,6 @@ public ConnectionStringPool(string connectionString, ConnectionPool pool)
478493

479494
static readonly IMySqlConnectorLogger Log = MySqlConnectorLogManager.CreateLogger(nameof(ConnectionPool));
480495
static readonly ConcurrentDictionary<string, ConnectionPool> s_pools = new ConcurrentDictionary<string, ConnectionPool>();
481-
#if DEBUG
482-
static readonly TimeSpan ReaperInterval = TimeSpan.FromSeconds(1);
483-
#else
484-
static readonly TimeSpan ReaperInterval = TimeSpan.FromMinutes(1);
485-
#endif
486-
static readonly Task Reaper = Task.Run(async () => {
487-
while (true)
488-
{
489-
var task = Task.Delay(ReaperInterval);
490-
try
491-
{
492-
await ReapPoolsAsync(IOBehavior.Asynchronous, new CancellationTokenSource(ReaperInterval).Token).ConfigureAwait(false);
493-
}
494-
catch
495-
{
496-
// do nothing; we'll try to reap again
497-
}
498-
await task.ConfigureAwait(false);
499-
}
500-
});
501496

502497
static int s_poolId;
503498
static ConnectionStringPool s_mruCache;
@@ -510,6 +505,7 @@ public ConnectionStringPool(string connectionString, ConnectionPool pool)
510505
readonly ILoadBalancer m_loadBalancer;
511506
readonly Dictionary<string, int> m_hostSessions;
512507
readonly object[] m_logArguments;
508+
readonly Task m_reaperTask;
513509
uint m_lastRecoveryTime;
514510
int m_lastSessionId;
515511
Dictionary<string, CachedProcedure> m_procedureCache;

tests/SideBySide/ConnectionPool.cs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using System.Diagnostics;
44
using System.Linq;
@@ -146,6 +146,7 @@ public void LeakConnections()
146146

147147
// have to GC for leaked connections to be removed from the pool
148148
GC.Collect();
149+
Thread.Sleep(400);
149150
}
150151
}
151152

@@ -282,6 +283,41 @@ public async Task ClearConnectionPool()
282283
connection.Dispose();
283284
}
284285

286+
#if !BASELINE
287+
[Theory]
288+
[InlineData(1u, 3u, 0u, 5u)]
289+
[InlineData(1u, 3u, 3u, 5u)]
290+
public async Task ConnectionLifeTime(uint idleTimeout, uint delaySeconds, uint minPoolSize, uint maxPoolSize)
291+
{
292+
var csb = AppConfig.CreateConnectionStringBuilder();
293+
csb.Pooling = true;
294+
csb.MinimumPoolSize = minPoolSize;
295+
csb.MaximumPoolSize = maxPoolSize;
296+
csb.ConnectionIdleTimeout = idleTimeout;
297+
HashSet<int> serverThreadIdsBegin = new HashSet<int>();
298+
HashSet<int> serverThreadIdsEnd = new HashSet<int>();
299+
300+
async Task OpenConnections(uint numConnections, HashSet<int> serverIdSet)
301+
{
302+
using (var connection = new MySqlConnection(csb.ConnectionString))
303+
{
304+
await connection.OpenAsync();
305+
serverIdSet.Add(connection.ServerThread);
306+
if (--numConnections <= 0)
307+
return;
308+
await OpenConnections(numConnections, serverIdSet);
309+
}
310+
}
311+
312+
await OpenConnections(maxPoolSize, serverThreadIdsBegin);
313+
await Task.Delay(TimeSpan.FromSeconds(delaySeconds));
314+
await OpenConnections(maxPoolSize, serverThreadIdsEnd);
315+
316+
serverThreadIdsEnd.IntersectWith(serverThreadIdsBegin);
317+
Assert.Equal((int) minPoolSize, serverThreadIdsEnd.Count);
318+
}
319+
#endif
320+
285321
private Task ClearPoolAsync(MySqlConnection connection)
286322
{
287323
#if BASELINE

tests/SideBySide/DebugOnlyTests.cs

Lines changed: 0 additions & 46 deletions
This file was deleted.

tests/SideBySide/SideBySide.csproj

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,6 @@
4444
<PackageReference Include="MySql.Data" Version="6.9.9" />
4545
</ItemGroup>
4646

47-
<ItemGroup Condition=" '$(Configuration)' != 'Debug' ">
48-
<Compile Remove="DebugOnlyTests.cs" />
49-
</ItemGroup>
50-
5147
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp1.1.2' ">
5248
<Compile Remove="TransactionScopeTests.cs" />
5349
</ItemGroup>

0 commit comments

Comments
 (0)