Skip to content

Commit a433553

Browse files
committed
Merge reset-connection branch into master.
2 parents a3817d7 + 80a3763 commit a433553

File tree

10 files changed

+191
-9
lines changed

10 files changed

+191
-9
lines changed

docs/content/connection-options.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ Connection pooling is enabled by default. These options are used to configure it
169169
<tr id="ConnectionReset">
170170
<td>Connection Reset, ConnectionReset</td>
171171
<td><code>true</code></td>
172-
<td>If <code>true</code>, the connection state is reset when it is retrieved from the pool. The default value of <code>true</code> ensures that the connection is in the same state whether it’s newly created or retrieved from the pool. A value of <code>false</code> avoids making an additional server round trip when obtaining a connection, but the connection state is not reset, meaning that session variables and other session state changes from any previous use of the connection are carried over.</td>
172+
<td>If <code>true</code>, all connections retrieved from the pool will have been reset. The default value of <code>true</code> ensures that the connection is in the same state whether it’s newly created or retrieved from the pool. A value of <code>false</code> avoids making an additional server round trip to reset the connection, but the connection state is not reset, meaning that session variables and other session state changes from any previous use of the connection are carried over.</td>
173173
</tr>
174174
<tr id="ConnectionIdlePingTime">
175175
<td>Connection Idle Ping Time, ConnectionIdlePingTime <em>(Experimental)</em></td>
@@ -189,6 +189,11 @@ Connection pooling is enabled by default. These options are used to configure it
189189
<td>180</td>
190190
<td>The amount of time (in seconds) that a connection can remain idle in the pool. Any connection above <code>MinimumPoolSize</code> connections that is idle for longer than <code>ConnectionIdleTimeout</code> is subject to being closed by a background task. The background task runs every minute, or half of <code>ConnectionIdleTimeout</code>, whichever is more frequent. A value of zero (0) means pooled connections will never incur a ConnectionIdleTimeout, and if the pool grows to its maximum size, it will never get smaller.</td>
191191
</tr>
192+
<tr id="DeferConnectionReset">
193+
<td>Defer Connection Reset, DeferConnectionReset</td>
194+
<td><code>false</code></td>
195+
<td>If <code>true</code>, the connection state is not reset until the connection is retrieved from the pool. This was the default behaviour before MySqlConnector 1.3. The default value of <code>false</code> resets connections in the background after they’re closed which makes opening a connection faster, and releases server resources sooner.</td>
196+
</tr>
192197
<tr id="MaxPoolSize">
193198
<td>Maximum Pool Size, Max Pool Size, MaximumPoolsize, maxpoolsize</td>
194199
<td>100</td>
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using MySqlConnector.Logging;
6+
using MySqlConnector.Protocol.Serialization;
7+
8+
namespace MySqlConnector.Core
9+
{
10+
internal static class BackgroundConnectionResetHelper
11+
{
12+
public static void AddSession(ServerSession session, MySqlConnection? owningConnection)
13+
{
14+
var resetTask = session.TryResetConnectionAsync(session.Pool!.ConnectionSettings, IOBehavior.Asynchronous, default);
15+
lock (s_lock)
16+
s_sessions.Add(new SessionResetTask(session, resetTask, owningConnection));
17+
18+
if (Log.IsDebugEnabled())
19+
Log.Debug("Started Session{0} reset in background; waiting SessionCount: {1}.", session.Id, s_sessions.Count);
20+
21+
// release only if it is likely to succeed
22+
if (s_semaphore.CurrentCount == 0)
23+
{
24+
Log.Debug("Releasing semaphore.");
25+
try
26+
{
27+
s_semaphore.Release();
28+
}
29+
catch (SemaphoreFullException)
30+
{
31+
// ignore
32+
}
33+
}
34+
}
35+
36+
public static void Start()
37+
{
38+
Log.Info("Starting BackgroundConnectionResetHelper worker.");
39+
lock (s_lock)
40+
{
41+
if (s_workerTask is null)
42+
s_workerTask = Task.Run(async () => await ReturnSessionsAsync());
43+
}
44+
}
45+
46+
public static void Stop()
47+
{
48+
Log.Info("Stopping BackgroundConnectionResetHelper worker.");
49+
s_cancellationTokenSource.Cancel();
50+
Task? workerTask;
51+
lock (s_lock)
52+
workerTask = s_workerTask;
53+
54+
if (workerTask is not null)
55+
{
56+
try
57+
{
58+
workerTask.GetAwaiter().GetResult();
59+
}
60+
catch (OperationCanceledException)
61+
{
62+
}
63+
}
64+
Log.Info("Stopped BackgroundConnectionResetHelper worker.");
65+
}
66+
67+
public static async Task ReturnSessionsAsync()
68+
{
69+
Log.Info("Started BackgroundConnectionResetHelper worker.");
70+
71+
List<Task<bool>> localTasks = new();
72+
List<SessionResetTask> localSessions = new();
73+
74+
// keep running until stopped
75+
while (!s_cancellationTokenSource.IsCancellationRequested)
76+
{
77+
try
78+
{
79+
// block until AddSession releases the semaphore
80+
Log.Info("Waiting for semaphore.");
81+
await s_semaphore.WaitAsync(s_cancellationTokenSource.Token).ConfigureAwait(false);
82+
83+
// process all sessions that have started being returned
84+
while (true)
85+
{
86+
lock (s_lock)
87+
{
88+
if (s_sessions.Count == 0)
89+
{
90+
if (localTasks.Count == 0)
91+
break;
92+
}
93+
else
94+
{
95+
foreach (var session in s_sessions)
96+
{
97+
localSessions.Add(session);
98+
localTasks.Add(session.ResetTask);
99+
}
100+
s_sessions.Clear();
101+
}
102+
}
103+
104+
if (Log.IsDebugEnabled())
105+
Log.Debug("Found SessionCount {0} session(s) to return.", localSessions.Count);
106+
107+
while (localTasks.Count != 0)
108+
{
109+
var completedTask = await Task.WhenAny(localTasks).ConfigureAwait(false);
110+
var index = localTasks.IndexOf(completedTask);
111+
var session = localSessions[index].Session;
112+
var connection = localSessions[index].OwningConnection;
113+
localSessions.RemoveAt(index);
114+
localTasks.RemoveAt(index);
115+
await session.Pool!.ReturnAsync(IOBehavior.Asynchronous, session).ConfigureAwait(false);
116+
GC.KeepAlive(connection);
117+
}
118+
}
119+
}
120+
catch (Exception ex) when (!(ex is OperationCanceledException oce && oce.CancellationToken == s_cancellationTokenSource.Token))
121+
{
122+
Log.Error("Unhandled exception: {0}", ex);
123+
}
124+
}
125+
}
126+
127+
internal struct SessionResetTask
128+
{
129+
public SessionResetTask(ServerSession session, Task<bool> resetTask, MySqlConnection? owningConnection)
130+
{
131+
Session = session;
132+
ResetTask = resetTask;
133+
OwningConnection = owningConnection;
134+
}
135+
136+
public ServerSession Session { get; }
137+
public Task<bool> ResetTask { get; }
138+
public MySqlConnection? OwningConnection { get; }
139+
}
140+
141+
static readonly IMySqlConnectorLogger Log = MySqlConnectorLogManager.CreateLogger(nameof(BackgroundConnectionResetHelper));
142+
static readonly object s_lock = new();
143+
static readonly SemaphoreSlim s_semaphore = new(1, 1);
144+
static readonly CancellationTokenSource s_cancellationTokenSource = new();
145+
static readonly List<SessionResetTask> s_sessions = new();
146+
static Task? s_workerTask;
147+
}
148+
}

