Skip to content

Commit 6e45dcd

Browse files
rusherbgrainger
andauthored
Rewrite server redirection logic. (#1499)
This is based on https://jira.mariadb.org/browse/MDEV-15935: first OK Packet can contain variable 'redirect_url' using "mariadb/mysql://[{user}[:{password}]@]{host}[:{port}]/[{db}[?{opt1}={value1}[&{opt2}={value2}]]]" format. Introduce IConnectionPoolMetadata to unify the code between pooled and non-pooled sessions. Standardise all redirection logging to use the MySqlConnection logger, whether pooled or non-pooled. Drop support for redirection format used by Azure Database for MySQL Single Server (which is now deprecated). Signed-off-by: rusher <[email protected]> Signed-off-by: Bradley Grainger <[email protected]> Co-authored-by: Bradley Grainger <[email protected]>
1 parent cb2e8e7 commit 6e45dcd

17 files changed

+418
-217
lines changed

.ci/config/config.compression+ssl.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
55
"PasswordlessUser": "no_password",
66
"SecondaryDatabase": "testdb2",
7-
"UnsupportedFeatures": "RsaEncryption,CachingSha2Password,Tls12,Tls13,UuidToBin",
7+
"UnsupportedFeatures": "CachingSha2Password,Redirection,RsaEncryption,Tls12,Tls13,UuidToBin",
88
"MySqlBulkLoaderLocalCsvFile": "../../../TestData/LoadData_UTF8_BOM_Unix.CSV",
99
"MySqlBulkLoaderLocalTsvFile": "../../../TestData/LoadData_UTF8_BOM_Unix.TSV",
1010
"CertificatesPath": "../../../../.ci/server/certs"

.ci/config/config.compression.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
55
"PasswordlessUser": "no_password",
66
"SecondaryDatabase": "testdb2",
7-
"UnsupportedFeatures": "Ed25519,QueryAttributes,StreamingResults,Tls11,UnixDomainSocket,ZeroDateTime",
7+
"UnsupportedFeatures": "Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,UnixDomainSocket,ZeroDateTime",
88
"MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV",
99
"MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV"
1010
}

.ci/config/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
55
"PasswordlessUser": "no_password",
66
"SecondaryDatabase": "testdb2",
7-
"UnsupportedFeatures": "Ed25519,QueryAttributes,StreamingResults,Tls11,UnixDomainSocket,ZeroDateTime",
7+
"UnsupportedFeatures": "Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,UnixDomainSocket,ZeroDateTime",
88
"MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV",
99
"MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV"
1010
}

.ci/config/config.ssl.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
55
"PasswordlessUser": "no_password",
66
"SecondaryDatabase": "testdb2",
7-
"UnsupportedFeatures": "RsaEncryption,CachingSha2Password,Tls12,Tls13,UuidToBin",
7+
"UnsupportedFeatures": "CachingSha2Password,Redirection,RsaEncryption,Tls12,Tls13,UuidToBin",
88
"MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV",
99
"MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV",
1010
"CertificatesPath": "../../../../.ci/server/certs"

azure-pipelines.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ jobs:
136136
arguments: '-c Release --no-restore'
137137
testRunTitle: ${{ format('{0}, $(Agent.OS), {1}, {2}', 'mysql:8.0', 'net472/net8.0', 'No SSL') }}
138138
env:
139-
DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,StreamingResults,Tls11,UnixDomainSocket'
139+
DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,UnixDomainSocket'
140140
DATA__CONNECTIONSTRING: 'server=localhost;port=3306;user id=mysqltest;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600;AllowPublicKeyRetrieval=True;UseCompression=True'
141141

142142
- job: windows_integration_tests_2
@@ -174,7 +174,7 @@ jobs:
174174
arguments: '-c Release --no-restore'
175175
testRunTitle: ${{ format('{0}, $(Agent.OS), {1}, {2}', 'mysql:8.0', 'net6.0', 'No SSL') }}
176176
env:
177-
DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,StreamingResults,Tls11,UnixDomainSocket'
177+
DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,UnixDomainSocket'
178178
DATA__CONNECTIONSTRING: 'server=localhost;port=3306;user id=mysqltest;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600;AllowPublicKeyRetrieval=True'
179179

