// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // // This code is copied from the source described in the comment below. // ======================================================================================= // Microsoft Windows Server AppFabric Customer Advisory Team (CAT) Best Practices Series // // This sample is supplemental to the technical guidance published on the community // blog at http://blogs.msdn.com/appfabriccat/ and copied from // sqlmain ./sql/manageability/mfx/common/ // // ======================================================================================= // Copyright © 2012 Microsoft Corporation. All rights reserved. // // THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER // EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. YOU BEAR THE RISK OF USING IT. // ======================================================================================= // namespace Microsoft.AppFabricCAT.Samples.Azure.TransientFaultHandling.SqlAzure // namespace Microsoft.SqlServer.Management.Common using System; using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Data.SqlClient; using System.Diagnostics; using System.Globalization; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.SqlTools.Utility; namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection { /// /// Provides a reliable way of opening connections to and executing commands /// taking into account potential network unreliability and a requirement for connection retry. /// internal sealed partial class ReliableSqlConnection : DbConnection, IDisposable { private const string QueryAzureSessionId = "SELECT CONVERT(NVARCHAR(36), CONTEXT_INFO())"; private readonly SqlConnection _underlyingConnection; private readonly RetryPolicy _connectionRetryPolicy; private RetryPolicy _commandRetryPolicy; private Guid _azureSessionId; private bool _isSqlDwDatabase; /// /// Initializes a new instance of the ReliableSqlConnection class with a given connection string /// and a policy defining whether to retry a request if the connection fails to be opened or a command /// fails to be successfully executed. /// /// The connection string used to open the SQL Azure database. /// The retry policy defining whether to retry a request if a connection fails to be established. /// The retry policy defining whether to retry a request if a command fails to be executed. public ReliableSqlConnection(string connectionString, RetryPolicy connectionRetryPolicy, RetryPolicy commandRetryPolicy) { _underlyingConnection = new SqlConnection(connectionString); _connectionRetryPolicy = connectionRetryPolicy ?? RetryPolicyFactory.CreateNoRetryPolicy(); _commandRetryPolicy = commandRetryPolicy ?? RetryPolicyFactory.CreateNoRetryPolicy(); _underlyingConnection.StateChange += OnConnectionStateChange; _connectionRetryPolicy.RetryOccurred += RetryConnectionCallback; _commandRetryPolicy.RetryOccurred += RetryCommandCallback; } /// /// Performs application-defined tasks associated with freeing, releasing, or /// resetting managed and unmanaged resources. /// /// A flag indicating that managed resources must be released. protected override void Dispose(bool disposing) { if (disposing) { if (_connectionRetryPolicy != null) { _connectionRetryPolicy.RetryOccurred -= RetryConnectionCallback; } if (_commandRetryPolicy != null) { _commandRetryPolicy.RetryOccurred -= RetryCommandCallback; } _underlyingConnection.StateChange -= OnConnectionStateChange; if (_underlyingConnection.State == ConnectionState.Open) { _underlyingConnection.Close(); } _underlyingConnection.Dispose(); } } /// /// Determines if a connection is being made to a SQL DW database. /// /// A connection object. private bool IsSqlDwConnection(IDbConnection conn) { //Set the connection only if it has not been set earlier. //This is assuming that it is highly unlikely for a connection to change between instances. //Hence any subsequent calls to this method will just return the cached value and not //verify again if this is a SQL DW database connection or not. if (!CachedServerInfo.Instance.TryGetIsSqlDw(conn, out _isSqlDwDatabase)) { _isSqlDwDatabase = ReliableConnectionHelper.IsSqlDwDatabase(conn); CachedServerInfo.Instance.AddOrUpdateIsSqlDw(conn, _isSqlDwDatabase);; } return _isSqlDwDatabase; } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")] internal static void SetLockAndCommandTimeout(IDbConnection conn) { Validate.IsNotNull(nameof(conn), conn); // Make sure we use the underlying connection as ReliableConnection.Open also calls // this method ReliableSqlConnection reliableConn = conn as ReliableSqlConnection; if (reliableConn != null) { conn = reliableConn._underlyingConnection; } const string setLockTimeout = @"set LOCK_TIMEOUT {0}"; using (IDbCommand cmd = conn.CreateCommand()) { cmd.CommandText = string.Format(CultureInfo.InvariantCulture, setLockTimeout, AmbientSettings.LockTimeoutMilliSeconds); cmd.CommandType = CommandType.Text; cmd.CommandTimeout = CachedServerInfo.Instance.GetQueryTimeoutSeconds(conn); cmd.ExecuteNonQuery(); } } internal static void SetDefaultAnsiSettings(IDbConnection conn, bool isSqlDw) { Validate.IsNotNull(nameof(conn), conn); // Make sure we use the underlying connection as ReliableConnection.Open also calls // this method ReliableSqlConnection reliableConn = conn as ReliableSqlConnection; if (reliableConn != null) { conn = reliableConn._underlyingConnection; } // Configure the connection with proper ANSI settings and lock timeout using (IDbCommand cmd = conn.CreateCommand()) { cmd.CommandTimeout = CachedServerInfo.Instance.GetQueryTimeoutSeconds(conn); if (!isSqlDw) { cmd.CommandText = @"SET ANSI_NULLS, ANSI_PADDING, ANSI_WARNINGS, ARITHABORT, CONCAT_NULL_YIELDS_NULL, QUOTED_IDENTIFIER ON; SET NUMERIC_ROUNDABORT OFF;"; } else { cmd.CommandText = @"SET ANSI_NULLS ON; SET ANSI_PADDING ON; SET ANSI_WARNINGS ON; SET ARITHABORT ON; SET CONCAT_NULL_YIELDS_NULL ON; SET QUOTED_IDENTIFIER ON;"; //SQL DW does not support NUMERIC_ROUNDABORT } cmd.ExecuteNonQuery(); } } /// /// Gets or sets the connection string for opening a connection to the SQL Azure database. /// public override string ConnectionString { get { return _underlyingConnection.ConnectionString; } set { _underlyingConnection.ConnectionString = value; } } /// /// Gets the policy which decides whether to retry a connection request, based on how many /// times the request has been made and the reason for the last failure. /// public RetryPolicy ConnectionRetryPolicy { get { return _connectionRetryPolicy; } } /// /// Gets the policy which decides whether to retry a command, based on how many /// times the request has been made and the reason for the last failure. /// public RetryPolicy CommandRetryPolicy { get { return _commandRetryPolicy; } set { Validate.IsNotNull(nameof(value), value); if (_commandRetryPolicy != null) { _commandRetryPolicy.RetryOccurred -= RetryCommandCallback; } _commandRetryPolicy = value; _commandRetryPolicy.RetryOccurred += RetryCommandCallback; } } /// /// Gets the server name from the underlying connection. /// public override string DataSource { get { return _underlyingConnection.DataSource; } } /// /// Gets the server version from the underlying connection. /// public override string ServerVersion { get { return _underlyingConnection.ServerVersion; } } /// /// If the underlying SqlConnection absolutely has to be accessed, for instance /// to pass to external APIs that require this type of connection, then this /// can be used. /// /// public SqlConnection GetUnderlyingConnection() { return _underlyingConnection; } /// /// Begins a database transaction with the specified System.Data.IsolationLevel value. /// /// One of the System.Data.IsolationLevel values. /// An object representing the new transaction. protected override DbTransaction BeginDbTransaction(IsolationLevel level) { return _underlyingConnection.BeginTransaction(level); } /// /// Changes the current database for an open Connection object. /// /// The name of the database to use in place of the current database. public override void ChangeDatabase(string databaseName) { _underlyingConnection.ChangeDatabase(databaseName); } /// /// Opens a database connection with the settings specified by the ConnectionString /// property of the provider-specific Connection object. /// public override void Open() { // Check if retry policy was specified, if not, disable retries by executing the Open method using RetryPolicy.NoRetry. _connectionRetryPolicy.ExecuteAction(() => { if (_underlyingConnection.State != ConnectionState.Open) { _underlyingConnection.Open(); } SetLockAndCommandTimeout(_underlyingConnection); SetDefaultAnsiSettings(_underlyingConnection, IsSqlDwConnection(_underlyingConnection)); }); } /// /// Opens a database connection with the settings specified by the ConnectionString /// property of the provider-specific Connection object. /// public override Task OpenAsync(CancellationToken token) { // Make sure that the token isn't cancelled before we try if (token.IsCancellationRequested) { return Task.FromCanceled(token); } // Check if retry policy was specified, if not, disable retries by executing the Open method using RetryPolicy.NoRetry. try { return _connectionRetryPolicy.ExecuteAction(async () => { if (_underlyingConnection.State != ConnectionState.Open) { await _underlyingConnection.OpenAsync(token); } SetLockAndCommandTimeout(_underlyingConnection); SetDefaultAnsiSettings(_underlyingConnection, IsSqlDwConnection(_underlyingConnection)); }); } catch (Exception e) { return Task.FromException(e); } } /// /// Closes the connection to the database. /// public override void Close() { _underlyingConnection.Close(); } /// /// Gets the time to wait while trying to establish a connection before terminating /// the attempt and generating an error. /// public override int ConnectionTimeout { get { return _underlyingConnection.ConnectionTimeout; } } /// /// Creates and returns an object implementing the IDbCommand interface which is associated /// with the underlying SqlConnection. /// /// A object. protected override DbCommand CreateDbCommand() { return CreateReliableCommand(); } /// /// Creates and returns an object implementing the IDbCommand interface which is associated /// with the underlying SqlConnection. /// /// A object. public SqlCommand CreateSqlCommand() { return _underlyingConnection.CreateCommand(); } /// /// Gets the name of the current database or the database to be used after a /// connection is opened. /// public override string Database { get { return _underlyingConnection.Database; } } /// /// Gets the current state of the connection. /// public override ConnectionState State { get { return _underlyingConnection.State; } } /// /// Adds an info message event Listener. /// /// An info message event Listener. public void AddInfoMessageHandler(SqlInfoMessageEventHandler handler) { _underlyingConnection.InfoMessage += handler; } /// /// Removes an info message event Listener. /// /// An info message event Listener. public void RemoveInfoMessageHandler(SqlInfoMessageEventHandler handler) { _underlyingConnection.InfoMessage -= handler; } /// /// Clears underlying connection pool. /// public void ClearPool() { if (_underlyingConnection != null) { SqlConnection.ClearPool(_underlyingConnection); } } private void RetryCommandCallback(RetryState retryState) { RetryPolicyUtils.RaiseSchemaAmbientRetryMessage(retryState, SqlSchemaModelErrorCodes.ServiceActions.CommandRetry, _azureSessionId); } private void RetryConnectionCallback(RetryState retryState) { RetryPolicyUtils.RaiseSchemaAmbientRetryMessage(retryState, SqlSchemaModelErrorCodes.ServiceActions.ConnectionRetry, _azureSessionId); } public void OnConnectionStateChange(object sender, StateChangeEventArgs e) { SqlConnection conn = (SqlConnection)sender; switch (e.CurrentState) { case ConnectionState.Open: RetrieveSessionId(); break; case ConnectionState.Broken: case ConnectionState.Closed: _azureSessionId = Guid.Empty; break; case ConnectionState.Connecting: case ConnectionState.Executing: case ConnectionState.Fetching: default: break; } } private void RetrieveSessionId() { try { using (IDbCommand command = CreateReliableCommand()) { IDbConnection connection = command.Connection; if (!IsSqlDwConnection(connection)) { command.CommandText = QueryAzureSessionId; object result = command.ExecuteScalar(); // Only returns a session id for SQL Azure if (DBNull.Value != result) { string sessionId = (string)command.ExecuteScalar(); if (!Guid.TryParse(sessionId, out _azureSessionId)) { Logger.Write(TraceEventType.Error, Resources.UnableToRetrieveAzureSessionId); } } } } } catch (Exception exception) { Logger.Write(TraceEventType.Error, Resources.UnableToRetrieveAzureSessionId + exception.ToString()); } } /// /// Creates and returns a ReliableSqlCommand object associated /// with the underlying SqlConnection. /// /// A object. private ReliableSqlCommand CreateReliableCommand() { return new ReliableSqlCommand(this); } private void VerifyConnectionOpen(IDbCommand command) { // Verify whether or not the connection is valid and is open. This code may be retried therefore // it is important to ensure that a connection is re-established should it have previously failed. if (command.Connection == null) { command.Connection = this; } if (command.Connection.State != ConnectionState.Open) { SqlConnection.ClearPool(_underlyingConnection); command.Connection.Open(); } } private IDataReader ExecuteReader(IDbCommand command, CommandBehavior behavior) { Tuple[] sessionSettings = null; return _commandRetryPolicy.ExecuteAction(() => { VerifyConnectionOpen(command); sessionSettings = CacheOrReplaySessionSettings(command, sessionSettings); return command.ExecuteReader(behavior); }); } // Because retry loses session settings, cache session settings or reply if the settings are already cached. internal Tuple[] CacheOrReplaySessionSettings(IDbCommand originalCommand, Tuple[] sessionSettings) { if (sessionSettings == null) { sessionSettings = QuerySessionSettings(originalCommand); } else { SetSessionSettings(originalCommand.Connection, sessionSettings); } return sessionSettings; } private object ExecuteScalar(IDbCommand command) { Tuple[] sessionSettings = null; return _commandRetryPolicy.ExecuteAction(() => { VerifyConnectionOpen(command); sessionSettings = CacheOrReplaySessionSettings(command, sessionSettings); return command.ExecuteScalar(); }); } private Tuple[] QuerySessionSettings(IDbCommand originalCommand) { Tuple[] sessionSettings = new Tuple[2]; IDbConnection connection = originalCommand.Connection; if (IsSqlDwConnection(connection)) { // SESSIONPROPERTY is not supported. Use default values for now sessionSettings[0] = Tuple.Create("ANSI_NULLS", true); sessionSettings[1] = Tuple.Create("QUOTED_IDENTIFIER", true); } else { using (IDbCommand localCommand = connection.CreateCommand()) { // Executing a reader requires preservation of any pending transaction created by the calling command localCommand.Transaction = originalCommand.Transaction; localCommand.CommandText = "SELECT ISNULL(SESSIONPROPERTY ('ANSI_NULLS'), 0), ISNULL(SESSIONPROPERTY ('QUOTED_IDENTIFIER'), 1)"; using (IDataReader reader = localCommand.ExecuteReader()) { if (reader.Read()) { sessionSettings[0] = Tuple.Create("ANSI_NULLS", ((int)reader[0] == 1)); sessionSettings[1] = Tuple.Create("QUOTED_IDENTIFIER", ((int)reader[1] ==1)); } else { Debug.Assert(false, "Reader cannot be empty"); } } } } return sessionSettings; } private void SetSessionSettings(IDbConnection connection, params Tuple[] settings) { List setONOptions = new List(); List setOFFOptions = new List(); if(settings != null) { foreach (Tuple setting in settings) { if (setting.Item2) { setONOptions.Add(setting.Item1); } else { setOFFOptions.Add(setting.Item1); } } } SetSessionSettings(connection, setONOptions, "ON"); SetSessionSettings(connection, setOFFOptions, "OFF"); } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")] private static void SetSessionSettings(IDbConnection connection, List sessionOptions, string onOff) { if (sessionOptions.Count > 0) { using (IDbCommand localCommand = connection.CreateCommand()) { StringBuilder builder = new StringBuilder("SET "); for (int i = 0; i < sessionOptions.Count; i++) { if (i > 0) { builder.Append(','); } builder.Append(sessionOptions[i]); } builder.Append(" "); builder.Append(onOff); localCommand.CommandText = builder.ToString(); localCommand.ExecuteNonQuery(); } } } private int ExecuteNonQuery(IDbCommand command) { Tuple[] sessionSettings = null; return _commandRetryPolicy.ExecuteAction(() => { VerifyConnectionOpen(command); sessionSettings = CacheOrReplaySessionSettings(command, sessionSettings); return command.ExecuteNonQuery(); }); } } }