src/MySqlConnector/Core/ConnectionPool.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public async ValueTask<ServerSession> GetSessionAsync(MySqlConnection connection
6666
}
6767
else
6868
{
69-
if (ConnectionSettings.ConnectionReset || session.DatabaseOverride is not null)
69+
if ((ConnectionSettings.ConnectionReset && ConnectionSettings.DeferConnectionReset) || session.DatabaseOverride is not null)
7070
{
7171
reuseSession = await session.TryResetConnectionAsync(ConnectionSettings, ioBehavior, cancellationToken).ConfigureAwait(false);
7272
}
@@ -249,7 +249,7 @@ private async Task RecoverLeakedSessionsAsync(IOBehavior ioBehavior)
249249
else
250250
Log.Warn("Pool{0}: RecoveredSessionCount={1}", m_logArguments[0], recoveredSessions.Count);
251251
foreach (var session in recoveredSessions)
252-
await session.ReturnToPoolAsync(ioBehavior).ConfigureAwait(false);
252+
await session.ReturnToPoolAsync(ioBehavior, null).ConfigureAwait(false);
253253
}
254254

255255
private async Task CleanPoolAsync(IOBehavior ioBehavior, Func<ServerSession, bool> shouldCleanFn, bool respectMinPoolSize, CancellationToken cancellationToken)
@@ -413,6 +413,9 @@ private async Task CreateMinimumPooledSessions(IOBehavior ioBehavior, Cancellati
413413
// if we won the race to create the new pool, also store it under the original connection string
414414
if (connectionString != normalizedConnectionString)
415415
s_pools.GetOrAdd(connectionString, pool);
416+
417+
if (connectionSettings.ConnectionReset)
418+
BackgroundConnectionResetHelper.Start();
416419
}
417420
else if (pool != newPool && Log.IsInfoEnabled())
418421
{
@@ -534,7 +537,11 @@ static ConnectionPool()
534537
AppDomain.CurrentDomain.ProcessExit += OnAppDomainShutDown;
535538
}
536539