180180
- job: linux_integration_tests
@@ -187,27 +187,27 @@ jobs:
187187
'MySQL 8.0':
188188
image: 'mysql:8.0'
189189
connectionStringExtra: 'AllowPublicKeyRetrieval=True'
190-
unsupportedFeatures: 'Ed25519,StreamingResults,Tls11,ZeroDateTime'
190+
unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,ZeroDateTime'
191191
'MySQL 8.4':
192192
image: 'mysql:8.4'
193193
connectionStringExtra: 'AllowPublicKeyRetrieval=True'
194-
unsupportedFeatures: 'Ed25519,StreamingResults,Tls11,ZeroDateTime'
194+
unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,ZeroDateTime'
195195
'MySQL 9.0':
196196
image: 'mysql:9.0'
197197
connectionStringExtra: 'AllowPublicKeyRetrieval=True'
198-
unsupportedFeatures: 'Ed25519,StreamingResults,Tls11,ZeroDateTime'
198+
unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,ZeroDateTime'
199199
'MariaDB 10.6':
200200
image: 'mariadb:10.6'
201201
connectionStringExtra: ''
202-
unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin'
202+
unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin,Redirection'
203203
'MariaDB 10.11':
204204
image: 'mariadb:10.11'
205205
connectionStringExtra: ''
206-
unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin'
206+
unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin,Redirection'
207207
'MariaDB 11.4':
208208
image: 'mariadb:11.4'
209209
connectionStringExtra: ''
210-
unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin'
210+
unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin,Redirection'
211211
steps:
212212
- template: '.ci/integration-tests-steps.yml'
213213
parameters:

src/MySqlConnector/Core/ConnectionPool.cs

Lines changed: 12 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,16 @@
88

99
namespace MySqlConnector.Core;
1010

