You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
440 lines
18 KiB
440 lines
18 KiB
// Copyright (c) 2017, 2020 Oracle and/or its affiliates.
|
|
//
|
|
// This program is free software; you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License, version 2.0, as
|
|
// published by the Free Software Foundation.
|
|
//
|
|
// This program is also distributed with certain software (including
|
|
// but not limited to OpenSSL) that is licensed under separate terms,
|
|
// as designated in a particular file or component or in included license
|
|
// documentation. The authors of MySQL hereby grant you an
|
|
// additional permission to link the program and your derivative works
|
|
// with the separately licensed software that they have included with
|
|
// MySQL.
|
|
//
|
|
// Without limiting anything contained in the foregoing, this file,
|
|
// which is part of MySQL Connector/NET, is also subject to the
|
|
// Universal FOSS Exception, version 1.0, a copy of which can be found at
|
|
// http://oss.oracle.com/licenses/universal-foss-exception.
|
|
//
|
|
// This program is distributed in the hope that it will be useful, but
|
|
// WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
// See the GNU General Public License, version 2.0, for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program; if not, write to the Free Software Foundation, Inc.,
|
|
// 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
|
|
using MySql.Data.MySqlClient;
|
|
|
|
// using MySqlX.Sessions;
|
|
// using MySqlX.XDevAPI;
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Threading;
|
|
using Sog.Properties;
|
|
|
|
namespace MySql.Data.Failover
|
|
{
|
|
/// <summary>
|
|
/// Implements common elements that allow to manage the hosts available for client side failover.
|
|
/// </summary>
|
|
internal static class FailoverManager
|
|
{
|
|
/// <summary>
|
|
/// Gets and sets the failover group which consists of a host list.
|
|
/// </summary>
|
|
internal static FailoverGroup FailoverGroup { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Resets the manager.
|
|
/// </summary>
|
|
internal static void Reset()
|
|
{
|
|
if (FailoverGroup != null)
|
|
{
|
|
FailoverGroup = null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the host list to be used during failover operations.
|
|
/// </summary>
|
|
/// <param name="hostList">The host list.</param>
|
|
/// <param name="failoverMethod">The failover method.</param>
|
|
internal static void SetHostList(List<FailoverServer> hostList, FailoverMethod failoverMethod)
|
|
{
|
|
if (FailoverGroup != null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
switch (failoverMethod)
|
|
{
|
|
case FailoverMethod.Sequential:
|
|
FailoverGroup = new SequentialFailoverGroup(hostList);
|
|
break;
|
|
case FailoverMethod.Priority:
|
|
FailoverGroup = new SequentialFailoverGroup(hostList.OrderByDescending(o => o.Priority).ToList());
|
|
break;
|
|
case FailoverMethod.Random:
|
|
FailoverGroup = new RandomFailoverGroup(hostList);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to establish a connection to a host specified from the list.
|
|
/// </summary>
|
|
/// <param name="originalConnectionString">The original connection string set by the user.</param>
|
|
/// <param name="connectionString">An out parameter that stores the updated connection string.</param>
|
|
/// <param name="client">A <see cref="Client"/> object in case this is a pooling scenario.</param>
|
|
/// <param name="isDefaultPort">A flag indicating if the default port is used in the connection.</param>
|
|
/// <returns>An <see cref="InternalSession"/> instance if the connection was succesfully established, a <see cref="MySqlException"/> exception is thrown otherwise.</returns>
|
|
// internal static InternalSession AttemptConnectionXProtocol(string originalConnectionString, out string connectionString, bool isDefaultPort, Client client = null)
|
|
// {
|
|
// if (FailoverGroup == null || originalConnectionString == null)
|
|
// {
|
|
// connectionString = null;
|
|
// return null;
|
|
// }
|
|
|
|
// if (client != null)
|
|
// {
|
|
// if (client.Hosts == null)
|
|
// {
|
|
// client.Hosts = FailoverGroup.Hosts;
|
|
// client.DemotedHosts = new ConcurrentQueue<FailoverServer>();
|
|
// }
|
|
// else
|
|
// {
|
|
// FailoverGroup.Hosts = client.Hosts;
|
|
// }
|
|
// }
|
|
|
|
// FailoverServer currentHost = FailoverGroup.ActiveHost;
|
|
// FailoverServer initialHost = currentHost;
|
|
// MySqlXConnectionStringBuilder Settings = null;
|
|
// InternalSession internalSession = null;
|
|
|
|
// do
|
|
// {
|
|
// // Attempt to connect to each host by retrieving the next host based on the failover method being used.
|
|
// connectionString = originalConnectionString.Contains("server") ?
|
|
// originalConnectionString.Replace(originalConnectionString.Split(';').First(p => p.Contains("server")).Split('=')[1], currentHost.Host) :
|
|
// "server=" + currentHost.Host + ";" + originalConnectionString;
|
|
// if (currentHost != null && currentHost.Port != -1)
|
|
// connectionString = connectionString.Replace(connectionString.Split(';').First(p => p.Contains("port")).Split('=')[1], currentHost.Port.ToString());
|
|
// Settings = new MySqlXConnectionStringBuilder(connectionString, isDefaultPort);
|
|
|
|
// try { internalSession = InternalSession.GetSession(Settings); }
|
|
// catch (Exception) { }
|
|
|
|
// if (internalSession != null)
|
|
// break;
|
|
|
|
// var tmpHost = currentHost;
|
|
// currentHost = FailoverGroup.GetNextHost();
|
|
|
|
// if (client != null)
|
|
// {
|
|
// tmpHost.DemotedTime = DateTime.Now;
|
|
// client.Hosts.Remove(tmpHost);
|
|
// client.DemotedHosts.Enqueue(tmpHost);
|
|
|
|
// if (client.DemotedServersTimer == null)
|
|
// client.DemotedServersTimer = new Timer(new TimerCallback(client.ReleaseDemotedHosts),
|
|
// null, Client.DEMOTED_TIMEOUT, Timeout.Infinite);
|
|
// }
|
|
// }
|
|
// while (!currentHost.Equals(initialHost));
|
|
|
|
// // All connection attempts failed.
|
|
// if (internalSession == null)
|
|
// throw new MySqlException(Resources.UnableToConnectToHost);
|
|
|
|
// return internalSession;
|
|
// }
|
|
|
|
/// <summary>
|
|
/// Attempts to establish a connection to a host specified from the list.
|
|
/// </summary>
|
|
/// <param name="connection">MySqlConnection object where the new driver will be assigned</param>
|
|
/// <param name="originalConnectionString">The original connection string set by the user.</param>
|
|
/// <param name="connectionString">An out parameter that stores the updated connection string.</param>
|
|
/// <param name="mySqlPoolManager">A <see cref="MySqlPoolManager"> in case this is a pooling scenario."/></param>
|
|
internal static void AttemptConnection(MySqlConnection connection, string originalConnectionString, out string connectionString, bool mySqlPoolManager = false)
|
|
{
|
|
if (mySqlPoolManager)
|
|
{
|
|
if (MySqlPoolManager.Hosts == null)
|
|
{
|
|
MySqlPoolManager.Hosts = FailoverGroup.Hosts;
|
|
MySqlPoolManager.DemotedHosts = new ConcurrentQueue<FailoverServer>();
|
|
}
|
|
else
|
|
{
|
|
FailoverGroup.Hosts = MySqlPoolManager.Hosts;
|
|
}
|
|
}
|
|
|
|
FailoverServer currentHost = FailoverGroup.ActiveHost;
|
|
FailoverServer initialHost = currentHost;
|
|
Driver driver = null;
|
|
|
|
do
|
|
{
|
|
// Attempt to connect to each host by retrieving the next host based on the failover method being used
|
|
MySqlConnectionStringBuilder msb;
|
|
connectionString = "server=" + currentHost.Host + ";" + originalConnectionString.Substring(originalConnectionString.IndexOf(';') + 1);
|
|
if (currentHost != null && currentHost.Port != -1)
|
|
{
|
|
connectionString += ";port=" + currentHost.Port;
|
|
}
|
|
|
|
msb = new MySqlConnectionStringBuilder(connectionString);
|
|
|
|
if ((FailoverGroup.Hosts.Count == 1 && !mySqlPoolManager) ||
|
|
(mySqlPoolManager && MySqlPoolManager.Hosts.Count == 1 && MySqlPoolManager.DemotedHosts.IsEmpty))
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
driver = Driver.Create(msb);
|
|
if (!mySqlPoolManager)
|
|
{
|
|
connection.driver = driver;
|
|
}
|
|
|
|
break;
|
|
}
|
|
catch (Exception) { }
|
|
|
|
var tmpHost = currentHost;
|
|
currentHost = FailoverGroup.GetNextHost();
|
|
|
|
if (mySqlPoolManager)
|
|
{
|
|
tmpHost.DemotedTime = DateTime.Now;
|
|
MySqlPoolManager.Hosts.Remove(tmpHost);
|
|
MySqlPoolManager.DemotedHosts.Enqueue(tmpHost);
|
|
|
|
if (MySqlPoolManager.DemotedServersTimer == null)
|
|
{
|
|
MySqlPoolManager.DemotedServersTimer = new Timer(
|
|
new TimerCallback(MySqlPoolManager.ReleaseDemotedHosts),
|
|
null, MySqlPoolManager.DEMOTED_TIMEOUT, Timeout.Infinite);
|
|
}
|
|
}
|
|
} while (!currentHost.Equals(initialHost));
|
|
|
|
if (driver == null)
|
|
{
|
|
throw new MySqlException(Resources.UnableToConnectToHost);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a <see cref="FailoverGroup"/> if more than one host is found.
|
|
/// </summary>
|
|
/// <param name="hierPart">A string containing an unparsed list of hosts.</param>
|
|
/// <param name="isXProtocol"><c>true</c> if the connection is X Protocol; otherwise <c>false</c>.</param>
|
|
/// <param name="connectionDataIsUri"><c>true</c> if the connection data is a URI; otherwise <c>false</c>.</param>
|
|
/// <returns>The number of hosts found, -1 if an error was raised during parsing.</returns>
|
|
internal static int ParseHostList(string hierPart, bool isXProtocol, bool connectionDataIsUri = true)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(hierPart))
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
int hostCount = -1;
|
|
FailoverMethod failoverMethod = FailoverMethod.Random;
|
|
string[] hostArray = null;
|
|
List<FailoverServer> hostList = new List<FailoverServer>();
|
|
hierPart = hierPart.Replace(" ", "");
|
|
|
|
if (!hierPart.StartsWith("(") && !hierPart.EndsWith(")"))
|
|
{
|
|
hostArray = hierPart.Split(',');
|
|
if (hostArray.Length == 1)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
foreach (var host in hostArray)
|
|
{
|
|
hostList.Add(ConvertToFailoverServer(host, connectionDataIsUri: connectionDataIsUri));
|
|
}
|
|
|
|
hostCount = hostArray.Length;
|
|
}
|
|
else
|
|
{
|
|
string[] groups = hierPart.Split(new string[] { "),(" }, StringSplitOptions.RemoveEmptyEntries);
|
|
bool? allHavePriority = null;
|
|
int defaultPriority = -1;
|
|
foreach (var group in groups)
|
|
{
|
|
// Remove leading parenthesis.
|
|
var normalizedGroup = group;
|
|
if (normalizedGroup.StartsWith("("))
|
|
{
|
|
normalizedGroup = group.Substring(1);
|
|
}
|
|
|
|
if (normalizedGroup.EndsWith(")"))
|
|
{
|
|
normalizedGroup = normalizedGroup.Substring(0, normalizedGroup.Length - 1);
|
|
}
|
|
|
|
string[] items = normalizedGroup.Split(',');
|
|
string[] keyValuePairs = items[0].Split('=');
|
|
if (keyValuePairs[0].ToLowerInvariant() != "address")
|
|
{
|
|
throw new KeyNotFoundException(string.Format(ResourcesX.KeywordNotFound, "address"));
|
|
}
|
|
|
|
string host = keyValuePairs[1];
|
|
if (string.IsNullOrWhiteSpace(host))
|
|
{
|
|
throw new ArgumentNullException("server");
|
|
}
|
|
|
|
if (items.Length == 2)
|
|
{
|
|
if (allHavePriority != null && allHavePriority == false)
|
|
{
|
|
throw new ArgumentException(ResourcesX.PriorityForAllOrNoHosts);
|
|
}
|
|
|
|
allHavePriority = allHavePriority ?? true;
|
|
keyValuePairs = items[1].Split('=');
|
|
if (keyValuePairs[0].ToLowerInvariant() != "priority")
|
|
{
|
|
throw new KeyNotFoundException(string.Format(ResourcesX.KeywordNotFound, "priority"));
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(keyValuePairs[1]))
|
|
{
|
|
throw new ArgumentNullException("priority");
|
|
}
|
|
|
|
int priority = -1;
|
|
Int32.TryParse(keyValuePairs[1], out priority);
|
|
if (priority < 0 || priority > 100)
|
|
{
|
|
throw new ArgumentException(ResourcesX.PriorityOutOfLimits);
|
|
}
|
|
|
|
if (isXProtocol)
|
|
{
|
|
;
|
|
}
|
|
|
|
// hostList.Add(ConvertToFailoverServer(BaseSession.IsUnixSocket(host) ? BaseSession.NormalizeUnixSocket(host) : host, priority, connectionDataIsUri: connectionDataIsUri));
|
|
else
|
|
{
|
|
hostList.Add(ConvertToFailoverServer(host, priority));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (allHavePriority != null && allHavePriority == true)
|
|
{
|
|
throw new ArgumentException(ResourcesX.PriorityForAllOrNoHosts);
|
|
}
|
|
|
|
allHavePriority = allHavePriority ?? false;
|
|
|
|
hostList.Add(ConvertToFailoverServer(host, defaultPriority, connectionDataIsUri: connectionDataIsUri));
|
|
}
|
|
}
|
|
|
|
hostCount = groups.Length;
|
|
if (hostList.GroupBy(h => h.Priority).ToList().Count > 1)
|
|
{
|
|
failoverMethod = FailoverMethod.Priority;
|
|
}
|
|
else
|
|
{
|
|
failoverMethod = FailoverMethod.Random;
|
|
}
|
|
}
|
|
|
|
SetHostList(hostList, failoverMethod);
|
|
return hostCount;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a <see cref="FailoverServer"/> object based on the provided parameters.
|
|
/// </summary>
|
|
/// <param name="host">The host string that can be a simple host name or a host name and port.</param>
|
|
/// <param name="priority">The priority of the host.</param>
|
|
/// <param name="port">The port number of the host.</param>
|
|
/// <param name="connectionDataIsUri"><c>true</c> if the connection data is a URI; otherwise <c>false</c>.</param>
|
|
/// <returns></returns>
|
|
private static FailoverServer ConvertToFailoverServer(string host, int priority = -1, int port = -1, bool connectionDataIsUri = true)
|
|
{
|
|
host = host.Trim();
|
|
int colonIndex = -1;
|
|
if (IPAddress.TryParse(host, out IPAddress address))
|
|
{
|
|
switch (address.AddressFamily)
|
|
{
|
|
case System.Net.Sockets.AddressFamily.InterNetworkV6:
|
|
if (host.StartsWith("[") && host.Contains("]") && !host.EndsWith("]"))
|
|
{
|
|
colonIndex = host.LastIndexOf(":");
|
|
}
|
|
|
|
break;
|
|
default:
|
|
colonIndex = host.IndexOf(":");
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
colonIndex = host.IndexOf(":");
|
|
}
|
|
|
|
if (colonIndex != -1)
|
|
{
|
|
if (!connectionDataIsUri)
|
|
{
|
|
throw new ArgumentException(ResourcesX.PortNotSupported);
|
|
}
|
|
|
|
int.TryParse(host.Substring(colonIndex + 1), out port);
|
|
host = host.Substring(0, colonIndex);
|
|
}
|
|
|
|
return new FailoverServer(host, port, priority);
|
|
}
|
|
}
|
|
|
|
internal enum FailoverMethod
|
|
{
|
|
/// <summary>
|
|
/// Attempts the next host in the list. Moves to the first element if the end of the list is reached.
|
|
/// </summary>
|
|
Sequential,
|
|
|
|
/// <summary>
|
|
/// Determines the next host on which to attempt a connection by checking the value of the Priority property in descending order.
|
|
/// </summary>
|
|
Priority,
|
|
|
|
/// <summary>
|
|
/// Determines the next host on which to attempt a connection randomly.
|
|
/// </summary>
|
|
Random
|
|
}
|
|
}
|
|
|