// 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 { /// /// Implements common elements that allow to manage the hosts available for client side failover. /// internal static class FailoverManager { /// /// Gets and sets the failover group which consists of a host list. /// internal static FailoverGroup FailoverGroup { get; private set; } /// /// Resets the manager. /// internal static void Reset() { if (FailoverGroup != null) { FailoverGroup = null; } } /// /// Sets the host list to be used during failover operations. /// /// The host list. /// The failover method. internal static void SetHostList(List 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; } } /// /// Attempts to establish a connection to a host specified from the list. /// /// The original connection string set by the user. /// An out parameter that stores the updated connection string. /// A object in case this is a pooling scenario. /// A flag indicating if the default port is used in the connection. /// An instance if the connection was succesfully established, a exception is thrown otherwise. // 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(); // } // 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; // } /// /// Attempts to establish a connection to a host specified from the list. /// /// MySqlConnection object where the new driver will be assigned /// The original connection string set by the user. /// An out parameter that stores the updated connection string. /// A in case this is a pooling scenario."/> 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(); } 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); } } /// /// Creates a if more than one host is found. /// /// A string containing an unparsed list of hosts. /// true if the connection is X Protocol; otherwise false. /// true if the connection data is a URI; otherwise false. /// The number of hosts found, -1 if an error was raised during parsing. 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 hostList = new List(); 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; } /// /// Creates a object based on the provided parameters. /// /// The host string that can be a simple host name or a host name and port. /// The priority of the host. /// The port number of the host. /// true if the connection data is a URI; otherwise false. /// 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 { /// /// Attempts the next host in the list. Moves to the first element if the end of the list is reached. /// Sequential, /// /// Determines the next host on which to attempt a connection by checking the value of the Priority property in descending order. /// Priority, /// /// Determines the next host on which to attempt a connection randomly. /// Random } }