Skip to content

Commit f2255cc

Browse files
committed
Reduce allocations when opening a connection.
Defer creation of MySqlConnectionStringBuilder and ConnectionSettings until it is known that a new ConnectionPool needs to be created.
1 parent 3590c51 commit f2255cc

File tree

4 files changed

+91
-45
lines changed

4 files changed

+91
-45
lines changed

src/MySqlConnector/Core/ConnectionPool.cs

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ internal sealed class ConnectionPool
1414
{
1515
public int Id { get; }
1616

17+
public ConnectionSettings ConnectionSettings { get; }
18+
1719
public async Task<ServerSession> GetSessionAsync(MySqlConnection connection, IOBehavior ioBehavior, CancellationToken cancellationToken)
1820
{
1921
cancellationToken.ThrowIfCancellationRequested();
@@ -27,7 +29,7 @@ public async Task<ServerSession> GetSessionAsync(MySqlConnection connection, IOB
2729
RecoverLeakedSessions();
2830
}
2931

30-
if (m_connectionSettings.MinimumPoolSize > 0)
32+
if (ConnectionSettings.MinimumPoolSize > 0)
3133
await CreateMinimumPooledSessions(ioBehavior, cancellationToken).ConfigureAwait(false);
3234

3335
// wait for an open slot (until the cancellationToken is cancelled, which is typically due to timeout)
@@ -61,9 +63,9 @@ public async Task<ServerSession> GetSessionAsync(MySqlConnection connection, IOB
6163
}
6264
else
6365
{
64-
if (m_connectionSettings.ConnectionReset)
66+
if (ConnectionSettings.ConnectionReset)
6567
{
66-
reuseSession = await session.TryResetConnectionAsync(m_connectionSettings, ioBehavior, cancellationToken).ConfigureAwait(false);
68+
reuseSession = await session.TryResetConnectionAsync(ConnectionSettings, ioBehavior, cancellationToken).ConfigureAwait(false);
6769
}
6870
else
6971
{
@@ -98,7 +100,7 @@ public async Task<ServerSession> GetSessionAsync(MySqlConnection connection, IOB
98100
session = new ServerSession(this, m_generation, Interlocked.Increment(ref m_lastSessionId));
99101
if (Log.IsInfoEnabled())
100102
Log.Info("{0} no pooled session available; created new Session{1}", m_logArguments[0], session.Id);
101-
await session.ConnectAsync(m_connectionSettings, m_loadBalancer, ioBehavior, cancellationToken).ConfigureAwait(false);
103+
await session.ConnectAsync(ConnectionSettings, m_loadBalancer, ioBehavior, cancellationToken).ConfigureAwait(false);
102104
AdjustHostConnectionCount(session, 1);
103105
session.OwningConnection = new WeakReference<MySqlConnection>(connection);
104106
int leasedSessionsCountNew;
@@ -126,8 +128,8 @@ private bool SessionIsHealthy(ServerSession session)
126128
return false;
127129
if (session.DatabaseOverride != null)
128130
return false;
129-
if (m_connectionSettings.ConnectionLifeTime > 0
130-
&& (DateTime.UtcNow - session.CreatedUtc).TotalSeconds >= m_connectionSettings.ConnectionLifeTime)
131+
if (ConnectionSettings.ConnectionLifeTime > 0
132+
&& (DateTime.UtcNow - session.CreatedUtc).TotalSeconds >= ConnectionSettings.ConnectionLifeTime)
131133
return false;
132134

133135
return true;
@@ -175,9 +177,9 @@ public async Task ReapAsync(IOBehavior ioBehavior, CancellationToken cancellatio
175177
{
176178
Log.Debug("{0} reaping connection pool", m_logArguments);
177179
RecoverLeakedSessions();
178-
if (m_connectionSettings.ConnectionIdleTimeout == 0)
180+
if (ConnectionSettings.ConnectionIdleTimeout == 0)
179181
return;
180-
await CleanPoolAsync(ioBehavior, session => (DateTime.UtcNow - session.LastReturnedUtc).TotalSeconds >= m_connectionSettings.ConnectionIdleTimeout, true, cancellationToken).ConfigureAwait(false);
182+
await CleanPoolAsync(ioBehavior, session => (DateTime.UtcNow - session.LastReturnedUtc).TotalSeconds >= ConnectionSettings.ConnectionIdleTimeout, true, cancellationToken).ConfigureAwait(false);
181183
}
182184

183185
/// <summary>
@@ -196,7 +198,7 @@ public Dictionary<string, CachedProcedure> GetProcedureCache()
196198
}
197199
return procedureCache;
198200
}
199-
201+
200202
/// <summary>
201203
/// Examines all the <see cref="ServerSession"/> objects in <see cref="m_leasedSessions"/> to determine if any
202204
/// have an owning <see cref="MySqlConnection"/> that has been garbage-collected. If so, assumes that the connection
@@ -238,7 +240,7 @@ private async Task CleanPoolAsync(IOBehavior ioBehavior, Func<ServerSession, boo
238240
// if respectMinPoolSize is true, return if (leased sessions + waiting sessions <= minPoolSize)
239241
if (respectMinPoolSize)
240242
lock (m_sessions)
241-
if (m_connectionSettings.MaximumPoolSize - m_sessionSemaphore.CurrentCount + m_sessions.Count <= m_connectionSettings.MinimumPoolSize)
243+
if (ConnectionSettings.MaximumPoolSize - m_sessionSemaphore.CurrentCount + m_sessions.Count <= ConnectionSettings.MinimumPoolSize)
242244
return;
243245

244246
// try to get an open slot; if this fails, connection pool is full and sessions will be disposed when returned to pool
@@ -301,7 +303,7 @@ private async Task CreateMinimumPooledSessions(IOBehavior ioBehavior, Cancellati
301303
lock (m_sessions)
302304
{
303305
// check if the desired minimum number of sessions have been created
304-
if (m_connectionSettings.MaximumPoolSize - m_sessionSemaphore.CurrentCount + m_sessions.Count >= m_connectionSettings.MinimumPoolSize)
306+
if (ConnectionSettings.MaximumPoolSize - m_sessionSemaphore.CurrentCount + m_sessions.Count >= ConnectionSettings.MinimumPoolSize)
305307
return;
306308
}
307309

@@ -322,7 +324,7 @@ private async Task CreateMinimumPooledSessions(IOBehavior ioBehavior, Cancellati
322324
{
323325
var session = new ServerSession(this, m_generation, Interlocked.Increment(ref m_lastSessionId));
324326
Log.Info("{0} created Session{1} to reach minimum pool size", m_logArguments[0], session.Id);
325-
await session.ConnectAsync(m_connectionSettings, m_loadBalancer, ioBehavior, cancellationToken).ConfigureAwait(false);
327+
await session.ConnectAsync(ConnectionSettings, m_loadBalancer, ioBehavior, cancellationToken).ConfigureAwait(false);
326328
AdjustHostConnectionCount(session, 1);
327329
lock (m_sessions)
328330
m_sessions.AddFirst(session);
@@ -335,29 +337,51 @@ private async Task CreateMinimumPooledSessions(IOBehavior ioBehavior, Cancellati
335337
}
336338
}
337339

338-
public static ConnectionPool GetPool(ConnectionSettings cs)
340+
public static ConnectionPool GetPool(string connectionString)
339341
{
340-
if (!cs.Pooling)
341-
return null;
342+
// check if pool has already been created for this exact connection string
343+
try
344+
{
345+
s_poolLock.EnterReadLock();
346+
if (s_pools.TryGetValue(connectionString, out var pool))
347+
return pool;
348+
}
349+
finally
350+
{
351+
s_poolLock.ExitReadLock();
352+
}
342353

343-
var key = cs.ConnectionString;
354+
// parse connection string and check for 'Pooling' setting
355+
var connectionStringBuilder = new MySqlConnectionStringBuilder(connectionString);
356+
if (!connectionStringBuilder.Pooling)
357+
return null;
344358

359+
// check for pool using normalized form of connection string
360+
var normalizedConnectionString = connectionStringBuilder.ConnectionString;
345361
try
346362
{
347363
s_poolLock.EnterReadLock();
348-
if (s_pools.TryGetValue(key, out var pool))
364+
if (s_pools.TryGetValue(normalizedConnectionString, out var pool))
349365
return pool;
366+
// TODO: Add an entry using 'connectionString'?
350367
}
351368
finally
352369
{
353370
s_poolLock.ExitReadLock();
354371
}
355372

373+
var connectionSettings = new ConnectionSettings(connectionStringBuilder);
374+
356375
try
357376
{
358377
s_poolLock.EnterWriteLock();
359-
if (!s_pools.TryGetValue(key, out var pool))
360-
pool = s_pools[key] = new ConnectionPool(cs);
378+
if (!s_pools.TryGetValue(normalizedConnectionString, out var pool))
379+
{
380+
pool = new ConnectionPool(connectionSettings);
381+
s_pools[normalizedConnectionString] = pool;
382+
if (normalizedConnectionString != connectionString)
383+
s_pools[connectionString] = pool;
384+
}
361385
return pool;
362386
}
363387
finally
@@ -393,7 +417,7 @@ private static IReadOnlyList<ConnectionPool> GetAllPools()
393417

394418
private ConnectionPool(ConnectionSettings cs)
395419
{
396-
m_connectionSettings = cs;
420+
ConnectionSettings = cs;
397421
m_generation = 0;
398422
m_cleanSemaphore = new SemaphoreSlim(1);
399423
m_sessionSemaphore = new SemaphoreSlim(cs.MaximumPoolSize);
@@ -414,10 +438,7 @@ private ConnectionPool(ConnectionSettings cs)
414438
Id = Interlocked.Increment(ref s_poolId);
415439
m_logArguments = new object[] { "Pool{0}".FormatInvariant(Id) };
416440
if (Log.IsInfoEnabled())
417-
{
418-
var csb = new MySqlConnectionStringBuilder(cs.ConnectionString);
419-
Log.Info("{0} creating new connection pool for {1}", m_logArguments[0], csb.GetConnectionString(includePassword: false));
420-
}
441+
Log.Info("{0} creating new connection pool for {1}", m_logArguments[0], cs.ConnectionStringBuilder.GetConnectionString(includePassword: false));
421442
}
422443

423444
private void AdjustHostConnectionCount(ServerSession session, int delta)
@@ -472,7 +493,6 @@ public IEnumerable<string> LoadBalance(IReadOnlyList<string> hosts)
472493
readonly SemaphoreSlim m_cleanSemaphore;
473494
readonly SemaphoreSlim m_sessionSemaphore;
474495
readonly LinkedList<ServerSession> m_sessions;
475-
readonly ConnectionSettings m_connectionSettings;
476496
readonly Dictionary<string, ServerSession> m_leasedSessions;
477497
readonly ILoadBalancer m_loadBalancer;
478498
readonly Dictionary<string, int> m_hostSessions;

src/MySqlConnector/Core/ConnectionSettings.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ internal sealed class ConnectionSettings
1010
{
1111
public ConnectionSettings(MySqlConnectionStringBuilder csb)
1212
{
13+
ConnectionStringBuilder = csb;
1314
ConnectionString = csb.ConnectionString;
1415

1516
// Base Options
@@ -64,6 +65,12 @@ public ConnectionSettings(MySqlConnectionStringBuilder csb)
6465
UseCompression = csb.UseCompression;
6566
}
6667

68+
/// <summary>
69+
/// The <see cref="MySqlConnectionStringBuilder" /> that was used to create this <see cref="ConnectionSettings" />.!--
70+
/// This object must not be mutated.
71+
/// </summary>
72+
public MySqlConnectionStringBuilder ConnectionStringBuilder { get; }
73+
6774
// Base Options
6875
public string ConnectionString { get; }
6976
public ConnectionType ConnectionType { get; }

src/MySqlConnector/MySql.Data.MySqlClient/MySqlConnection.cs

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,9 @@ private async ValueTask<bool> PingAsync(IOBehavior ioBehavior, CancellationToken
154154

155155
public override void Open() => OpenAsync(IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
156156

157-
public override Task OpenAsync(CancellationToken cancellationToken) =>
158-
OpenAsync(AsyncIOBehavior, cancellationToken);
157+
public override Task OpenAsync(CancellationToken cancellationToken) => OpenAsync(default, cancellationToken);
159158

160-
private async Task OpenAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
159+
private async Task OpenAsync(IOBehavior? ioBehavior, CancellationToken cancellationToken)
161160
{
162161
VerifyNotDisposed();
163162
if (State != ConnectionState.Closed)
@@ -191,23 +190,28 @@ private async Task OpenAsync(IOBehavior ioBehavior, CancellationToken cancellati
191190

192191
public override string ConnectionString
193192
{
194-
get => m_connectionStringBuilder.GetConnectionString(!m_hasBeenOpened || m_connectionSettings.PersistSecurityInfo);
193+
get
194+
{
195+
if (!m_hasBeenOpened)
196+
return m_connectionString;
197+
var connectionStringBuilder = GetConnectionSettings().ConnectionStringBuilder;
198+
return connectionStringBuilder.GetConnectionString(connectionStringBuilder.PersistSecurityInfo);
199+
}
195200
set
196201
{
197202
if (m_hasBeenOpened)
198203
throw new InvalidOperationException("Cannot change connection string on a connection that has already been opened.");
199-
m_connectionStringBuilder = new MySqlConnectionStringBuilder(value);
200-
m_connectionSettings = new ConnectionSettings(m_connectionStringBuilder);
204+
m_connectionString = value;
201205
}
202206
}
203207

204-
public override string Database => m_session?.DatabaseOverride ?? m_connectionSettings.Database;
208+
public override string Database => m_session?.DatabaseOverride ?? GetConnectionSettings().Database;
205209

206210
public override ConnectionState State => m_connectionState;
207211

208-
public override string DataSource => (m_connectionSettings.ConnectionType == ConnectionType.Tcp
209-
? string.Join(",", m_connectionSettings.HostNames)
210-
: m_connectionSettings.UnixSocket) ?? "";
212+
public override string DataSource => (GetConnectionSettings().ConnectionType == ConnectionType.Tcp
213+
? string.Join(",", GetConnectionSettings().HostNames)
214+
: GetConnectionSettings().UnixSocket) ?? "";
211215

212216
public override string ServerVersion => m_session.ServerVersion.OriginalString;
213217

@@ -225,7 +229,7 @@ private static async Task ClearPoolAsync(MySqlConnection connection, IOBehavior
225229
if (connection == null)
226230
throw new ArgumentNullException(nameof(connection));
227231

228-
var pool = ConnectionPool.GetPool(connection.m_connectionSettings);
232+
var pool = ConnectionPool.GetPool(connection.m_connectionString);
229233
if (pool != null)
230234
await pool.ClearAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
231235
}
@@ -288,7 +292,7 @@ internal void Cancel(MySqlCommand command)
288292
try
289293
{
290294
// open a dedicated connection to the server to kill the active query
291-
var csb = new MySqlConnectionStringBuilder(m_connectionStringBuilder.GetConnectionString(includePassword: true));
295+
var csb = new MySqlConnectionStringBuilder(m_connectionString);
292296
csb.Pooling = false;
293297
if (m_session.IPAddress != null)
294298
csb.Server = m_session.IPAddress.ToString();
@@ -343,10 +347,10 @@ internal async Task<CachedProcedure> GetCachedProcedure(IOBehavior ioBehavior, s
343347
internal MySqlTransaction CurrentTransaction { get; set; }
344348
internal bool AllowUserVariables => m_connectionSettings.AllowUserVariables;
345349
internal bool ConvertZeroDateTime => m_connectionSettings.ConvertZeroDateTime;
346-
internal int DefaultCommandTimeout => m_connectionSettings.DefaultCommandTimeout;
350+
internal int DefaultCommandTimeout => GetConnectionSettings().DefaultCommandTimeout;
347351
internal bool OldGuids => m_connectionSettings.OldGuids;
348352
internal bool TreatTinyAsBoolean => m_connectionSettings.TreatTinyAsBoolean;
349-
internal IOBehavior AsyncIOBehavior => m_connectionSettings.ForceSynchronous ? IOBehavior.Synchronous : IOBehavior.Asynchronous;
353+
internal IOBehavior AsyncIOBehavior => GetConnectionSettings().ForceSynchronous ? IOBehavior.Synchronous : IOBehavior.Asynchronous;
350354

351355
internal MySqlSslMode SslMode => m_connectionSettings.SslMode;
352356

@@ -367,20 +371,23 @@ internal void FinishQuerying()
367371
m_activeReader = null;
368372
}
369373

370-
private async Task<ServerSession> CreateSessionAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
374+
private async Task<ServerSession> CreateSessionAsync(IOBehavior? ioBehavior, CancellationToken cancellationToken)
371375
{
376+
var pool = ConnectionPool.GetPool(m_connectionString);
377+
m_connectionSettings = pool?.ConnectionSettings ?? new ConnectionSettings(new MySqlConnectionStringBuilder(m_connectionString));
378+
var actualIOBehavior = ioBehavior ?? (m_connectionSettings.ForceSynchronous ? IOBehavior.Synchronous : IOBehavior.Asynchronous);
379+
372380
var connectTimeout = m_connectionSettings.ConnectionTimeout == 0 ? Timeout.InfiniteTimeSpan : TimeSpan.FromMilliseconds(m_connectionSettings.ConnectionTimeoutMilliseconds);
373381
using (var timeoutSource = new CancellationTokenSource(connectTimeout))
374382
using (var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutSource.Token))
375383
{
376384
try
377385
{
378386
// get existing session from the pool if possible
379-
if (m_connectionSettings.Pooling)
387+
if (pool != null)
380388
{
381-
var pool = ConnectionPool.GetPool(m_connectionSettings);
382389
// this returns an open session
383-
return await pool.GetSessionAsync(this, ioBehavior, linkedSource.Token).ConfigureAwait(false);
390+
return await pool.GetSessionAsync(this, actualIOBehavior, linkedSource.Token).ConfigureAwait(false);
384391
}
385392
else
386393
{
@@ -390,7 +397,7 @@ private async Task<ServerSession> CreateSessionAsync(IOBehavior ioBehavior, Canc
390397

391398
var session = new ServerSession();
392399
Log.Info("Created new non-pooled Session{0}", session.Id);
393-
await session.ConnectAsync(m_connectionSettings, loadBalancer, ioBehavior, linkedSource.Token).ConfigureAwait(false);
400+
await session.ConnectAsync(m_connectionSettings, loadBalancer, actualIOBehavior, linkedSource.Token).ConfigureAwait(false);
394401
return session;
395402
}
396403
}
@@ -472,9 +479,16 @@ private void CloseDatabase()
472479
}
473480
}
474481

482+
private ConnectionSettings GetConnectionSettings()
483+
{
484+
if (m_connectionSettings == null)
485+
m_connectionSettings = new ConnectionSettings(new MySqlConnectionStringBuilder(m_connectionString));
486+
return m_connectionSettings;
487+
}
488+
475489
static readonly IMySqlConnectorLogger Log = MySqlConnectorLogManager.CreateLogger(nameof(MySqlConnection));
476490

477-
MySqlConnectionStringBuilder m_connectionStringBuilder;
491+
string m_connectionString;
478492
ConnectionSettings m_connectionSettings;
479493
ServerSession m_session;
480494
ConnectionState m_connectionState;

tests/Conformance.Tests/ConnectionTests.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ public ConnectionTests(DbFactoryFixture fixture)
1010
{
1111
}
1212

13+
[Fact(Skip = "Throws MySqlException when it attempts to connect.")]
14+
public override void Set_ConnectionString_throws_when_invalid()
15+
{
16+
}
17+
1318
[Fact(Skip = "Throws MySqlException when it attempts to connect, not InvalidOperationException before connecting")]
1419
public override void Open_throws_when_no_connection_string()
1520
{

0 commit comments

Comments
 (0)