Skip to content

Commit 0b01393

Browse files
committed
Recover leaked connection pool entries. Fixes #251
1 parent 3196707 commit 0b01393

File tree

3 files changed

+68
-4
lines changed

3 files changed

+68
-4
lines changed

src/MySqlConnector/MySqlClient/ConnectionPool.cs

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ public async Task<MySqlSession> GetSessionAsync(IOBehavior ioBehavior, Cancellat
1414
{
1515
cancellationToken.ThrowIfCancellationRequested();
1616

17-
// wait for an open slot
17+
// if all sessions are used, see if any have been leaked and can be recovered
18+
// check at most once per second (although this isn't enforced via a mutex so multiple threads might block
19+
// on the lock in RecoverLeakedSessions in high-concurrency situations
20+
if (m_sessionSemaphore.CurrentCount == 0 && unchecked(((uint) Environment.TickCount) - m_lastRecoveryTime) >= 1000u)
21+
RecoverLeakedSessions();
22+
23+
// wait for an open slot (until the cancellationToken is cancelled, which is typically due to timeout)
1824
if (ioBehavior == IOBehavior.Asynchronous)
1925
await m_sessionSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
2026
else
@@ -46,14 +52,19 @@ public async Task<MySqlSession> GetSessionAsync(IOBehavior ioBehavior, Cancellat
4652
{
4753
await session.ResetConnectionAsync(m_connectionSettings, ioBehavior, cancellationToken).ConfigureAwait(false);
4854
}
55+
4956
// pooled session is ready to be used; return it
57+
lock (m_leasedSessions)
58+
m_leasedSessions.Add(session.Id, new WeakReference<MySqlSession>(session));
5059
return session;
5160
}
5261
}
5362

5463
// create a new session
55-
session = new MySqlSession(this, m_generation);
64+
session = new MySqlSession(this, m_generation, Interlocked.Increment(ref m_lastId));
5665
await session.ConnectAsync(m_connectionSettings, ioBehavior, cancellationToken).ConfigureAwait(false);
66+
lock (m_leasedSessions)
67+
m_leasedSessions.Add(session.Id, new WeakReference<MySqlSession>(session));
5768
return session;
5869
}
5970
catch
@@ -82,6 +93,8 @@ public void Return(MySqlSession session)
8293
{
8394
try
8495
{
96+
lock (m_leasedSessions)
97+
m_leasedSessions.Remove(session.Id);
8598
if (SessionIsHealthy(session))
8699
lock (m_sessions)
87100
m_sessions.AddFirst(session);
@@ -103,11 +116,36 @@ public async Task ClearAsync(IOBehavior ioBehavior, CancellationToken cancellati
103116

104117
public async Task ReapAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
105118
{
119+
RecoverLeakedSessions();
106120
if (m_connectionSettings.ConnectionIdleTimeout == 0)
107121
return;
108122
await CleanPoolAsync(ioBehavior, session => (DateTime.UtcNow - session.LastReturnedUtc).TotalSeconds >= m_connectionSettings.ConnectionIdleTimeout, true, cancellationToken).ConfigureAwait(false);
109123
}
110124

125+
/// <summary>
126+
/// Examines all the <see cref="WeakReference{MySqlSession}"/> in <see cref="m_leasedSessions"/> to determine if any
127+
/// <see cref="MySqlSession"/> objects have been garbage-collected. If so, assumes that the related <see cref="MySqlConnection"/>
128+
/// was not properly disposed but the associated server connection has been closed (by the finalizer). Releases the semaphore
129+
/// once for each leaked session to allow new client connections to be made.
130+
/// </summary>
131+
private void RecoverLeakedSessions()
132+
{
133+
var recoveredIds = new List<int>();
134+
lock (m_leasedSessions)
135+
{
136+
m_lastRecoveryTime = unchecked((uint) Environment.TickCount);
137+
foreach (var pair in m_leasedSessions)
138+
{
139+
if (!pair.Value.TryGetTarget(out var _))
140+
recoveredIds.Add(pair.Key);
141+
}
142+
foreach (var id in recoveredIds)
143+
m_leasedSessions.Remove(id);
144+
}
145+
if (recoveredIds.Count > 0)
146+
m_sessionSemaphore.Release(recoveredIds.Count);
147+
}
148+
111149
private async Task CleanPoolAsync(IOBehavior ioBehavior, Func<MySqlSession, bool> shouldCleanFn, bool respectMinPoolSize, CancellationToken cancellationToken)
112150
{
113151
// synchronize access to this method as only one clean routine should be run at a time
@@ -212,6 +250,7 @@ private ConnectionPool(ConnectionSettings cs)
212250
m_cleanSemaphore = new SemaphoreSlim(1);
213251
m_sessionSemaphore = new SemaphoreSlim(cs.MaximumPoolSize);
214252
m_sessions = new LinkedList<MySqlSession>();
253+
m_leasedSessions = new Dictionary<int, WeakReference<MySqlSession>>();
215254
}
216255

217256
static readonly ConcurrentDictionary<string, ConnectionPool> s_pools = new ConcurrentDictionary<string, ConnectionPool>();
@@ -241,5 +280,8 @@ private ConnectionPool(ConnectionSettings cs)
241280
readonly SemaphoreSlim m_sessionSemaphore;
242281
readonly LinkedList<MySqlSession> m_sessions;
243282
readonly ConnectionSettings m_connectionSettings;
283+
readonly Dictionary<int, WeakReference<MySqlSession>> m_leasedSessions;
284+
int m_lastId;
285+
uint m_lastRecoveryTime;
244286
}
245287
}

src/MySqlConnector/Serialization/MySqlSession.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,21 @@ namespace MySql.Data.Serialization
1717
internal sealed class MySqlSession
1818
{
1919
public MySqlSession()
20-
: this(null, 0)
20+
: this(null, 0, 0)
2121
{
2222
}
2323

24-
public MySqlSession(ConnectionPool pool, int poolGeneration)
24+
public MySqlSession(ConnectionPool pool, int poolGeneration, int id)
2525
{
2626
m_lock = new object();
2727
m_payloadCache = new ArraySegmentHolder<byte>();
28+
Id = id;
2829
CreatedUtc = DateTime.UtcNow;
2930
Pool = pool;
3031
PoolGeneration = poolGeneration;
3132
}
3233

34+
public int Id { get; }
3335
public ServerVersion ServerVersion { get; set; }
3436
public int ConnectionId { get; set; }
3537
public byte[] AuthPluginData { get; set; }

tests/SideBySide/ConnectionPool.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,26 @@ public async Task ExhaustConnectionPoolWithTimeout()
130130
connection.Dispose();
131131
}
132132

133+
[Fact]
134+
public void LeakConnections()
135+
{
136+
var csb = AppConfig.CreateConnectionStringBuilder();
137+
csb.Pooling = true;
138+
csb.MinimumPoolSize = 0;
139+
csb.MaximumPoolSize = 6;
140+
csb.ConnectionTimeout = 3u;
141+
142+
for (int i = 0; i < csb.MaximumPoolSize + 2; i++)
143+
{
144+
var connection = new MySqlConnection(csb.ConnectionString);
145+
connection.Open();
146+
147+
// have to GC for leaked connections to be removed from the pool
148+
GC.Collect();
149+
}
150+
}
151+
152+
133153
[Fact]
134154
public async Task WaitTimeout()
135155
{

0 commit comments

Comments
 (0)