537-
static void OnAppDomainShutDown(object? sender, EventArgs e) => ClearPoolsAsync(IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
540+
static void OnAppDomainShutDown(object? sender, EventArgs e)
541+
{
542+
ClearPoolsAsync(IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
543+
BackgroundConnectionResetHelper.Stop();
544+
}
538545
#endif
539546

540547
static readonly IMySqlConnectorLogger Log = MySqlConnectorLogManager.CreateLogger(nameof(ConnectionPool));

src/MySqlConnector/Core/ConnectionSettings.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ public ConnectionSettings(MySqlConnectionStringBuilder csb)
116116
ConnectionReset = csb.ConnectionReset;
117117
ConnectionIdlePingTime = Math.Min(csb.ConnectionIdlePingTime, uint.MaxValue / 1000) * 1000;
118118
ConnectionIdleTimeout = (int) csb.ConnectionIdleTimeout;
119+
DeferConnectionReset = csb.DeferConnectionReset;
119120
if (csb.MinimumPoolSize > csb.MaximumPoolSize)
120121
throw new MySqlException("MaximumPoolSize must be greater than or equal to MinimumPoolSize");
121122
MinimumPoolSize = ToSigned(csb.MinimumPoolSize);
@@ -209,6 +210,7 @@ private static MySqlGuidFormat GetEffectiveGuidFormat(MySqlGuidFormat guidFormat
209210
public bool ConnectionReset { get; }
210211
public uint ConnectionIdlePingTime { get; }
211212
public int ConnectionIdleTimeout { get; }
213+
public bool DeferConnectionReset { get; }
212214
public int MinimumPoolSize { get; }
213215
public int MaximumPoolSize { get; }
214216

src/MySqlConnector/Core/ServerSession.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public ServerSession(ConnectionPool? pool, int poolGeneration, int id)
6868
public bool SupportsSessionTrack => m_supportsSessionTrack;
6969
public bool ProcAccessDenied { get; set; }
7070

71-
public Task ReturnToPoolAsync(IOBehavior ioBehavior)
71+
public Task ReturnToPoolAsync(IOBehavior ioBehavior, MySqlConnection? owningConnection)
7272
{
7373
if (Log.IsDebugEnabled())
7474
{
@@ -78,7 +78,10 @@ public Task ReturnToPoolAsync(IOBehavior ioBehavior)
7878
LastReturnedTicks = unchecked((uint) Environment.TickCount);
7979
if (Pool is null)
8080
return Utility.CompletedTask;
81-
return Pool.ReturnAsync(ioBehavior, this);
81+
if (!Pool.ConnectionSettings.ConnectionReset || Pool.ConnectionSettings.DeferConnectionReset)
82+
return Pool.ReturnAsync(ioBehavior, this);
83+
BackgroundConnectionResetHelper.AddSession(this, owningConnection);
84+
return Utility.CompletedTask;
8285
}
8386

8487
public bool IsConnected

src/MySqlConnector/MySqlConnection.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -925,7 +925,7 @@ m_enlistedTransaction is null &&
925925
m_cachedProcedures = null;
926926
if (m_session is not null)
927927
{
928-
await m_session.ReturnToPoolAsync(ioBehavior).ConfigureAwait(false);
928+
await m_session.ReturnToPoolAsync(ioBehavior, this).ConfigureAwait(false);
929929
m_session = null;
930930
}
931931
if (changeState)
@@ -994,7 +994,7 @@ private async Task DoCloseAsync(bool changeState, IOBehavior ioBehavior)
994994
{
995995
if (GetInitializedConnectionSettings().Pooling)
996996
{
997-
await m_session.ReturnToPoolAsync(ioBehavior).ConfigureAwait(false);
997+
await m_session.ReturnToPoolAsync(ioBehavior, this).ConfigureAwait(false);
998998
}
999999
else
10001000
{

src/MySqlConnector/MySqlConnectionStringBuilder.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,12 @@ public uint ConnectionIdleTimeout
181181
set => MySqlConnectionStringOption.ConnectionIdleTimeout.SetValue(this, value);
182182
}
183183

184+
public bool DeferConnectionReset
185+
{
186+
get => MySqlConnectionStringOption.DeferConnectionReset.GetValue(this);
187+
set => MySqlConnectionStringOption.DeferConnectionReset.SetValue(this, value);
188+
}
189+
184190
public uint MinimumPoolSize
185191
{
186192
get => MySqlConnectionStringOption.MinimumPoolSize.GetValue(this);
@@ -445,6 +451,7 @@ internal abstract class MySqlConnectionStringOption
445451
public static readonly MySqlConnectionStringValueOption<bool> Pooling;
446452
public static readonly MySqlConnectionStringValueOption<uint> ConnectionLifeTime;
447453
public static readonly MySqlConnectionStringValueOption<bool> ConnectionReset;
454+
public static readonly MySqlConnectionStringValueOption<bool> DeferConnectionReset;
448455
public static readonly MySqlConnectionStringValueOption<uint> ConnectionIdlePingTime;
449456
public static readonly MySqlConnectionStringValueOption<uint> ConnectionIdleTimeout;
450457
public static readonly MySqlConnectionStringValueOption<uint> MinimumPoolSize;
@@ -627,6 +634,10 @@ static MySqlConnectionStringOption()
627634
keys: new[] { "Connection Reset", "ConnectionReset" },
628635
defaultValue: true));
629636

637+
AddOption(DeferConnectionReset = new(
638+
keys: new[] { "Defer Connection Reset", "DeferConnectionReset" },
639+
defaultValue: false));
640+
630641
AddOption(ConnectionIdlePingTime = new(
631642
keys: new[] { "Connection Idle Ping Time", "ConnectionIdlePingTime" },
632643
defaultValue: 0));

tests/MySqlConnector.Tests/MySqlConnectionStringBuilderTests.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public void Defaults()
3636
Assert.False(csb.ConnectionReset);
3737
#else
3838
Assert.True(csb.ConnectionReset);
39+
Assert.False(csb.DeferConnectionReset);
3940
#endif
4041
Assert.Equal(15u, csb.ConnectionTimeout);
4142
Assert.False(csb.ConvertZeroDateTime);
@@ -123,6 +124,7 @@ public void ParseConnectionString()
123124
"cancellation timeout = -1;" +
124125
"connection idle ping time=60;" +
125126
"connectionidletimeout=30;" +
127+
"defer connection reset=true;" +
126128
"forcesynchronous=true;" +
127129
"ignore command transaction=true;" +
128130
"server rsa public key file=rsa.pem;" +
@@ -182,6 +184,7 @@ public void ParseConnectionString()
182184
Assert.Equal("My Test Application", csb.ApplicationName);
183185
Assert.Equal(60u, csb.ConnectionIdlePingTime);
184186
Assert.Equal(30u, csb.ConnectionIdleTimeout);
187+
Assert.True(csb.DeferConnectionReset);
185188
Assert.True(csb.ForceSynchronous);
186189
Assert.True(csb.IgnoreCommandTransaction);
187190
Assert.Equal("rsa.pem", csb.ServerRsaPublicKeyFile);

tests/SideBySide/ConnectionPool.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@ public async Task WaitTimeout()
155155
csb.Pooling = true;
156156
csb.MinimumPoolSize = 0;
157157
csb.MaximumPoolSize = 1;
158+
#if !BASELINE
159+
csb.DeferConnectionReset = true;
160+
#endif
158161
int serverThread;
159162

160163
using (var connection = new MySqlConnection(csb.ConnectionString))

tests/SideBySide/TransactionScopeTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,7 @@ public void ReusingConnectionInOneTransactionWithoutAutoEnlistDoesNotDeadlockWit
608608
public void ReusingConnectionInOneTransactionReusesPhysicalConnection(string connectionString)
609609
{
610610
connectionString = AppConfig.ConnectionString + ";" + connectionString;
611-
using (new CommittableTransaction())
611+
using (var transactionScope = new TransactionScope())
612612
{
613613
using (var connection = new MySqlConnection(connectionString))
614614
{

0 commit comments

Comments
 (0)