11-
internal sealed class ConnectionPool : IDisposable
11+
internal sealed class ConnectionPool : IConnectionPoolMetadata, IDisposable
1212
{
1313
public int Id { get; }
1414

15+
ConnectionPool? IConnectionPoolMetadata.ConnectionPool => this;
16+
17+
int IConnectionPoolMetadata.Generation => m_generation;
18+
19+
int IConnectionPoolMetadata.GetNewSessionId() => Interlocked.Increment(ref m_lastSessionId);
20+
1521
public string? Name { get; }
1622

1723
public ConnectionSettings ConnectionSettings { get; }
@@ -95,6 +101,7 @@ public async ValueTask<ServerSession> GetSessionAsync(MySqlConnection connection
95101
m_leasedSessions.Add(session.Id, session);
96102
leasedSessionsCountPooled = m_leasedSessions.Count;
97103
}
104+
98105
MetricsReporter.AddUsed(this);
99106
ActivitySourceHelper.CopyTags(session.ActivityTags, activity);
100107
Log.ReturningPooledSession(m_logger, Id, session.Id, leasedSessionsCountPooled);
@@ -106,7 +113,8 @@ public async ValueTask<ServerSession> GetSessionAsync(MySqlConnection connection
106113
}
107114

108115
// create a new session
109-
session = await ConnectSessionAsync(connection, s_createdNewSession, startingTimestamp, activity, ioBehavior, cancellationToken).ConfigureAwait(false);
116+
session = await ServerSession.ConnectAndRedirectAsync(m_connectionLogger, m_logger, this, ConnectionSettings, m_loadBalancer,
117+
connection, s_createdNewSession, startingTimestamp, activity, ioBehavior, cancellationToken).ConfigureAwait(false);
110118
AdjustHostConnectionCount(session, 1);
111119
session.OwningConnection = new(connection);
112120
int leasedSessionsCountNew;
@@ -402,7 +410,8 @@ private async Task CreateMinimumPooledSessions(MySqlConnection connection, IOBeh
402410

403411
try
404412
{
405-
var session = await ConnectSessionAsync(connection, s_createdToReachMinimumPoolSize, Stopwatch.GetTimestamp(), null, ioBehavior, cancellationToken).ConfigureAwait(false);
413+
var session = await ServerSession.ConnectAndRedirectAsync(m_connectionLogger, m_logger, this, ConnectionSettings, m_loadBalancer,
414+
connection, s_createdToReachMinimumPoolSize, Stopwatch.GetTimestamp(), null, ioBehavior, cancellationToken).ConfigureAwait(false);
406415
AdjustHostConnectionCount(session, 1);
407416
lock (m_sessions)
408417
_ = m_sessions.AddFirst(session);
@@ -416,81 +425,6 @@ private async Task CreateMinimumPooledSessions(MySqlConnection connection, IOBeh
416425
}
417426
}
418427

419-
private async ValueTask<ServerSession> ConnectSessionAsync(MySqlConnection connection, Action<ILogger, int, string, Exception?> logMessage, long startingTimestamp, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken)
420-
{
421-
var session = new ServerSession(m_connectionLogger, this, m_generation, Interlocked.Increment(ref m_lastSessionId));
422-
if (m_logger.IsEnabled(LogLevel.Debug))
423-
logMessage(m_logger, Id, session.Id, null);
424-
string? statusInfo;
425-
try
426-
{
427-
statusInfo = await session.ConnectAsync(ConnectionSettings, connection, startingTimestamp, m_loadBalancer, activity, ioBehavior, cancellationToken).ConfigureAwait(false);
428-
}
429-
catch (Exception)
430-
{
431-
await session.DisposeAsync(ioBehavior, default).ConfigureAwait(false);
432-
throw;
433-
}
434-
435-
Exception? redirectionException = null;
436-
if (statusInfo is not null && statusInfo.StartsWith("Location: mysql://", StringComparison.Ordinal))
437-
{
438-
// server redirection string has the format "Location: mysql://{host}:{port}/user={userId}[&ttl={ttl}]"
439-
Log.HasServerRedirectionHeader(m_logger, session.Id, statusInfo);
440-
441-
if (ConnectionSettings.ServerRedirectionMode == MySqlServerRedirectionMode.Disabled)
442-
{
443-
Log.ServerRedirectionIsDisabled(m_logger, Id);
444-
}
445-
else if (Utility.TryParseRedirectionHeader(statusInfo, out var host, out var port, out var user))
446-
{
447-
if (host != ConnectionSettings.HostNames![0] || port != ConnectionSettings.Port || user != ConnectionSettings.UserID)
448-
{
449-
var redirectedSettings = ConnectionSettings.CloneWith(host, port, user);
450-
Log.OpeningNewConnection(m_logger, Id, host, port, user);
451-
var redirectedSession = new ServerSession(m_connectionLogger, this, m_generation, Interlocked.Increment(ref m_lastSessionId));
452-
try
453-
{
454-
_ = await redirectedSession.ConnectAsync(redirectedSettings, connection, startingTimestamp, m_loadBalancer, activity, ioBehavior, cancellationToken).ConfigureAwait(false);
455-
}
456-
catch (Exception ex)
457-
{
458-
Log.FailedToConnectRedirectedSession(m_logger, ex, Id, redirectedSession.Id);
459-
redirectionException = ex;
460-
}
461-
462-
if (redirectionException is null)
463-
{
464-
Log.ClosingSessionToUseRedirectedSession(m_logger, Id, session.Id, redirectedSession.Id);
465-
await session.DisposeAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
466-
return redirectedSession;
467-
}
468-
else
469-
{
470-
try
471-
{
472-
await redirectedSession.DisposeAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
473-
}
474-
catch (Exception)
475-
{
476-
}
477-
}
478-
}
479-
else
480-
{
481-
Log.SessionAlreadyConnectedToServer(m_logger, session.Id);
482-
}
483-
}
484-
}
485-
486-
if (ConnectionSettings.ServerRedirectionMode == MySqlServerRedirectionMode.Required)
487-
{
488-
Log.RequiresServerRedirection(m_logger, Id);
489-
throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Server does not support redirection", redirectionException);
490-
}
491-
return session;
492-
}
493-
494428
public static ConnectionPool? CreatePool(string connectionString, MySqlConnectorLoggingConfiguration loggingConfiguration, string? name)
495429
{
496430
// parse connection string and check for 'Pooling' setting; return 'null' if pooling is disabled

src/MySqlConnector/Core/ConnectionSettings.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,8 +270,12 @@ public int ConnectionTimeoutMilliseconds
270270

271271
private ConnectionSettings(ConnectionSettings other, string host, int port, string userId)
272272
{
273-
ConnectionStringBuilder = other.ConnectionStringBuilder;
274-
ConnectionString = other.ConnectionString;
273+
ConnectionStringBuilder = new MySqlConnectionStringBuilder(other.ConnectionString);
274+
ConnectionStringBuilder.Port = (uint)port;
275+
ConnectionStringBuilder.Server = host;
276+
ConnectionStringBuilder.UserID = userId;
277+
278+
ConnectionString = ConnectionStringBuilder.ConnectionString;
275279

276280
ConnectionProtocol = MySqlConnectionProtocol.Sockets;
277281
HostNames = [host];
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
namespace MySqlConnector.Core;
2+
3+
internal interface IConnectionPoolMetadata
4+
{
5+
/// <summary>
6+
/// Returns the <see cref="ConnectionPool"/> this <see cref="IConnectionPoolMetadata"/> is associated with,
7+
/// or <c>null</c> if it represents a non-pooled connection.
8+
/// </summary>
9+
ConnectionPool? ConnectionPool { get; }
10+
11+
/// <summary>
12+
/// Returns the ID of the connection pool, or 0 if this is a non-pooled connection.
13+
/// </summary>
14+
int Id { get; }
15+
16+
/// <summary>
17+
/// Returns the generation of the connection pool, or 0 if this is a non-pooled connection.
18+
/// </summary>
19+
int Generation { get; }
20+
21+
/// <summary>
22+
/// Returns a new session ID.
23+
/// </summary>
24+
/// <returns>A new session ID.</returns>
25+
int GetNewSessionId();
26+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace MySqlConnector.Core;
2+
3+
internal sealed class NonPooledConnectionPoolMetadata : IConnectionPoolMetadata
4+
{
5+
public static IConnectionPoolMetadata Instance { get; } = new NonPooledConnectionPoolMetadata();
6+
7+
public ConnectionPool? ConnectionPool => null;
8+
public int Id => 0;
9+
public int Generation => 0;
10+
public int GetNewSessionId() => Interlocked.Increment(ref m_lastId);
11+
12+
private int m_lastId;
13+
}

0 commit comments

Comments
 (0)