From 3ea4d86c4ae8d69493b99b4a1d4b1a84c2b5d4b4 Mon Sep 17 00:00:00 2001 From: rusher Date: Sat, 20 Jul 2024 12:31:35 +0200 Subject: [PATCH 01/11] Changing Permit redirection 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. Signed-off-by: rusher --- src/MySqlConnector/Core/ConnectionPool.cs | 98 ++------- src/MySqlConnector/Core/ConnectionSettings.cs | 8 +- src/MySqlConnector/Core/ServerSession.cs | 89 +++++++- src/MySqlConnector/Logging/Log.cs | 28 +-- src/MySqlConnector/MySqlConnection.cs | 21 +- .../Protocol/Payloads/OkPayload.cs | 20 +- src/MySqlConnector/Utilities/Utility.cs | 79 +++---- tests/IntegrationTests/RedirectionTests.cs | 199 ++++++++++++++++++ tests/MySqlConnector.Tests/UtilityTests.cs | 47 ++--- 9 files changed, 384 insertions(+), 205 deletions(-) create mode 100644 tests/IntegrationTests/RedirectionTests.cs diff --git a/src/MySqlConnector/Core/ConnectionPool.cs b/src/MySqlConnector/Core/ConnectionPool.cs index 17227b631..56d8da03b 100644 --- a/src/MySqlConnector/Core/ConnectionPool.cs +++ b/src/MySqlConnector/Core/ConnectionPool.cs @@ -68,8 +68,11 @@ public async ValueTask GetSessionAsync(MySqlConnection connection if (ConnectionSettings.ConnectionReset || session.DatabaseOverride is not null) { if (timeoutMilliseconds != 0) - session.SetTimeout(Math.Max(1, timeoutMilliseconds - Utility.GetElapsedMilliseconds(startingTimestamp))); - reuseSession = await session.TryResetConnectionAsync(ConnectionSettings, connection, ioBehavior, cancellationToken).ConfigureAwait(false); + session.SetTimeout(Math.Max(1, + timeoutMilliseconds - Utility.GetElapsedMilliseconds(startingTimestamp))); + reuseSession = await session + .TryResetConnectionAsync(ConnectionSettings, connection, ioBehavior, cancellationToken) + .ConfigureAwait(false); session.SetTimeout(Constants.InfiniteTimeout); } else @@ -95,18 +98,24 @@ public async ValueTask GetSessionAsync(MySqlConnection connection m_leasedSessions.Add(session.Id, session); leasedSessionsCountPooled = m_leasedSessions.Count; } + MetricsReporter.AddUsed(this); ActivitySourceHelper.CopyTags(session.ActivityTags, activity); Log.ReturningPooledSession(m_logger, Id, session.Id, leasedSessionsCountPooled); session.LastLeasedTimestamp = Stopwatch.GetTimestamp(); - MetricsReporter.RecordWaitTime(this, Utility.GetElapsedSeconds(startingTimestamp, session.LastLeasedTimestamp)); + MetricsReporter.RecordWaitTime(this, + Utility.GetElapsedSeconds(startingTimestamp, session.LastLeasedTimestamp)); return session; } } // create a new session - session = await ConnectSessionAsync(connection, s_createdNewSession, startingTimestamp, activity, ioBehavior, cancellationToken).ConfigureAwait(false); + session = await ServerSession.ConnectAndRedirectAsync( + () => new ServerSession(m_connectionLogger, this, m_generation, + Interlocked.Increment(ref m_lastSessionId)), m_logger, Id, ConnectionSettings, m_loadBalancer, + connection, s_createdNewSession, startingTimestamp, activity, ioBehavior, cancellationToken) + .ConfigureAwait(false); AdjustHostConnectionCount(session, 1); session.OwningConnection = new(connection); int leasedSessionsCountNew; @@ -402,7 +411,11 @@ private async Task CreateMinimumPooledSessions(MySqlConnection connection, IOBeh try { - var session = await ConnectSessionAsync(connection, s_createdToReachMinimumPoolSize, Stopwatch.GetTimestamp(), null, ioBehavior, cancellationToken).ConfigureAwait(false); + var session = await ServerSession.ConnectAndRedirectAsync( + () => new ServerSession(m_connectionLogger, this, m_generation, + Interlocked.Increment(ref m_lastSessionId)), m_logger, Id, ConnectionSettings, m_loadBalancer, + connection, s_createdToReachMinimumPoolSize, Stopwatch.GetTimestamp(), null, ioBehavior, + cancellationToken).ConfigureAwait(false); AdjustHostConnectionCount(session, 1); lock (m_sessions) _ = m_sessions.AddFirst(session); @@ -416,81 +429,6 @@ private async Task CreateMinimumPooledSessions(MySqlConnection connection, IOBeh } } - private async ValueTask ConnectSessionAsync(MySqlConnection connection, Action logMessage, long startingTimestamp, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken) - { - var session = new ServerSession(m_connectionLogger, this, m_generation, Interlocked.Increment(ref m_lastSessionId)); - if (m_logger.IsEnabled(LogLevel.Debug)) - logMessage(m_logger, Id, session.Id, null); - string? statusInfo; - try - { - statusInfo = await session.ConnectAsync(ConnectionSettings, connection, startingTimestamp, m_loadBalancer, activity, ioBehavior, cancellationToken).ConfigureAwait(false); - } - catch (Exception) - { - await session.DisposeAsync(ioBehavior, default).ConfigureAwait(false); - throw; - } - - Exception? redirectionException = null; - if (statusInfo is not null && statusInfo.StartsWith("Location: mysql://", StringComparison.Ordinal)) - { - // server redirection string has the format "Location: mysql://{host}:{port}/user={userId}[&ttl={ttl}]" - Log.HasServerRedirectionHeader(m_logger, session.Id, statusInfo); - - if (ConnectionSettings.ServerRedirectionMode == MySqlServerRedirectionMode.Disabled) - { - Log.ServerRedirectionIsDisabled(m_logger, Id); - } - else if (Utility.TryParseRedirectionHeader(statusInfo, out var host, out var port, out var user)) - { - if (host != ConnectionSettings.HostNames![0] || port != ConnectionSettings.Port || user != ConnectionSettings.UserID) - { - var redirectedSettings = ConnectionSettings.CloneWith(host, port, user); - Log.OpeningNewConnection(m_logger, Id, host, port, user); - var redirectedSession = new ServerSession(m_connectionLogger, this, m_generation, Interlocked.Increment(ref m_lastSessionId)); - try - { - _ = await redirectedSession.ConnectAsync(redirectedSettings, connection, startingTimestamp, m_loadBalancer, activity, ioBehavior, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - Log.FailedToConnectRedirectedSession(m_logger, ex, Id, redirectedSession.Id); - redirectionException = ex; - } - - if (redirectionException is null) - { - Log.ClosingSessionToUseRedirectedSession(m_logger, Id, session.Id, redirectedSession.Id); - await session.DisposeAsync(ioBehavior, cancellationToken).ConfigureAwait(false); - return redirectedSession; - } - else - { - try - { - await redirectedSession.DisposeAsync(ioBehavior, cancellationToken).ConfigureAwait(false); - } - catch (Exception) - { - } - } - } - else - { - Log.SessionAlreadyConnectedToServer(m_logger, session.Id); - } - } - } - - if (ConnectionSettings.ServerRedirectionMode == MySqlServerRedirectionMode.Required) - { - Log.RequiresServerRedirection(m_logger, Id); - throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Server does not support redirection", redirectionException); - } - return session; - } - public static ConnectionPool? CreatePool(string connectionString, MySqlConnectorLoggingConfiguration loggingConfiguration, string? name) { // parse connection string and check for 'Pooling' setting; return 'null' if pooling is disabled diff --git a/src/MySqlConnector/Core/ConnectionSettings.cs b/src/MySqlConnector/Core/ConnectionSettings.cs index c93e16004..8b365833c 100644 --- a/src/MySqlConnector/Core/ConnectionSettings.cs +++ b/src/MySqlConnector/Core/ConnectionSettings.cs @@ -270,8 +270,12 @@ public int ConnectionTimeoutMilliseconds private ConnectionSettings(ConnectionSettings other, string host, int port, string userId) { - ConnectionStringBuilder = other.ConnectionStringBuilder; - ConnectionString = other.ConnectionString; + ConnectionStringBuilder = new MySqlConnectionStringBuilder(other.ConnectionString); + ConnectionStringBuilder.Port = (uint)port; + ConnectionStringBuilder.Server = host; + ConnectionStringBuilder.UserID = userId; + + ConnectionString = ConnectionStringBuilder.ConnectionString; ConnectionProtocol = MySqlConnectionProtocol.Sockets; HostNames = [host]; diff --git a/src/MySqlConnector/Core/ServerSession.cs b/src/MySqlConnector/Core/ServerSession.cs index dcaf1e69f..0884675ea 100644 --- a/src/MySqlConnector/Core/ServerSession.cs +++ b/src/MySqlConnector/Core/ServerSession.cs @@ -51,6 +51,7 @@ public ServerSession(ILogger logger, ConnectionPool? pool, int poolGeneration, i public bool SupportsPerQueryVariables => ServerVersion.IsMariaDb && ServerVersion.Version >= ServerVersions.MariaDbSupportsPerQueryVariables; public int ActiveCommandId { get; private set; } public int CancellationTimeout { get; private set; } + public string? ConnectionString { get; private set; } public int ConnectionId { get; set; } public byte[]? AuthPluginData { get; set; } public long CreatedTimestamp { get; } @@ -391,7 +392,7 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella m_state = State.Closed; } - public async Task ConnectAsync(ConnectionSettings cs, MySqlConnection connection, long startingTimestamp, ILoadBalancer? loadBalancer, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken) + private async Task ConnectAsync(ConnectionSettings cs, MySqlConnection connection, long startingTimestamp, ILoadBalancer? loadBalancer, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken) { try { @@ -403,16 +404,16 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella // set activity tags { - var connectionString = cs.ConnectionStringBuilder.GetConnectionString(cs.ConnectionStringBuilder.PersistSecurityInfo); + ConnectionString = cs.ConnectionStringBuilder.GetConnectionString(cs.ConnectionStringBuilder.PersistSecurityInfo); m_activityTags.Add(ActivitySourceHelper.DatabaseSystemTagName, ActivitySourceHelper.DatabaseSystemValue); - m_activityTags.Add(ActivitySourceHelper.DatabaseConnectionStringTagName, connectionString); + m_activityTags.Add(ActivitySourceHelper.DatabaseConnectionStringTagName, ConnectionString); m_activityTags.Add(ActivitySourceHelper.DatabaseUserTagName, cs.UserID); if (cs.Database.Length != 0) m_activityTags.Add(ActivitySourceHelper.DatabaseNameTagName, cs.Database); if (activity is { IsAllDataRequested: true }) { activity.SetTag(ActivitySourceHelper.DatabaseSystemTagName, ActivitySourceHelper.DatabaseSystemValue) - .SetTag(ActivitySourceHelper.DatabaseConnectionStringTagName, connectionString) + .SetTag(ActivitySourceHelper.DatabaseConnectionStringTagName, ConnectionString) .SetTag(ActivitySourceHelper.DatabaseUserTagName, cs.UserID); if (cs.Database.Length != 0) activity.SetTag(ActivitySourceHelper.DatabaseNameTagName, cs.Database); @@ -533,7 +534,7 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella } var ok = OkPayload.Create(payload.Span, this); - var statusInfo = ok.StatusInfo; + var redirectionUrl = ok.RedirectionUrl; if (m_useCompression) m_payloadHandler = new CompressedPayloadHandler(m_payloadHandler.ByteHandler); @@ -558,7 +559,7 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella } m_payloadHandler.ByteHandler.RemainingTimeout = Constants.InfiniteTimeout; - return statusInfo; + return redirectionUrl; } catch (ArgumentException ex) { @@ -572,6 +573,82 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella } } + public static async ValueTask ConnectAndRedirectAsync(Func createSession, ILogger logger, int? poolId, ConnectionSettings cs, ILoadBalancer? loadBalancer, MySqlConnection connection, Action? logMessage, long startingTimestamp, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken) + { + var session = createSession(); + if (poolId is not null) + { + if (logger.IsEnabled(LogLevel.Debug)) logMessage!(logger, poolId.Value, session.Id, null); + } + else + { + Log.CreatedNonPooledSession(logger, session.Id); + } + + string? redirectionUrl; + try + { + redirectionUrl = await session.ConnectAsync(cs, connection, startingTimestamp, loadBalancer, activity, ioBehavior, cancellationToken).ConfigureAwait(false); + } + catch (Exception) + { + await session.DisposeAsync(ioBehavior, default).ConfigureAwait(false); + throw; + } + + Exception? redirectionException = null; + var poolPrefix = poolId is not null ? "Pool {PoolId} " : ""; + if (redirectionUrl is not null) + { + Log.HasServerRedirectionHeader(logger, poolPrefix, session.Id, redirectionUrl!); + if (cs.ServerRedirectionMode == MySqlServerRedirectionMode.Disabled) + { + Log.ServerRedirectionIsDisabled(logger, poolPrefix); + return session; + } + + if (Utility.TryParseRedirectionHeader(redirectionUrl, cs.UserID, out var host, out var port, out var user)) + { + if (host != cs.HostNames![0] || port != cs.Port || user != cs.UserID) + { + var redirectedSettings = cs.CloneWith(host, port, user); + Log.OpeningNewConnection(logger, poolPrefix, host, port, user); + var redirectedSession = createSession(); + try + { + await redirectedSession.ConnectAsync(redirectedSettings, connection, startingTimestamp, loadBalancer, activity, ioBehavior, cancellationToken).ConfigureAwait(false); + Log.ClosingSessionToUseRedirectedSession(logger, poolPrefix, session.Id, redirectedSession.Id); + await session.DisposeAsync(ioBehavior, cancellationToken).ConfigureAwait(false); + return redirectedSession; + } + catch (Exception ex) + { + redirectionException = ex; + Log.FailedToConnectRedirectedSession(logger, ex, poolPrefix, redirectedSession.Id); + try + { + await redirectedSession.DisposeAsync(ioBehavior, cancellationToken).ConfigureAwait(false); + } + catch (Exception) + { + } + } + } + else + { + Log.SessionAlreadyConnectedToServer(logger, poolPrefix, session.Id); + } + } + } + + if (cs.ServerRedirectionMode == MySqlServerRedirectionMode.Required) + { + Log.RequiresServerRedirection(logger, poolPrefix); + throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Server does not support redirection", redirectionException); + } + return session; + } + public async Task TryResetConnectionAsync(ConnectionSettings cs, MySqlConnection connection, IOBehavior ioBehavior, CancellationToken cancellationToken) { VerifyState(State.Connected); diff --git a/src/MySqlConnector/Logging/Log.cs b/src/MySqlConnector/Logging/Log.cs index 7cb3bd432..b83db5092 100644 --- a/src/MySqlConnector/Logging/Log.cs +++ b/src/MySqlConnector/Logging/Log.cs @@ -402,26 +402,26 @@ internal static partial class Log [LoggerMessage(EventIds.FoundSessionToCleanUp, LogLevel.Debug, "Pool {PoolId} found session {SessionId} to clean up")] public static partial void FoundSessionToCleanUp(ILogger logger, int poolId, string sessionId); - [LoggerMessage(EventIds.HasServerRedirectionHeader, LogLevel.Trace, "Session {SessionId} has server redirection header {Header}")] - public static partial void HasServerRedirectionHeader(ILogger logger, string sessionId, string header); + [LoggerMessage(EventIds.HasServerRedirectionHeader, LogLevel.Trace, "{poolPrefix}Session {SessionId} has server redirection header {Header}")] + public static partial void HasServerRedirectionHeader(ILogger logger, string poolPrefix, string sessionId, string header); - [LoggerMessage(EventIds.ServerRedirectionIsDisabled, LogLevel.Trace, "Pool {PoolId} server redirection is disabled; ignoring redirection")] - public static partial void ServerRedirectionIsDisabled(ILogger logger, int poolId); + [LoggerMessage(EventIds.ServerRedirectionIsDisabled, LogLevel.Trace, "{poolPrefix}server redirection is disabled; ignoring redirection")] + public static partial void ServerRedirectionIsDisabled(ILogger logger, string poolPrefix); - [LoggerMessage(EventIds.OpeningNewConnection, LogLevel.Debug, "Pool {PoolId} opening new connection to {Host}:{Port} as {User}")] - public static partial void OpeningNewConnection(ILogger logger, int poolId, string host, int port, string user); + [LoggerMessage(EventIds.OpeningNewConnection, LogLevel.Debug, "{poolPrefix}opening new connection to {Host}:{Port} as {User}")] + public static partial void OpeningNewConnection(ILogger logger, string poolPrefix, string host, int port, string user); - [LoggerMessage(EventIds.FailedToConnectRedirectedSession, LogLevel.Information, "Pool {PoolId} failed to connect redirected session {SessionId}")] - public static partial void FailedToConnectRedirectedSession(ILogger logger, Exception ex, int poolId, string sessionId); + [LoggerMessage(EventIds.FailedToConnectRedirectedSession, LogLevel.Information, "{poolPrefix}failed to connect redirected session {SessionId}")] + public static partial void FailedToConnectRedirectedSession(ILogger logger, Exception ex, string poolPrefix, string sessionId); - [LoggerMessage(EventIds.ClosingSessionToUseRedirectedSession, LogLevel.Trace, "Pool {PoolId} closing session {SessionId} to use redirected session {RedirectedSessionId} instead")] - public static partial void ClosingSessionToUseRedirectedSession(ILogger logger, int poolId, string sessionId, string redirectedSessionId); + [LoggerMessage(EventIds.ClosingSessionToUseRedirectedSession, LogLevel.Trace, "{poolPrefix}closing session {SessionId} to use redirected session {RedirectedSessionId} instead")] + public static partial void ClosingSessionToUseRedirectedSession(ILogger logger, string poolPrefix, string sessionId, string redirectedSessionId); - [LoggerMessage(EventIds.SessionAlreadyConnectedToServer, LogLevel.Trace, "Session {SessionId} is already connected to this server; ignoring redirection")] - public static partial void SessionAlreadyConnectedToServer(ILogger logger, string sessionId); + [LoggerMessage(EventIds.SessionAlreadyConnectedToServer, LogLevel.Trace, "{poolPrefix}Session {SessionId} is already connected to this server; ignoring redirection")] + public static partial void SessionAlreadyConnectedToServer(ILogger logger, string poolPrefix, string sessionId); - [LoggerMessage(EventIds.RequiresServerRedirection, LogLevel.Error, "Pool {PoolId} requires server redirection but server doesn't support it")] - public static partial void RequiresServerRedirection(ILogger logger, int poolId); + [LoggerMessage(EventIds.RequiresServerRedirection, LogLevel.Error, "{poolPrefix}new connection requires server redirection but server doesn't support it")] + public static partial void RequiresServerRedirection(ILogger logger, string poolPrefix); [LoggerMessage(EventIds.CreatedPoolWillNotBeUsed, LogLevel.Debug, "Pool {PoolId} was created but will not be used (due to race)")] public static partial void CreatedPoolWillNotBeUsed(ILogger logger, int poolId); diff --git a/src/MySqlConnector/MySqlConnection.cs b/src/MySqlConnector/MySqlConnection.cs index 7a835d202..371303ade 100644 --- a/src/MySqlConnector/MySqlConnection.cs +++ b/src/MySqlConnector/MySqlConnection.cs @@ -628,6 +628,8 @@ public override string ConnectionString } } + public string? SessionConnectionString => m_session?.ConnectionString; + public override string Database => m_session?.DatabaseOverride ?? GetConnectionSettings().Database; public override ConnectionState State => m_connectionState; @@ -1062,22 +1064,9 @@ private async ValueTask CreateSessionAsync(ConnectionPool? pool, // only "fail over" and "random" load balancers supported without connection pooling var loadBalancer = connectionSettings.LoadBalance == MySqlLoadBalance.Random && connectionSettings.HostNames!.Count > 1 ? RandomLoadBalancer.Instance : FailOverLoadBalancer.Instance; - - var session = new ServerSession(m_logger) - { - OwningConnection = new WeakReference(this), - }; - Log.CreatedNonPooledSession(m_logger, session.Id); - try - { - _ = await session.ConnectAsync(connectionSettings, this, startingTimestamp, loadBalancer, activity, actualIOBehavior, connectToken).ConfigureAwait(false); - return session; - } - catch (Exception) - { - await session.DisposeAsync(actualIOBehavior, default).ConfigureAwait(false); - throw; - } + var session = await ServerSession.ConnectAndRedirectAsync(() => new ServerSession(m_logger), m_logger, null, connectionSettings, loadBalancer, this, null, startingTimestamp, null, actualIOBehavior, cancellationToken).ConfigureAwait(false); + session.OwningConnection = new WeakReference(this); + return session; } } catch (OperationCanceledException) when (timeoutSource?.IsCancellationRequested is true) diff --git a/src/MySqlConnector/Protocol/Payloads/OkPayload.cs b/src/MySqlConnector/Protocol/Payloads/OkPayload.cs index a08a3195d..37db8862f 100644 --- a/src/MySqlConnector/Protocol/Payloads/OkPayload.cs +++ b/src/MySqlConnector/Protocol/Payloads/OkPayload.cs @@ -16,6 +16,7 @@ internal sealed class OkPayload public string? NewSchema { get; } public CharacterSet? NewCharacterSet { get; } public int? NewConnectionId { get; } + public string? RedirectionUrl { get; } public const byte Signature = 0x00; @@ -64,6 +65,7 @@ public static void Verify(ReadOnlySpan span, IServerCapabilities serverCap CharacterSet clientCharacterSet = default; CharacterSet connectionCharacterSet = default; CharacterSet resultsCharacterSet = default; + string? redirectionUrl = default; int? connectionId = null; ReadOnlySpan statusBytes; @@ -115,6 +117,13 @@ public static void Verify(ReadOnlySpan span, IServerCapabilities serverCap { connectionId = Utf8Parser.TryParse(systemVariableValue, out int parsedConnectionId, out var bytesConsumed) && bytesConsumed == systemVariableValue.Length ? parsedConnectionId : default(int?); } + else if (systemVariableName.SequenceEqual("redirect_url"u8)) + { + if (systemVariableValue.Length > 0) + { + redirectionUrl = Encoding.UTF8.GetString(systemVariableValue); + } + } } while (reader.Offset < systemVariablesEndOffset); break; @@ -150,7 +159,7 @@ public static void Verify(ReadOnlySpan span, IServerCapabilities serverCap clientCharacterSet == CharacterSet.Utf8Mb3Binary && connectionCharacterSet == CharacterSet.Utf8Mb3Binary && resultsCharacterSet == CharacterSet.Utf8Mb3Binary ? CharacterSet.Utf8Mb3Binary : CharacterSet.None; - if (affectedRowCount == 0 && lastInsertId == 0 && warningCount == 0 && statusInfo is null && newSchema is null && clientCharacterSet is CharacterSet.None && connectionId is null) + if (affectedRowCount == 0 && lastInsertId == 0 && warningCount == 0 && statusInfo is null && newSchema is null && clientCharacterSet is CharacterSet.None && connectionId is null && redirectionUrl is null) { if (serverStatus == ServerStatus.AutoCommit) return s_autoCommitOk; @@ -158,7 +167,7 @@ public static void Verify(ReadOnlySpan span, IServerCapabilities serverCap return s_autoCommitSessionStateChangedOk; } - return new OkPayload(affectedRowCount, lastInsertId, serverStatus, warningCount, statusInfo, newSchema, characterSet, connectionId); + return new OkPayload(affectedRowCount, lastInsertId, serverStatus, warningCount, statusInfo, newSchema, characterSet, connectionId, redirectionUrl); } else { @@ -166,7 +175,7 @@ public static void Verify(ReadOnlySpan span, IServerCapabilities serverCap } } - private OkPayload(ulong affectedRowCount, ulong lastInsertId, ServerStatus serverStatus, int warningCount, string? statusInfo, string? newSchema, CharacterSet newCharacterSet, int? connectionId) + private OkPayload(ulong affectedRowCount, ulong lastInsertId, ServerStatus serverStatus, int warningCount, string? statusInfo, string? newSchema, CharacterSet newCharacterSet, int? connectionId, string? redirectionUrl) { AffectedRowCount = affectedRowCount; LastInsertId = lastInsertId; @@ -176,8 +185,9 @@ private OkPayload(ulong affectedRowCount, ulong lastInsertId, ServerStatus serve NewSchema = newSchema; NewCharacterSet = newCharacterSet; NewConnectionId = connectionId; + RedirectionUrl = redirectionUrl; } - private static readonly OkPayload s_autoCommitOk = new(0, 0, ServerStatus.AutoCommit, 0, default, default, default, default); - private static readonly OkPayload s_autoCommitSessionStateChangedOk = new(0, 0, ServerStatus.AutoCommit | ServerStatus.SessionStateChanged, 0, default, default, default, default); + private static readonly OkPayload s_autoCommitOk = new(0, 0, ServerStatus.AutoCommit, 0, default, default, default, default, default); + private static readonly OkPayload s_autoCommitSessionStateChangedOk = new(0, 0, ServerStatus.AutoCommit | ServerStatus.SessionStateChanged, 0, default, default, default, default, default); } diff --git a/src/MySqlConnector/Utilities/Utility.cs b/src/MySqlConnector/Utilities/Utility.cs index d1d3ce02b..63f07c8c6 100644 --- a/src/MySqlConnector/Utilities/Utility.cs +++ b/src/MySqlConnector/Utilities/Utility.cs @@ -336,68 +336,45 @@ public static void Resize([NotNull] ref ResizableArray? resizableArray, in resizableArray.DoResize(newLength); } - public static bool TryParseRedirectionHeader(string header, out string host, out int port, out string user) + public static bool TryParseRedirectionHeader(string redirectUrl, string initialUser, out string host, out int port, out string user) { host = ""; port = 0; user = ""; - if (!header.StartsWith("Location: mysql://", StringComparison.Ordinal) || header.Length < 22) + // "mariadb/mysql://[{user}[:{password}]@]{host}[:{port}]/[{db}[?{opt1}={value1}[&{opt2}={value2}]]]']" + if (!redirectUrl.StartsWith("mysql://", StringComparison.Ordinal) && !redirectUrl.StartsWith("mariadb://", StringComparison.Ordinal)) return false; - bool isCommunityFormat; - int portIndex; - if (header[18] == '[') - { - // Community protocol: - // Location: mysql://[redirectedHostName]:redirectedPort/?user=redirectedUser&ttl=%d\n - isCommunityFormat = true; - - var hostIndex = 19; - var closeSquareBracketIndex = header.IndexOf(']', hostIndex); - if (closeSquareBracketIndex == -1) - return false; - - host = header[hostIndex..closeSquareBracketIndex]; - if (header.Length <= closeSquareBracketIndex + 2) - return false; - if (header[closeSquareBracketIndex + 1] != ':') - return false; - portIndex = closeSquareBracketIndex + 2; - } - else + try { - // Azure protocol: - // Location: mysql://redirectedHostName:redirectedPort/user=redirectedUser&ttl=%d (where ttl is optional) - isCommunityFormat = false; - - var hostIndex = 18; - var colonIndex = header.IndexOf(':', hostIndex); - if (colonIndex == -1) - return false; + var uri = new Uri(redirectUrl); + host = uri.Host; + if (string.IsNullOrEmpty(host)) return false; + if (host.StartsWith('[') && host.EndsWith("]", StringComparison.InvariantCulture)) host = host.Substring(1, host.Length - 2); + + port = uri.Port; + user = Uri.UnescapeDataString(uri.UserInfo.Split(':')[0]); + if (string.IsNullOrEmpty(user) && !string.IsNullOrEmpty(uri.Query)) + { + // query format "?{opt1}={value1}[&{opt2}={value2}]" + var q = uri.Query.Substring(1); + foreach (var token in q.Split('&')) + { + if (token.StartsWith("user=", StringComparison.InvariantCulture)) + { + user = Uri.UnescapeDataString(token.Substring(5)); + } + } + } - host = header[hostIndex..colonIndex]; - portIndex = colonIndex + 1; + if (string.IsNullOrEmpty(user)) user = initialUser; + return true; } - - var userIndex = header.IndexOf(isCommunityFormat ? "/?user=" : "/user=", StringComparison.Ordinal); - if (userIndex == -1) - return false; - -#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER - if (!int.TryParse(header.AsSpan(portIndex, userIndex - portIndex), out port) || port <= 0) -#else - if (!int.TryParse(header[portIndex..userIndex], out port) || port <= 0) -#endif + catch (UriFormatException) + { return false; - - userIndex += isCommunityFormat ? 7 : 6; - var ampersandIndex = header.IndexOf('&', userIndex); - var newlineIndex = header.IndexOf('\n', userIndex); - var terminatorIndex = ampersandIndex == -1 ? (newlineIndex == -1 ? header.Length : newlineIndex) : - (newlineIndex == -1 ? ampersandIndex : Math.Min(ampersandIndex, newlineIndex)); - user = header[userIndex..terminatorIndex]; - return user.Length != 0; + } } public static TimeSpan ParseTimeSpan(ReadOnlySpan value) diff --git a/tests/IntegrationTests/RedirectionTests.cs b/tests/IntegrationTests/RedirectionTests.cs new file mode 100644 index 000000000..3d4c7bd58 --- /dev/null +++ b/tests/IntegrationTests/RedirectionTests.cs @@ -0,0 +1,199 @@ +using System.Globalization; +using System.Net; +using System.Net.Sockets; + +namespace IntegrationTests; + +public class RedirectionTests : IClassFixture, IDisposable +{ + public RedirectionTests(DatabaseFixture database) + { + m_database = database; + m_database.Connection.Open(); + } + + public void Dispose() + { + m_database.Connection.Close(); + } + + + [Fact] + public void RedirectionTest() + { + StartProxy(); + + // wait for proxy to launch + Thread.Sleep(50); + var csb = AppConfig.CreateConnectionStringBuilder(); + var initialServer = csb.Server; + var initialPort = csb.Port; + var permitRedirection = true; + try + { + m_database.Connection.Execute( + $"set @@global.redirect_url=\"mariadb://{initialServer}:{initialPort}\""); + } + catch (Exception) + { + permitRedirection = false; + } + + if (permitRedirection) + { + try + { + // changing to proxy port + csb.Server = "localhost"; + csb.Port = (uint)proxy.ListenPort; + csb.ServerRedirectionMode = MySqlServerRedirectionMode.Preferred; + + // ensure that connection has been redirected + using (var db = new MySqlConnection(csb.ConnectionString)) + { + db.Open(); + using (var cmd = db.CreateCommand()) + { + cmd.CommandText = "SELECT 1"; + cmd.ExecuteNonQuery(); + } + + Assert.Contains(";Port=" + initialPort + ";", db.SessionConnectionString, + StringComparison.OrdinalIgnoreCase); + db.Close(); + } + + // ensure that connection has been redirected with Required + csb.ServerRedirectionMode = MySqlServerRedirectionMode.Required; + using (var db = new MySqlConnection(csb.ConnectionString)) + { + db.Open(); + using (var cmd = db.CreateCommand()) + { + cmd.CommandText = "SELECT 1"; + cmd.ExecuteNonQuery(); + } + + Assert.Contains(";Port=" + initialPort + ";", db.SessionConnectionString, + StringComparison.OrdinalIgnoreCase); + db.Close(); + } + + // ensure that redirection is not done + csb.ServerRedirectionMode = MySqlServerRedirectionMode.Disabled; + using (var db = new MySqlConnection(csb.ConnectionString)) + { + db.Open(); + using (var cmd = db.CreateCommand()) + { + cmd.CommandText = "SELECT 1"; + cmd.ExecuteNonQuery(); + } + + Assert.Contains(";Port=" + proxy.ListenPort + ";", db.SessionConnectionString, + StringComparison.OrdinalIgnoreCase); + db.Close(); + } + + } finally{ + m_database.Connection.Execute( + $"set @@global.redirect_url=\"\""); + } + MySqlConnection.ClearAllPools(); + // ensure that when required, throwing error if no redirection + csb.ServerRedirectionMode = MySqlServerRedirectionMode.Required; + using (var db = new MySqlConnection(csb.ConnectionString)) + { + try + { + db.Open(); + Assert.Fail("must have thrown error"); + } + catch (MySqlException ex) + { + Assert.Equal((int) MySqlErrorCode.UnableToConnectToHost, ex.Number); + } + } + } + StopProxy(); + } + + protected void StartProxy() + { + var csb = AppConfig.CreateConnectionStringBuilder(); + proxy = new ServerConfiguration( csb.Server, (int)csb.Port ); + Thread serverThread = new Thread( ServerThread ); + serverThread.Start( proxy ); + } + + protected void StopProxy() + { + proxy.RunServer = false; + proxy.ServerSocket.Close(); + } + + private class ServerConfiguration { + + public IPAddress RemoteAddress; + public int RemotePort; + public int ListenPort; + public Socket ServerSocket; + public ServerConfiguration(String remoteAddress, int remotePort) { + RemoteAddress = IPAddress.Parse( remoteAddress ); + RemotePort = remotePort; + ListenPort = 0; + } + public bool RunServer = true; + } + + private static void ServerThread(Object configObj) { + ServerConfiguration config = (ServerConfiguration)configObj; + Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + + serverSocket.Bind( new IPEndPoint( IPAddress.Any, 0 ) ); + serverSocket.Listen(1); + config.ListenPort = ((IPEndPoint) serverSocket.LocalEndPoint).Port; + config.ServerSocket = serverSocket; + while( config.RunServer ) { + Socket client = serverSocket.Accept(); + Thread clientThread = new Thread( ClientThread ); + clientThread.Start( new ClientContext() { Config = config, Client = client } ); + } + } + + private class ClientContext { + public ServerConfiguration Config; + public Socket Client; + } + + private static void ClientThread(Object contextObj) { + ClientContext context = (ClientContext)contextObj; + Socket client = context.Client; + ServerConfiguration config = context.Config; + IPEndPoint remoteEndPoint = new IPEndPoint( config.RemoteAddress, config.RemotePort ); + Socket remote = new Socket( remoteEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + remote.Connect( remoteEndPoint ); + Byte[] buffer = new Byte[4096]; + for(;;) { + if (!config.RunServer) + { + remote.Close(); + client.Close(); + return; + } + if( client.Available > 0 ) { + var count = client.Receive( buffer ); + if( count == 0 ) return; + remote.Send( buffer, count, SocketFlags.None ); + } + if( remote.Available > 0 ) { + var count = remote.Receive( buffer ); + if( count == 0 ) return; + client.Send( buffer, count, SocketFlags.None ); + } + } + } + + readonly DatabaseFixture m_database; + private ServerConfiguration proxy; +} diff --git a/tests/MySqlConnector.Tests/UtilityTests.cs b/tests/MySqlConnector.Tests/UtilityTests.cs index 57cdcc1b1..676ccd663 100644 --- a/tests/MySqlConnector.Tests/UtilityTests.cs +++ b/tests/MySqlConnector.Tests/UtilityTests.cs @@ -7,22 +7,19 @@ namespace MySqlConnector.Tests; public class UtilityTests { + [Theory] - [InlineData("Location: mysql://host.example.com:1234/user=user@host", "host.example.com", 1234, "user@host")] - [InlineData("Location: mysql://host.example.com:1234/user=user@host\n", "host.example.com", 1234, "user@host")] - [InlineData("Location: mysql://host.example.com:1234/user=user@host&ttl=60", "host.example.com", 1234, "user@host")] - [InlineData("Location: mysql://host.example.com:1234/user=user@host&ttl=60\n", "host.example.com", 1234, "user@host")] - [InlineData("Location: mysql://[host.example.com]:1234/?user=abcd", "host.example.com", 1234, "abcd")] - [InlineData("Location: mysql://[host.example.com]:1234/?user=abcd\n", "host.example.com", 1234, "abcd")] - [InlineData("Location: mysql://[host.example.com]:1234/?user=abcd&ttl=60", "host.example.com", 1234, "abcd")] - [InlineData("Location: mysql://[host.example.com]:1234/?user=abcd&ttl=60\n", "host.example.com", 1234, "abcd")] - [InlineData("Location: mysql://[2001:4860:4860::8888]:1234/?user=abcd", "2001:4860:4860::8888", 1234, "abcd")] - [InlineData("Location: mysql://[2001:4860:4860::8888]:1234/?user=abcd\n", "2001:4860:4860::8888", 1234, "abcd")] - [InlineData("Location: mysql://[2001:4860:4860::8888]:1234/?user=abcd&ttl=60", "2001:4860:4860::8888", 1234, "abcd")] - [InlineData("Location: mysql://[2001:4860:4860::8888]:1234/?user=abcd&ttl=60\n", "2001:4860:4860::8888", 1234, "abcd")] + [InlineData("mariadb://host.example.com:1234/?user=user@host", "host.example.com", 1234, "user@host")] + [InlineData("mariadb://user%40host:password@host.example.com:1234/", "host.example.com", 1234, "user@host")] + [InlineData("mariadb://host.example.com:1234/?user=user@host&ttl=60", "host.example.com", 1234, "user@host")] + [InlineData("mariadb://someuser:password@host.example.com:1234/?user=user@host&ttl=60\n", "host.example.com", 1234, "someuser")] + [InlineData("mysql://[2001:4860:4860::8888]:1234/?user=abcd", "2001:4860:4860::8888", 1234, "abcd")] + [InlineData("mysql://[2001:4860:4860::8888]:1234/?user=abcd\n", "2001:4860:4860::8888", 1234, "abcd")] + [InlineData("mysql://[2001:4860:4860::8888]:1234/?user=abcd&ttl=60", "2001:4860:4860::8888", 1234, "abcd")] + [InlineData("mysql://[2001:4860:4860::8888]:1234/?user=abcd&ttl=60\n", "2001:4860:4860::8888", 1234, "abcd")] public void ParseRedirectionHeader(string input, string expectedHost, int expectedPort, string expectedUser) { - Assert.True(Utility.TryParseRedirectionHeader(input, out var host, out var port, out var user)); + Assert.True(Utility.TryParseRedirectionHeader(input, null, out var host, out var port, out var user)); Assert.Equal(expectedHost, host); Assert.Equal(expectedPort, port); Assert.Equal(expectedUser, user); @@ -30,26 +27,14 @@ public void ParseRedirectionHeader(string input, string expectedHost, int expect [Theory] [InlineData("")] - [InlineData("Location: mysql")] - [InlineData("Location: mysql://host.example.com")] - [InlineData("Location: mysql://host.example.com:")] - [InlineData("Location: mysql://[host.example.com")] - [InlineData("Location: mysql://[host.example.com]")] - [InlineData("Location: mysql://[host.example.com]:")] - [InlineData("Location: mysql://host.example.com:123")] - [InlineData("Location: mysql://host.example.com:123/")] - [InlineData("Location: mysql://[host.example.com]:123")] - [InlineData("Location: mysql://[host.example.com]:123/")] - [InlineData("Location: mysql://host.example.com:/user=")] - [InlineData("Location: mysql://host.example.com:123/user=")] - [InlineData("Location: mysql://[host.example.com]:123/?user=")] - [InlineData("Location: mysql://host.example.com:/user=user@host")] - [InlineData("Location: mysql://host.example.com:-1/user=user@host")] - [InlineData("Location: mysql://host.example.com:0/user=user@host")] - [InlineData("Location: mysql://[host.example.com]:123/user=abcd")] + [InlineData("not formated")] + [InlineData("mysql")] + [InlineData("mysql://[host.example.com")] + [InlineData("mysql://host.example.com:-1/user=user@host")] + [InlineData("mysql://[host.example.com]:123/user=abcd")] public void ParseRedirectionHeaderFails(string input) { - Assert.False(Utility.TryParseRedirectionHeader(input, out _, out _, out _)); + Assert.False(Utility.TryParseRedirectionHeader(input, null, out _, out _, out _)); } [Theory] From 4e58e4225e62025c8aa4ad1a7295c73bf8a2d1eb Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Sun, 21 Jul 2024 14:01:29 -0700 Subject: [PATCH 02/11] Allow 'localhost' as a server address. Signed-off-by: Bradley Grainger --- tests/IntegrationTests/RedirectionTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/IntegrationTests/RedirectionTests.cs b/tests/IntegrationTests/RedirectionTests.cs index 3d4c7bd58..1d373206e 100644 --- a/tests/IntegrationTests/RedirectionTests.cs +++ b/tests/IntegrationTests/RedirectionTests.cs @@ -139,7 +139,8 @@ private class ServerConfiguration { public int ListenPort; public Socket ServerSocket; public ServerConfiguration(String remoteAddress, int remotePort) { - RemoteAddress = IPAddress.Parse( remoteAddress ); + var ipHostEntry = Dns.GetHostEntry(remoteAddress); + RemoteAddress = ipHostEntry.AddressList[0]; RemotePort = remotePort; ListenPort = 0; } From e7b1fc7a5f53f85e8c1f1ff4d265198c31f861fe Mon Sep 17 00:00:00 2001 From: rusher Date: Tue, 23 Jul 2024 16:17:15 +0200 Subject: [PATCH 03/11] permit skipping redirection test change redirection logging Signed-off-by: rusher --- azure-pipelines.yml | 12 +++++----- src/MySqlConnector/Core/ServerSession.cs | 24 +++++++------------ src/MySqlConnector/Logging/Log.cs | 28 +++++++++++----------- src/MySqlConnector/MySqlConnection.cs | 1 + tests/IntegrationTests/RedirectionTests.cs | 3 +-- tests/IntegrationTests/ServerFeatures.cs | 6 +++++ 6 files changed, 36 insertions(+), 38 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index e14ef7d82..dbe7aaba9 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -187,27 +187,27 @@ jobs: 'MySQL 8.0': image: 'mysql:8.0' connectionStringExtra: 'AllowPublicKeyRetrieval=True' - unsupportedFeatures: 'Ed25519,StreamingResults,Tls11,ZeroDateTime' + unsupportedFeatures: 'Ed25519,StreamingResults,Tls11,ZeroDateTime,Redirection' 'MySQL 8.4': image: 'mysql:8.4' connectionStringExtra: 'AllowPublicKeyRetrieval=True' - unsupportedFeatures: 'Ed25519,StreamingResults,Tls11,ZeroDateTime' + unsupportedFeatures: 'Ed25519,StreamingResults,Tls11,ZeroDateTime,Redirection' 'MySQL 9.0': image: 'mysql:9.0' connectionStringExtra: 'AllowPublicKeyRetrieval=True' - unsupportedFeatures: 'Ed25519,StreamingResults,Tls11,ZeroDateTime' + unsupportedFeatures: 'Ed25519,StreamingResults,Tls11,ZeroDateTime,Redirection' 'MariaDB 10.6': image: 'mariadb:10.6' connectionStringExtra: '' - unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin' + unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin,Redirection' 'MariaDB 10.11': image: 'mariadb:10.11' connectionStringExtra: '' - unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin' + unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin,Redirection' 'MariaDB 11.4': image: 'mariadb:11.4' connectionStringExtra: '' - unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin' + unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin,Redirection' steps: - template: '.ci/integration-tests-steps.yml' parameters: diff --git a/src/MySqlConnector/Core/ServerSession.cs b/src/MySqlConnector/Core/ServerSession.cs index 0884675ea..4ac7675dc 100644 --- a/src/MySqlConnector/Core/ServerSession.cs +++ b/src/MySqlConnector/Core/ServerSession.cs @@ -576,14 +576,7 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella public static async ValueTask ConnectAndRedirectAsync(Func createSession, ILogger logger, int? poolId, ConnectionSettings cs, ILoadBalancer? loadBalancer, MySqlConnection connection, Action? logMessage, long startingTimestamp, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken) { var session = createSession(); - if (poolId is not null) - { - if (logger.IsEnabled(LogLevel.Debug)) logMessage!(logger, poolId.Value, session.Id, null); - } - else - { - Log.CreatedNonPooledSession(logger, session.Id); - } + if (poolId is not null && logger.IsEnabled(LogLevel.Debug)) logMessage!(logger, poolId.Value, session.Id, null); string? redirectionUrl; try @@ -597,13 +590,12 @@ public static async ValueTask ConnectAndRedirectAsync(Func ConnectAndRedirectAsync(Func ConnectAndRedirectAsync(Func CreateSessionAsync(ConnectionPool? pool, RandomLoadBalancer.Instance : FailOverLoadBalancer.Instance; var session = await ServerSession.ConnectAndRedirectAsync(() => new ServerSession(m_logger), m_logger, null, connectionSettings, loadBalancer, this, null, startingTimestamp, null, actualIOBehavior, cancellationToken).ConfigureAwait(false); session.OwningConnection = new WeakReference(this); + Log.CreatedNonPooledSession(m_logger, session.Id); return session; } } diff --git a/tests/IntegrationTests/RedirectionTests.cs b/tests/IntegrationTests/RedirectionTests.cs index 1d373206e..181d6bf10 100644 --- a/tests/IntegrationTests/RedirectionTests.cs +++ b/tests/IntegrationTests/RedirectionTests.cs @@ -17,8 +17,7 @@ public void Dispose() m_database.Connection.Close(); } - - [Fact] + [SkippableFact(ServerFeatures.Redirection)] public void RedirectionTest() { StartProxy(); diff --git a/tests/IntegrationTests/ServerFeatures.cs b/tests/IntegrationTests/ServerFeatures.cs index cefe5d377..bfbec9fe8 100644 --- a/tests/IntegrationTests/ServerFeatures.cs +++ b/tests/IntegrationTests/ServerFeatures.cs @@ -35,4 +35,10 @@ public enum ServerFeatures /// A "SLEEP" command produces a result set when it is cancelled, not an error payload. /// CancelSleepSuccessfully = 0x40_0000, + + /// + /// Server permit redirection, available on first OK_Packet + /// + Redirection = 0x80_0000, + } From 6814615a6b62735025b525b8f786742e4a2f821f Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Sat, 27 Jul 2024 08:54:26 -0700 Subject: [PATCH 04/11] Catch exception when shutting down socket. Signed-off-by: Bradley Grainger --- tests/IntegrationTests/RedirectionTests.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/IntegrationTests/RedirectionTests.cs b/tests/IntegrationTests/RedirectionTests.cs index 181d6bf10..fea651177 100644 --- a/tests/IntegrationTests/RedirectionTests.cs +++ b/tests/IntegrationTests/RedirectionTests.cs @@ -155,9 +155,16 @@ private static void ServerThread(Object configObj) { config.ListenPort = ((IPEndPoint) serverSocket.LocalEndPoint).Port; config.ServerSocket = serverSocket; while( config.RunServer ) { - Socket client = serverSocket.Accept(); - Thread clientThread = new Thread( ClientThread ); - clientThread.Start( new ClientContext() { Config = config, Client = client } ); + try + { + Socket client = serverSocket.Accept(); + Thread clientThread = new Thread(ClientThread); + clientThread.Start(new ClientContext() { Config = config, Client = client }); + } + catch (SocketException) when (!config.RunServer) + { + return; + } } } From 17643e721c08c0371a5cb776c10eda52f4e95ae7 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Sat, 27 Jul 2024 09:15:36 -0700 Subject: [PATCH 05/11] Exclude RedirectionTests when using MySql.Data. Signed-off-by: Bradley Grainger --- tests/IntegrationTests/RedirectionTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/IntegrationTests/RedirectionTests.cs b/tests/IntegrationTests/RedirectionTests.cs index fea651177..64adef010 100644 --- a/tests/IntegrationTests/RedirectionTests.cs +++ b/tests/IntegrationTests/RedirectionTests.cs @@ -1,3 +1,4 @@ +#if !MYSQL_DATA using System.Globalization; using System.Net; using System.Net.Sockets; @@ -204,3 +205,4 @@ private static void ClientThread(Object contextObj) { readonly DatabaseFixture m_database; private ServerConfiguration proxy; } +#endif From 1a30a18a0a971ade522fdb5b2ca976bd50462fd5 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Sat, 27 Jul 2024 10:19:46 -0700 Subject: [PATCH 06/11] Fix non-pooled connect timeout. Signed-off-by: Bradley Grainger --- src/MySqlConnector/MySqlConnection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MySqlConnector/MySqlConnection.cs b/src/MySqlConnector/MySqlConnection.cs index c5790665f..9e6e1e0fc 100644 --- a/src/MySqlConnector/MySqlConnection.cs +++ b/src/MySqlConnector/MySqlConnection.cs @@ -1064,7 +1064,7 @@ private async ValueTask CreateSessionAsync(ConnectionPool? pool, // only "fail over" and "random" load balancers supported without connection pooling var loadBalancer = connectionSettings.LoadBalance == MySqlLoadBalance.Random && connectionSettings.HostNames!.Count > 1 ? RandomLoadBalancer.Instance : FailOverLoadBalancer.Instance; - var session = await ServerSession.ConnectAndRedirectAsync(() => new ServerSession(m_logger), m_logger, null, connectionSettings, loadBalancer, this, null, startingTimestamp, null, actualIOBehavior, cancellationToken).ConfigureAwait(false); + var session = await ServerSession.ConnectAndRedirectAsync(() => new ServerSession(m_logger), m_logger, null, connectionSettings, loadBalancer, this, null, startingTimestamp, null, actualIOBehavior, connectToken).ConfigureAwait(false); session.OwningConnection = new WeakReference(this); Log.CreatedNonPooledSession(m_logger, session.Id); return session; From c8a0b63063bc937a870c20a72e7bd8c6c2a65cd9 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Sat, 27 Jul 2024 10:28:10 -0700 Subject: [PATCH 07/11] Revert line-wrapping change. Signed-off-by: Bradley Grainger --- src/MySqlConnector/Core/ConnectionPool.cs | 10 +++------- tests/MySqlConnector.Tests/UtilityTests.cs | 1 - 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/MySqlConnector/Core/ConnectionPool.cs b/src/MySqlConnector/Core/ConnectionPool.cs index 56d8da03b..2c1599dc2 100644 --- a/src/MySqlConnector/Core/ConnectionPool.cs +++ b/src/MySqlConnector/Core/ConnectionPool.cs @@ -68,11 +68,8 @@ public async ValueTask GetSessionAsync(MySqlConnection connection if (ConnectionSettings.ConnectionReset || session.DatabaseOverride is not null) { if (timeoutMilliseconds != 0) - session.SetTimeout(Math.Max(1, - timeoutMilliseconds - Utility.GetElapsedMilliseconds(startingTimestamp))); - reuseSession = await session - .TryResetConnectionAsync(ConnectionSettings, connection, ioBehavior, cancellationToken) - .ConfigureAwait(false); + session.SetTimeout(Math.Max(1, timeoutMilliseconds - Utility.GetElapsedMilliseconds(startingTimestamp))); + reuseSession = await session.TryResetConnectionAsync(ConnectionSettings, connection, ioBehavior, cancellationToken).ConfigureAwait(false); session.SetTimeout(Constants.InfiniteTimeout); } else @@ -104,8 +101,7 @@ public async ValueTask GetSessionAsync(MySqlConnection connection Log.ReturningPooledSession(m_logger, Id, session.Id, leasedSessionsCountPooled); session.LastLeasedTimestamp = Stopwatch.GetTimestamp(); - MetricsReporter.RecordWaitTime(this, - Utility.GetElapsedSeconds(startingTimestamp, session.LastLeasedTimestamp)); + MetricsReporter.RecordWaitTime(this, Utility.GetElapsedSeconds(startingTimestamp, session.LastLeasedTimestamp)); return session; } } diff --git a/tests/MySqlConnector.Tests/UtilityTests.cs b/tests/MySqlConnector.Tests/UtilityTests.cs index 676ccd663..8040c2284 100644 --- a/tests/MySqlConnector.Tests/UtilityTests.cs +++ b/tests/MySqlConnector.Tests/UtilityTests.cs @@ -7,7 +7,6 @@ namespace MySqlConnector.Tests; public class UtilityTests { - [Theory] [InlineData("mariadb://host.example.com:1234/?user=user@host", "host.example.com", 1234, "user@host")] [InlineData("mariadb://user%40host:password@host.example.com:1234/", "host.example.com", 1234, "user@host")] From 6d93fa8a80fc835fc4bbe94b0bfe90bd6b3afd11 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Sat, 27 Jul 2024 11:54:13 -0700 Subject: [PATCH 08/11] Remove Func allocation when creating a ServerSession. Introduce IConnectionPoolMetadata to unify the code between pooled and non-pooled sessions. Signed-off-by: Bradley Grainger --- src/MySqlConnector/Core/ConnectionPool.cs | 22 +++++------ .../Core/IConnectionPoolMetadata.cs | 26 +++++++++++++ .../Core/NonPooledConnectionPoolMetadata.cs | 13 +++++++ src/MySqlConnector/Core/ServerSession.cs | 37 ++++++++----------- src/MySqlConnector/MySqlConnection.cs | 2 +- 5 files changed, 67 insertions(+), 33 deletions(-) create mode 100644 src/MySqlConnector/Core/IConnectionPoolMetadata.cs create mode 100644 src/MySqlConnector/Core/NonPooledConnectionPoolMetadata.cs diff --git a/src/MySqlConnector/Core/ConnectionPool.cs b/src/MySqlConnector/Core/ConnectionPool.cs index 2c1599dc2..a11391cd5 100644 --- a/src/MySqlConnector/Core/ConnectionPool.cs +++ b/src/MySqlConnector/Core/ConnectionPool.cs @@ -8,10 +8,16 @@ namespace MySqlConnector.Core; -internal sealed class ConnectionPool : IDisposable +internal sealed class ConnectionPool : IConnectionPoolMetadata, IDisposable { public int Id { get; } + ConnectionPool? IConnectionPoolMetadata.ConnectionPool => this; + + int IConnectionPoolMetadata.Generation => m_generation; + + int IConnectionPoolMetadata.GetNewSessionId() => Interlocked.Increment(ref m_lastSessionId); + public string? Name { get; } public ConnectionSettings ConnectionSettings { get; } @@ -107,11 +113,8 @@ public async ValueTask GetSessionAsync(MySqlConnection connection } // create a new session - session = await ServerSession.ConnectAndRedirectAsync( - () => new ServerSession(m_connectionLogger, this, m_generation, - Interlocked.Increment(ref m_lastSessionId)), m_logger, Id, ConnectionSettings, m_loadBalancer, - connection, s_createdNewSession, startingTimestamp, activity, ioBehavior, cancellationToken) - .ConfigureAwait(false); + session = await ServerSession.ConnectAndRedirectAsync(m_connectionLogger, m_logger, this, ConnectionSettings, m_loadBalancer, + connection, s_createdNewSession, startingTimestamp, activity, ioBehavior, cancellationToken).ConfigureAwait(false); AdjustHostConnectionCount(session, 1); session.OwningConnection = new(connection); int leasedSessionsCountNew; @@ -407,11 +410,8 @@ private async Task CreateMinimumPooledSessions(MySqlConnection connection, IOBeh try { - var session = await ServerSession.ConnectAndRedirectAsync( - () => new ServerSession(m_connectionLogger, this, m_generation, - Interlocked.Increment(ref m_lastSessionId)), m_logger, Id, ConnectionSettings, m_loadBalancer, - connection, s_createdToReachMinimumPoolSize, Stopwatch.GetTimestamp(), null, ioBehavior, - cancellationToken).ConfigureAwait(false); + var session = await ServerSession.ConnectAndRedirectAsync(m_connectionLogger, m_logger, this, ConnectionSettings, m_loadBalancer, + connection, s_createdToReachMinimumPoolSize, Stopwatch.GetTimestamp(), null, ioBehavior, cancellationToken).ConfigureAwait(false); AdjustHostConnectionCount(session, 1); lock (m_sessions) _ = m_sessions.AddFirst(session); diff --git a/src/MySqlConnector/Core/IConnectionPoolMetadata.cs b/src/MySqlConnector/Core/IConnectionPoolMetadata.cs new file mode 100644 index 000000000..64f8dd31b --- /dev/null +++ b/src/MySqlConnector/Core/IConnectionPoolMetadata.cs @@ -0,0 +1,26 @@ +namespace MySqlConnector.Core; + +internal interface IConnectionPoolMetadata +{ + /// + /// Returns the this is associated with, + /// or null if it represents a non-pooled connection. + /// + ConnectionPool? ConnectionPool { get; } + + /// + /// Returns the ID of the connection pool, or 0 if this is a non-pooled connection. + /// + int Id { get; } + + /// + /// Returns the generation of the connection pool, or 0 if this is a non-pooled connection. + /// + int Generation { get; } + + /// + /// Returns a new session ID. + /// + /// A new session ID. + int GetNewSessionId(); +} diff --git a/src/MySqlConnector/Core/NonPooledConnectionPoolMetadata.cs b/src/MySqlConnector/Core/NonPooledConnectionPoolMetadata.cs new file mode 100644 index 000000000..20d296722 --- /dev/null +++ b/src/MySqlConnector/Core/NonPooledConnectionPoolMetadata.cs @@ -0,0 +1,13 @@ +namespace MySqlConnector.Core; + +internal sealed class NonPooledConnectionPoolMetadata : IConnectionPoolMetadata +{ + public static IConnectionPoolMetadata Instance { get; } = new NonPooledConnectionPoolMetadata(); + + public ConnectionPool? ConnectionPool => null; + public int Id => 0; + public int Generation => 0; + public int GetNewSessionId() => Interlocked.Increment(ref m_lastId); + + private int m_lastId; +} diff --git a/src/MySqlConnector/Core/ServerSession.cs b/src/MySqlConnector/Core/ServerSession.cs index 4ac7675dc..317898ff8 100644 --- a/src/MySqlConnector/Core/ServerSession.cs +++ b/src/MySqlConnector/Core/ServerSession.cs @@ -25,21 +25,16 @@ namespace MySqlConnector.Core; internal sealed partial class ServerSession : IServerCapabilities { - public ServerSession(ILogger logger) - : this(logger, null, 0, Interlocked.Increment(ref s_lastId)) - { - } - - public ServerSession(ILogger logger, ConnectionPool? pool, int poolGeneration, int id) + public ServerSession(ILogger logger, IConnectionPoolMetadata pool) { m_logger = logger; m_lock = new(); m_payloadCache = new(); - Id = (pool?.Id ?? 0) + "." + id; + Id = pool.Id + "." + pool.GetNewSessionId(); ServerVersion = ServerVersion.Empty; CreatedTimestamp = Stopwatch.GetTimestamp(); - Pool = pool; - PoolGeneration = poolGeneration; + Pool = pool.ConnectionPool; + PoolGeneration = pool.Generation; HostName = ""; m_activityTags = []; DataReader = new(); @@ -573,10 +568,11 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella } } - public static async ValueTask ConnectAndRedirectAsync(Func createSession, ILogger logger, int? poolId, ConnectionSettings cs, ILoadBalancer? loadBalancer, MySqlConnection connection, Action? logMessage, long startingTimestamp, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken) + public static async ValueTask ConnectAndRedirectAsync(ILogger connectionLogger, ILogger poolLogger, IConnectionPoolMetadata pool, ConnectionSettings cs, ILoadBalancer? loadBalancer, MySqlConnection connection, Action? logMessage, long startingTimestamp, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken) { - var session = createSession(); - if (poolId is not null && logger.IsEnabled(LogLevel.Debug)) logMessage!(logger, poolId.Value, session.Id, null); + var session = new ServerSession(connectionLogger, pool); + if (logMessage is not null && poolLogger.IsEnabled(LogLevel.Debug)) + logMessage(poolLogger, pool.Id, session.Id, null); string? redirectionUrl; try @@ -592,10 +588,10 @@ public static async ValueTask ConnectAndRedirectAsync(Func ConnectAndRedirectAsync(Func ConnectAndRedirectAsync(Func CreateSessionAsync(ConnectionPool? pool, // only "fail over" and "random" load balancers supported without connection pooling var loadBalancer = connectionSettings.LoadBalance == MySqlLoadBalance.Random && connectionSettings.HostNames!.Count > 1 ? RandomLoadBalancer.Instance : FailOverLoadBalancer.Instance; - var session = await ServerSession.ConnectAndRedirectAsync(() => new ServerSession(m_logger), m_logger, null, connectionSettings, loadBalancer, this, null, startingTimestamp, null, actualIOBehavior, connectToken).ConfigureAwait(false); + var session = await ServerSession.ConnectAndRedirectAsync(m_logger, m_logger, NonPooledConnectionPoolMetadata.Instance, connectionSettings, loadBalancer, this, null, startingTimestamp, null, actualIOBehavior, connectToken).ConfigureAwait(false); session.OwningConnection = new WeakReference(this); Log.CreatedNonPooledSession(m_logger, session.Id); return session; From 72527c770d826108604977a55defaf931f23af70 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Sat, 27 Jul 2024 15:46:37 -0700 Subject: [PATCH 09/11] Use connectionLogger for all redirection log messages. This standardises all redirection logging to use the MySqlConnection logger, whether pooled or non-pooled. Signed-off-by: Bradley Grainger --- src/MySqlConnector/Core/ServerSession.cs | 14 +++++++------- src/MySqlConnector/Logging/Log.cs | 16 ++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/MySqlConnector/Core/ServerSession.cs b/src/MySqlConnector/Core/ServerSession.cs index 317898ff8..0bf5b89b1 100644 --- a/src/MySqlConnector/Core/ServerSession.cs +++ b/src/MySqlConnector/Core/ServerSession.cs @@ -588,10 +588,10 @@ public static async ValueTask ConnectAndRedirectAsync(ILogger con Exception? redirectionException = null; if (redirectionUrl is not null) { - Log.HasServerRedirectionHeader(poolLogger, session.Id, redirectionUrl); + Log.HasServerRedirectionHeader(connectionLogger, session.Id, redirectionUrl); if (cs.ServerRedirectionMode == MySqlServerRedirectionMode.Disabled) { - Log.ServerRedirectionIsDisabled(poolLogger, session.Id); + Log.ServerRedirectionIsDisabled(connectionLogger, session.Id); return session; } @@ -600,19 +600,19 @@ public static async ValueTask ConnectAndRedirectAsync(ILogger con if (host != cs.HostNames![0] || port != cs.Port || user != cs.UserID) { var redirectedSettings = cs.CloneWith(host, port, user); - Log.OpeningNewConnection(poolLogger, host, port, user); + Log.OpeningNewConnection(connectionLogger, session.Id, host, port, user); var redirectedSession = new ServerSession(connectionLogger, pool); try { await redirectedSession.ConnectAsync(redirectedSettings, connection, startingTimestamp, loadBalancer, activity, ioBehavior, cancellationToken).ConfigureAwait(false); - Log.ClosingSessionToUseRedirectedSession(poolLogger, session.Id, redirectedSession.Id); + Log.ClosingSessionToUseRedirectedSession(connectionLogger, session.Id, redirectedSession.Id); await session.DisposeAsync(ioBehavior, cancellationToken).ConfigureAwait(false); return redirectedSession; } catch (Exception ex) { redirectionException = ex; - Log.FailedToConnectRedirectedSession(poolLogger, ex, redirectedSession.Id); + Log.FailedToConnectRedirectedSession(connectionLogger, ex, session.Id, redirectedSession.Id); try { await redirectedSession.DisposeAsync(ioBehavior, cancellationToken).ConfigureAwait(false); @@ -624,14 +624,14 @@ public static async ValueTask ConnectAndRedirectAsync(ILogger con } else { - Log.SessionAlreadyConnectedToServer(poolLogger, session.Id); + Log.SessionAlreadyConnectedToServer(connectionLogger, session.Id); } } } if (cs.ServerRedirectionMode == MySqlServerRedirectionMode.Required) { - Log.RequiresServerRedirection(poolLogger, session.Id); + Log.RequiresServerRedirection(connectionLogger, session.Id); throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Server does not support redirection", redirectionException); } return session; diff --git a/src/MySqlConnector/Logging/Log.cs b/src/MySqlConnector/Logging/Log.cs index 4fbc23219..57b10a088 100644 --- a/src/MySqlConnector/Logging/Log.cs +++ b/src/MySqlConnector/Logging/Log.cs @@ -405,23 +405,23 @@ internal static partial class Log [LoggerMessage(EventIds.HasServerRedirectionHeader, LogLevel.Trace, "Session {SessionId} has server redirection header {Header}")] public static partial void HasServerRedirectionHeader(ILogger logger, string sessionId, string header); - [LoggerMessage(EventIds.ServerRedirectionIsDisabled, LogLevel.Trace, "Session {SessionId}, server redirection is disabled; ignoring redirection")] + [LoggerMessage(EventIds.ServerRedirectionIsDisabled, LogLevel.Trace, "Session {SessionId} server redirection is disabled; ignoring redirection")] public static partial void ServerRedirectionIsDisabled(ILogger logger, string sessionId); - [LoggerMessage(EventIds.OpeningNewConnection, LogLevel.Debug, "opening new connection to {Host}:{Port} as {User}")] - public static partial void OpeningNewConnection(ILogger logger, string host, int port, string user); + [LoggerMessage(EventIds.OpeningNewConnection, LogLevel.Debug, "Session {SessionId} opening new connection to {Host}:{Port} as {User}")] + public static partial void OpeningNewConnection(ILogger logger, string sessionId, string host, int port, string user); - [LoggerMessage(EventIds.FailedToConnectRedirectedSession, LogLevel.Information, "failed to connect redirected session {SessionId}")] - public static partial void FailedToConnectRedirectedSession(ILogger logger, Exception ex, string sessionId); + [LoggerMessage(EventIds.FailedToConnectRedirectedSession, LogLevel.Information, "Session {SessionId} failed to connect redirected session {RedirectedSessionId}")] + public static partial void FailedToConnectRedirectedSession(ILogger logger, Exception ex, string sessionId, string redirectedSessionId); - [LoggerMessage(EventIds.ClosingSessionToUseRedirectedSession, LogLevel.Trace, "closing session {SessionId} to use redirected session {RedirectedSessionId} instead")] + [LoggerMessage(EventIds.ClosingSessionToUseRedirectedSession, LogLevel.Trace, "Closing session {SessionId} to use redirected session {RedirectedSessionId} instead")] public static partial void ClosingSessionToUseRedirectedSession(ILogger logger, string sessionId, string redirectedSessionId); [LoggerMessage(EventIds.SessionAlreadyConnectedToServer, LogLevel.Trace, "Session {SessionId} is already connected to this server; ignoring redirection")] public static partial void SessionAlreadyConnectedToServer(ILogger logger, string sessionId); - [LoggerMessage(EventIds.RequiresServerRedirection, LogLevel.Error, "Session {SessionId}, new connection requires server redirection but server doesn't support it")] - public static partial void RequiresServerRedirection(ILogger logger, string SessionId); + [LoggerMessage(EventIds.RequiresServerRedirection, LogLevel.Error, "Session {SessionId} requires server redirection but server doesn't support it")] + public static partial void RequiresServerRedirection(ILogger logger, string sessionId); [LoggerMessage(EventIds.CreatedPoolWillNotBeUsed, LogLevel.Debug, "Pool {PoolId} was created but will not be used (due to race)")] public static partial void CreatedPoolWillNotBeUsed(ILogger logger, int poolId); From 6204285f52a851d9428620ad893f81065076ec27 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Sat, 27 Jul 2024 15:58:41 -0700 Subject: [PATCH 10/11] Remove public SessionConnectionString property. Signed-off-by: Bradley Grainger --- src/MySqlConnector/Core/ServerSession.cs | 7 +++---- src/MySqlConnector/MySqlConnection.cs | 5 +++-- tests/IntegrationTests/RedirectionTests.cs | 9 +++------ 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/MySqlConnector/Core/ServerSession.cs b/src/MySqlConnector/Core/ServerSession.cs index 0bf5b89b1..dcd64d009 100644 --- a/src/MySqlConnector/Core/ServerSession.cs +++ b/src/MySqlConnector/Core/ServerSession.cs @@ -46,7 +46,6 @@ public ServerSession(ILogger logger, IConnectionPoolMetadata pool) public bool SupportsPerQueryVariables => ServerVersion.IsMariaDb && ServerVersion.Version >= ServerVersions.MariaDbSupportsPerQueryVariables; public int ActiveCommandId { get; private set; } public int CancellationTimeout { get; private set; } - public string? ConnectionString { get; private set; } public int ConnectionId { get; set; } public byte[]? AuthPluginData { get; set; } public long CreatedTimestamp { get; } @@ -399,16 +398,16 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella // set activity tags { - ConnectionString = cs.ConnectionStringBuilder.GetConnectionString(cs.ConnectionStringBuilder.PersistSecurityInfo); + var connectionString = cs.ConnectionStringBuilder.GetConnectionString(cs.ConnectionStringBuilder.PersistSecurityInfo); m_activityTags.Add(ActivitySourceHelper.DatabaseSystemTagName, ActivitySourceHelper.DatabaseSystemValue); - m_activityTags.Add(ActivitySourceHelper.DatabaseConnectionStringTagName, ConnectionString); + m_activityTags.Add(ActivitySourceHelper.DatabaseConnectionStringTagName, connectionString); m_activityTags.Add(ActivitySourceHelper.DatabaseUserTagName, cs.UserID); if (cs.Database.Length != 0) m_activityTags.Add(ActivitySourceHelper.DatabaseNameTagName, cs.Database); if (activity is { IsAllDataRequested: true }) { activity.SetTag(ActivitySourceHelper.DatabaseSystemTagName, ActivitySourceHelper.DatabaseSystemValue) - .SetTag(ActivitySourceHelper.DatabaseConnectionStringTagName, ConnectionString) + .SetTag(ActivitySourceHelper.DatabaseConnectionStringTagName, connectionString) .SetTag(ActivitySourceHelper.DatabaseUserTagName, cs.UserID); if (cs.Database.Length != 0) activity.SetTag(ActivitySourceHelper.DatabaseNameTagName, cs.Database); diff --git a/src/MySqlConnector/MySqlConnection.cs b/src/MySqlConnector/MySqlConnection.cs index 866e45dfe..571af0e51 100644 --- a/src/MySqlConnector/MySqlConnection.cs +++ b/src/MySqlConnector/MySqlConnection.cs @@ -3,6 +3,7 @@ #if NET6_0_OR_GREATER using System.Globalization; #endif +using System.Net; using System.Net.Security; using System.Net.Sockets; using System.Security.Authentication; @@ -628,8 +629,6 @@ public override string ConnectionString } } - public string? SessionConnectionString => m_session?.ConnectionString; - public override string Database => m_session?.DatabaseOverride ?? GetConnectionSettings().Database; public override ConnectionState State => m_connectionState; @@ -1104,6 +1103,8 @@ private async ValueTask CreateSessionAsync(ConnectionPool? pool, internal SslProtocols SslProtocol => m_session!.SslProtocol; + internal IPEndPoint? SessionEndPoint => m_session!.IPEndPoint; + internal void SetState(ConnectionState newState) { if (m_connectionState != newState) diff --git a/tests/IntegrationTests/RedirectionTests.cs b/tests/IntegrationTests/RedirectionTests.cs index 64adef010..123923bc2 100644 --- a/tests/IntegrationTests/RedirectionTests.cs +++ b/tests/IntegrationTests/RedirectionTests.cs @@ -58,8 +58,7 @@ public void RedirectionTest() cmd.ExecuteNonQuery(); } - Assert.Contains(";Port=" + initialPort + ";", db.SessionConnectionString, - StringComparison.OrdinalIgnoreCase); + Assert.Equal((int) initialPort, db.SessionEndPoint!.Port); db.Close(); } @@ -74,8 +73,7 @@ public void RedirectionTest() cmd.ExecuteNonQuery(); } - Assert.Contains(";Port=" + initialPort + ";", db.SessionConnectionString, - StringComparison.OrdinalIgnoreCase); + Assert.Equal((int) initialPort, db.SessionEndPoint!.Port); db.Close(); } @@ -90,8 +88,7 @@ public void RedirectionTest() cmd.ExecuteNonQuery(); } - Assert.Contains(";Port=" + proxy.ListenPort + ";", db.SessionConnectionString, - StringComparison.OrdinalIgnoreCase); + Assert.Equal(proxy.ListenPort, db.SessionEndPoint!.Port); db.Close(); } From cf08470536469d59e41bb525f6ec9d8e5a399dae Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Sat, 27 Jul 2024 16:00:56 -0700 Subject: [PATCH 11/11] Fail Redirection test if redirection can't be set up. Use ServerFeatures.Redirection to ignore this test on servers that don't support it. Signed-off-by: Bradley Grainger --- .ci/config/config.compression+ssl.json | 2 +- .ci/config/config.compression.json | 2 +- .ci/config/config.json | 2 +- .ci/config/config.ssl.json | 2 +- azure-pipelines.yml | 10 +- tests/IntegrationTests/RedirectionTests.cs | 121 ++++++++++----------- tests/IntegrationTests/ServerFeatures.cs | 1 - 7 files changed, 64 insertions(+), 76 deletions(-) diff --git a/.ci/config/config.compression+ssl.json b/.ci/config/config.compression+ssl.json index 8a11c95bf..71cd817a0 100644 --- a/.ci/config/config.compression+ssl.json +++ b/.ci/config/config.compression+ssl.json @@ -4,7 +4,7 @@ "SocketPath": "./../../../../.ci/run/mysql/mysqld.sock", "PasswordlessUser": "no_password", "SecondaryDatabase": "testdb2", - "UnsupportedFeatures": "RsaEncryption,CachingSha2Password,Tls12,Tls13,UuidToBin", + "UnsupportedFeatures": "CachingSha2Password,Redirection,RsaEncryption,Tls12,Tls13,UuidToBin", "MySqlBulkLoaderLocalCsvFile": "../../../TestData/LoadData_UTF8_BOM_Unix.CSV", "MySqlBulkLoaderLocalTsvFile": "../../../TestData/LoadData_UTF8_BOM_Unix.TSV", "CertificatesPath": "../../../../.ci/server/certs" diff --git a/.ci/config/config.compression.json b/.ci/config/config.compression.json index f42f53ab6..bf1073a12 100644 --- a/.ci/config/config.compression.json +++ b/.ci/config/config.compression.json @@ -4,7 +4,7 @@ "SocketPath": "./../../../../.ci/run/mysql/mysqld.sock", "PasswordlessUser": "no_password", "SecondaryDatabase": "testdb2", - "UnsupportedFeatures": "Ed25519,QueryAttributes,StreamingResults,Tls11,UnixDomainSocket,ZeroDateTime", + "UnsupportedFeatures": "Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,UnixDomainSocket,ZeroDateTime", "MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV", "MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV" } diff --git a/.ci/config/config.json b/.ci/config/config.json index 183b2299c..035c05855 100644 --- a/.ci/config/config.json +++ b/.ci/config/config.json @@ -4,7 +4,7 @@ "SocketPath": "./../../../../.ci/run/mysql/mysqld.sock", "PasswordlessUser": "no_password", "SecondaryDatabase": "testdb2", - "UnsupportedFeatures": "Ed25519,QueryAttributes,StreamingResults,Tls11,UnixDomainSocket,ZeroDateTime", + "UnsupportedFeatures": "Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,UnixDomainSocket,ZeroDateTime", "MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV", "MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV" } diff --git a/.ci/config/config.ssl.json b/.ci/config/config.ssl.json index 84261b1be..705e0a168 100644 --- a/.ci/config/config.ssl.json +++ b/.ci/config/config.ssl.json @@ -4,7 +4,7 @@ "SocketPath": "./../../../../.ci/run/mysql/mysqld.sock", "PasswordlessUser": "no_password", "SecondaryDatabase": "testdb2", - "UnsupportedFeatures": "RsaEncryption,CachingSha2Password,Tls12,Tls13,UuidToBin", + "UnsupportedFeatures": "CachingSha2Password,Redirection,RsaEncryption,Tls12,Tls13,UuidToBin", "MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV", "MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV", "CertificatesPath": "../../../../.ci/server/certs" diff --git a/azure-pipelines.yml b/azure-pipelines.yml index dbe7aaba9..47aa77a39 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -136,7 +136,7 @@ jobs: arguments: '-c Release --no-restore' testRunTitle: ${{ format('{0}, $(Agent.OS), {1}, {2}', 'mysql:8.0', 'net472/net8.0', 'No SSL') }} env: - DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,StreamingResults,Tls11,UnixDomainSocket' + DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,UnixDomainSocket' DATA__CONNECTIONSTRING: 'server=localhost;port=3306;user id=mysqltest;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600;AllowPublicKeyRetrieval=True;UseCompression=True' - job: windows_integration_tests_2 @@ -174,7 +174,7 @@ jobs: arguments: '-c Release --no-restore' testRunTitle: ${{ format('{0}, $(Agent.OS), {1}, {2}', 'mysql:8.0', 'net6.0', 'No SSL') }} env: - DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,StreamingResults,Tls11,UnixDomainSocket' + DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,UnixDomainSocket' DATA__CONNECTIONSTRING: 'server=localhost;port=3306;user id=mysqltest;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600;AllowPublicKeyRetrieval=True' - job: linux_integration_tests @@ -187,15 +187,15 @@ jobs: 'MySQL 8.0': image: 'mysql:8.0' connectionStringExtra: 'AllowPublicKeyRetrieval=True' - unsupportedFeatures: 'Ed25519,StreamingResults,Tls11,ZeroDateTime,Redirection' + unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,ZeroDateTime' 'MySQL 8.4': image: 'mysql:8.4' connectionStringExtra: 'AllowPublicKeyRetrieval=True' - unsupportedFeatures: 'Ed25519,StreamingResults,Tls11,ZeroDateTime,Redirection' + unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,ZeroDateTime' 'MySQL 9.0': image: 'mysql:9.0' connectionStringExtra: 'AllowPublicKeyRetrieval=True' - unsupportedFeatures: 'Ed25519,StreamingResults,Tls11,ZeroDateTime,Redirection' + unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,ZeroDateTime' 'MariaDB 10.6': image: 'mariadb:10.6' connectionStringExtra: '' diff --git a/tests/IntegrationTests/RedirectionTests.cs b/tests/IntegrationTests/RedirectionTests.cs index 123923bc2..892f1eb7f 100644 --- a/tests/IntegrationTests/RedirectionTests.cs +++ b/tests/IntegrationTests/RedirectionTests.cs @@ -28,90 +28,79 @@ public void RedirectionTest() var csb = AppConfig.CreateConnectionStringBuilder(); var initialServer = csb.Server; var initialPort = csb.Port; - var permitRedirection = true; + m_database.Connection.Execute($"set @@global.redirect_url=\"mariadb://{initialServer}:{initialPort}\""); + try { - m_database.Connection.Execute( - $"set @@global.redirect_url=\"mariadb://{initialServer}:{initialPort}\""); - } - catch (Exception) - { - permitRedirection = false; - } + // changing to proxy port + csb.Server = "localhost"; + csb.Port = (uint)proxy.ListenPort; + csb.ServerRedirectionMode = MySqlServerRedirectionMode.Preferred; - if (permitRedirection) - { - try + // ensure that connection has been redirected + using (var db = new MySqlConnection(csb.ConnectionString)) { - // changing to proxy port - csb.Server = "localhost"; - csb.Port = (uint)proxy.ListenPort; - csb.ServerRedirectionMode = MySqlServerRedirectionMode.Preferred; - - // ensure that connection has been redirected - using (var db = new MySqlConnection(csb.ConnectionString)) + db.Open(); + using (var cmd = db.CreateCommand()) { - db.Open(); - using (var cmd = db.CreateCommand()) - { - cmd.CommandText = "SELECT 1"; - cmd.ExecuteNonQuery(); - } - - Assert.Equal((int) initialPort, db.SessionEndPoint!.Port); - db.Close(); + cmd.CommandText = "SELECT 1"; + cmd.ExecuteNonQuery(); } - // ensure that connection has been redirected with Required - csb.ServerRedirectionMode = MySqlServerRedirectionMode.Required; - using (var db = new MySqlConnection(csb.ConnectionString)) - { - db.Open(); - using (var cmd = db.CreateCommand()) - { - cmd.CommandText = "SELECT 1"; - cmd.ExecuteNonQuery(); - } - - Assert.Equal((int) initialPort, db.SessionEndPoint!.Port); - db.Close(); - } + Assert.Equal((int) initialPort, db.SessionEndPoint!.Port); + db.Close(); + } - // ensure that redirection is not done - csb.ServerRedirectionMode = MySqlServerRedirectionMode.Disabled; - using (var db = new MySqlConnection(csb.ConnectionString)) + // ensure that connection has been redirected with Required + csb.ServerRedirectionMode = MySqlServerRedirectionMode.Required; + using (var db = new MySqlConnection(csb.ConnectionString)) + { + db.Open(); + using (var cmd = db.CreateCommand()) { - db.Open(); - using (var cmd = db.CreateCommand()) - { - cmd.CommandText = "SELECT 1"; - cmd.ExecuteNonQuery(); - } - - Assert.Equal(proxy.ListenPort, db.SessionEndPoint!.Port); - db.Close(); + cmd.CommandText = "SELECT 1"; + cmd.ExecuteNonQuery(); } - } finally{ - m_database.Connection.Execute( - $"set @@global.redirect_url=\"\""); + Assert.Equal((int) initialPort, db.SessionEndPoint!.Port); + db.Close(); } - MySqlConnection.ClearAllPools(); - // ensure that when required, throwing error if no redirection - csb.ServerRedirectionMode = MySqlServerRedirectionMode.Required; + + // ensure that redirection is not done + csb.ServerRedirectionMode = MySqlServerRedirectionMode.Disabled; using (var db = new MySqlConnection(csb.ConnectionString)) { - try - { - db.Open(); - Assert.Fail("must have thrown error"); - } - catch (MySqlException ex) + db.Open(); + using (var cmd = db.CreateCommand()) { - Assert.Equal((int) MySqlErrorCode.UnableToConnectToHost, ex.Number); + cmd.CommandText = "SELECT 1"; + cmd.ExecuteNonQuery(); } + + Assert.Equal(proxy.ListenPort, db.SessionEndPoint!.Port); + db.Close(); } + + } finally{ + m_database.Connection.Execute( + $"set @@global.redirect_url=\"\""); } + MySqlConnection.ClearAllPools(); + // ensure that when required, throwing error if no redirection + csb.ServerRedirectionMode = MySqlServerRedirectionMode.Required; + using (var db = new MySqlConnection(csb.ConnectionString)) + { + try + { + db.Open(); + Assert.Fail("must have thrown error"); + } + catch (MySqlException ex) + { + Assert.Equal((int) MySqlErrorCode.UnableToConnectToHost, ex.Number); + } + } + StopProxy(); } diff --git a/tests/IntegrationTests/ServerFeatures.cs b/tests/IntegrationTests/ServerFeatures.cs index bfbec9fe8..ac4fa4863 100644 --- a/tests/IntegrationTests/ServerFeatures.cs +++ b/tests/IntegrationTests/ServerFeatures.cs @@ -40,5 +40,4 @@ public enum ServerFeatures /// Server permit redirection, available on first OK_Packet /// Redirection = 0x80_0000, - }