From f2a5654a209f4b54b8e7e90398c3a305393eccd9 Mon Sep 17 00:00:00 2001 From: Mitchell Sternke Date: Tue, 13 Sep 2016 18:10:26 -0700 Subject: [PATCH] Feature/reliable connection (#44) * Initial commit of reliable connection port * Made ReliableSqlConnection inherit from DbConnection instead of IDbConnection * Cleanup * Fixed autocomplete service to use reliable connection * Fix copyright headers * Renamed ConnectResponse.Server to ServerInfo * Removed unused using * Addressing code review feedback --- .../Connection/ConnectionService.cs | 24 + .../Contracts/ConnectParamsExtensions.cs | 2 - .../Connection/Contracts/ConnectResponse.cs | 5 + .../Connection/Contracts/ServerInfo.cs | 63 + .../ReliableConnection/AmbientSettings.cs | 452 ++++++ .../ReliableConnection/CachedServerInfo.cs | 137 ++ .../ReliableConnection/Constants.cs | 17 + .../ReliableConnection/DataSchemaError.cs | 214 +++ .../ReliableConnection/DbCommandWrapper.cs | 71 + .../ReliableConnection/DbConnectionWrapper.cs | 113 ++ .../ReliableConnection/ErrorSeverity.cs | 15 + .../IStackSettingsContext.cs | 19 + .../ReliableConnectionHelper.cs | 1267 +++++++++++++++++ .../ReliableConnection/ReliableSqlCommand.cs | 247 ++++ .../ReliableSqlConnection.cs | 548 +++++++ .../ReliableConnection/Resources.cs | 149 ++ .../RetryCallbackEventArgs.cs | 61 + .../RetryLimitExceededException.cs | 38 + ...licy.DataTransferDetectionErrorStrategy.cs | 43 + .../RetryPolicy.IErrorDetectionStrategy.cs | 97 ++ ...Policy.NetworkConnectivityErrorStrategy.cs | 43 + ...oraryAndIgnorableErrorDetectionStrategy.cs | 63 + ...SqlAzureTemporaryErrorDetectionStrategy.cs | 43 + .../RetryPolicy.ThrottleReason.cs | 357 +++++ .../ReliableConnection/RetryPolicy.cs | 542 +++++++ .../ReliableConnection/RetryPolicyFactory.cs | 459 ++++++ .../ReliableConnection/RetryPolicyUtils.cs | 476 +++++++ .../ReliableConnection/RetryState.cs | 86 ++ .../SqlConnectionHelperScripts.cs | 51 + .../ReliableConnection/SqlErrorNumbers.cs | 32 + .../SqlSchemaModelErrorCodes.cs | 465 ++++++ .../ReliableConnection/SqlServerError.cs | 74 + .../ReliableConnection/SqlServerRetryError.cs | 54 + .../Connection/SqlConnectionFactory.cs | 6 +- .../LanguageServices/LanguageService.cs | 8 +- .../QueryExecution/Batch.cs | 5 +- .../QueryExecution/Query.cs | 5 +- .../Utility/Validate.cs | 17 +- .../Workspace/Workspace.cs | 4 +- .../project.json | 1 + .../Connection/ConnectionServiceTests.cs | 1 - 41 files changed, 6358 insertions(+), 16 deletions(-) create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ServerInfo.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/AmbientSettings.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/CachedServerInfo.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/Constants.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/DataSchemaError.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/DbCommandWrapper.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/DbConnectionWrapper.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/ErrorSeverity.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/IStackSettingsContext.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/ReliableConnectionHelper.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/ReliableSqlCommand.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/ReliableSqlConnection.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/Resources.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryCallbackEventArgs.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryLimitExceededException.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.DataTransferDetectionErrorStrategy.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.IErrorDetectionStrategy.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.NetworkConnectivityErrorStrategy.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.SqlAzureTemporaryAndIgnorableErrorDetectionStrategy.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.SqlAzureTemporaryErrorDetectionStrategy.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.ThrottleReason.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicyFactory.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicyUtils.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryState.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/SqlConnectionHelperScripts.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/SqlErrorNumbers.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/SqlSchemaModelErrorCodes.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/SqlServerError.cs create mode 100644 src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/SqlServerRetryError.cs diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs index 1de51051..c694ca93 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs @@ -11,6 +11,7 @@ using System.Data.SqlClient; using System.Threading.Tasks; using Microsoft.SqlTools.EditorServices.Utility; using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; +using Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection; using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.SqlContext; using Microsoft.SqlTools.ServiceLayer.Workspace; @@ -185,6 +186,29 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection activity(connectionInfo); } + // try to get information about the connected SQL Server instance + try + { + ReliableConnectionHelper.ServerInfo serverInfo = ReliableConnectionHelper.GetServerVersion(connectionInfo.SqlConnection); + response.ServerInfo = new Contracts.ServerInfo() + { + ServerMajorVersion = serverInfo.ServerMajorVersion, + ServerMinorVersion = serverInfo.ServerMinorVersion, + ServerReleaseVersion = serverInfo.ServerReleaseVersion, + EngineEditionId = serverInfo.EngineEditionId, + ServerVersion = serverInfo.ServerVersion, + ServerLevel = serverInfo.ServerLevel, + ServerEdition = serverInfo.ServerEdition, + IsCloud = serverInfo.IsCloud, + AzureVersion = serverInfo.AzureVersion, + OsVersion = serverInfo.OsVersion + }; + } + catch(Exception ex) + { + response.Messages = ex.ToString(); + } + // return the connection result response.ConnectionId = connectionInfo.ConnectionId.ToString(); return response; diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectParamsExtensions.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectParamsExtensions.cs index 9f2c7356..c345532d 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectParamsExtensions.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectParamsExtensions.cs @@ -3,8 +3,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using System; - namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts { /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectResponse.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectResponse.cs index 2dd1b2fa..9066efa8 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectResponse.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectResponse.cs @@ -20,6 +20,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts /// public string Messages { get; set; } + /// + /// Information about the connected server. + /// + public ServerInfo ServerInfo { get; set; } + /// /// Gets or sets the actual Connection established, including Database Name /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ServerInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ServerInfo.cs new file mode 100644 index 00000000..3bc9e73d --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ServerInfo.cs @@ -0,0 +1,63 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts +{ + /// + /// Contract for information on the connected SQL Server instance. + /// + public class ServerInfo + { + /// + /// The major version of the SQL Server instance. + /// + public int ServerMajorVersion { get; set; } + + /// + /// The minor version of the SQL Server instance. + /// + public int ServerMinorVersion { get; set; } + + /// + /// The build of the SQL Server instance. + /// + public int ServerReleaseVersion { get; set; } + + /// + /// The ID of the engine edition of the SQL Server instance. + /// + public int EngineEditionId { get; set; } + + /// + /// String containing the full server version text. + /// + public string ServerVersion { get; set; } + + /// + /// String describing the product level of the server. + /// + public string ServerLevel { get; set; } + + /// + /// The edition of the SQL Server instance. + /// + public string ServerEdition { get; set; } + + /// + /// Whether the SQL Server instance is running in the cloud (Azure) or not. + /// + public bool IsCloud { get; set; } + + /// + /// The version of Azure that the SQL Server instance is running on, if applicable. + /// + public int AzureVersion { get; set; } + + /// + /// The Operating System version string of the machine running the SQL Server instance. + /// + public string OsVersion { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/AmbientSettings.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/AmbientSettings.cs new file mode 100644 index 00000000..e56769dc --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/AmbientSettings.cs @@ -0,0 +1,452 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.SqlTools.EditorServices.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + /// + /// This class represents connection (and other) settings specified by called of the DacFx API. DacFx + /// cannot rely on the registry to supply override values therefore setting overrides must be made + /// by the top-of-the-stack + /// + internal sealed class AmbientSettings + { + private const string LogicalContextName = "__LocalContextConfigurationName"; + + internal enum StreamBackingStore + { + // MemoryStream + Memory = 0, + + // FileStream + File = 1 + } + + // Internal for test purposes + internal const string MasterReferenceFilePathIndex = "MasterReferenceFilePath"; + internal const string DatabaseLockTimeoutIndex = "DatabaseLockTimeout"; + internal const string QueryTimeoutIndex = "QueryTimeout"; + internal const string LongRunningQueryTimeoutIndex = "LongRunningQueryTimeout"; + internal const string AlwaysRetryOnTransientFailureIndex = "AlwaysRetryOnTransientFailure"; + internal const string MaxDataReaderDegreeOfParallelismIndex = "MaxDataReaderDegreeOfParallelism"; + internal const string ConnectionRetryHandlerIndex = "ConnectionRetryHandler"; + internal const string TraceRowCountFailureIndex = "TraceRowCountFailure"; + internal const string TableProgressUpdateIntervalIndex = "TableProgressUpdateInterval"; + internal const string UseOfflineDataReaderIndex = "UseOfflineDataReader"; + internal const string StreamBackingStoreForOfflineDataReadingIndex = "StreamBackingStoreForOfflineDataReading"; + internal const string DisableIndexesForDataPhaseIndex = "DisableIndexesForDataPhase"; + internal const string ReliableDdlEnabledIndex = "ReliableDdlEnabled"; + internal const string ImportModelDatabaseIndex = "ImportModelDatabase"; + internal const string SupportAlwaysEncryptedIndex = "SupportAlwaysEncrypted"; + internal const string SkipObjectTypeBlockingIndex = "SkipObjectTypeBlocking"; + internal const string DoNotSerializeQueryStoreSettingsIndex = "DoNotSerializeQueryStoreSettings"; + internal const string AlwaysEncryptedWizardMigrationIndex = "AlwaysEncryptedWizardMigration"; + + private static readonly AmbientData _defaultSettings; + + static AmbientSettings() + { + _defaultSettings = new AmbientData(); + } + + /// + /// Access to the default ambient settings. Access to these settings is made available + /// for SSDT scenarios where settings are read from the registry and not set explicitly through + /// the API + /// + public static AmbientData DefaultSettings + { + get { return _defaultSettings; } + } + + public static string MasterReferenceFilePath + { + get { return GetValue(MasterReferenceFilePathIndex); } + } + + public static int LockTimeoutMilliSeconds + { + get { return GetValue(DatabaseLockTimeoutIndex); } + } + + public static int QueryTimeoutSeconds + { + get { return GetValue(QueryTimeoutIndex); } + } + + public static int LongRunningQueryTimeoutSeconds + { + get { return GetValue(LongRunningQueryTimeoutIndex); } + } + + public static Action ConnectionRetryMessageHandler + { + get { return GetValue>(ConnectionRetryHandlerIndex); } + } + + public static bool AlwaysRetryOnTransientFailure + { + get { return GetValue(AlwaysRetryOnTransientFailureIndex); } + } + + public static int MaxDataReaderDegreeOfParallelism + { + get { return GetValue(MaxDataReaderDegreeOfParallelismIndex); } + } + + public static int TableProgressUpdateInterval + { + // value of zero means do not fire 'heartbeat' progress events. Non-zero values will + // fire a heartbeat progress event every n seconds. + get { return GetValue(TableProgressUpdateIntervalIndex); } + } + + public static bool TraceRowCountFailure + { + get { return GetValue(TraceRowCountFailureIndex); } + } + + public static bool UseOfflineDataReader + { + get { return GetValue(UseOfflineDataReaderIndex); } + } + + public static StreamBackingStore StreamBackingStoreForOfflineDataReading + { + get { return GetValue(StreamBackingStoreForOfflineDataReadingIndex); } + } + + public static bool DisableIndexesForDataPhase + { + get { return GetValue(DisableIndexesForDataPhaseIndex); } + } + + public static bool ReliableDdlEnabled + { + get { return GetValue(ReliableDdlEnabledIndex); } + } + + public static bool ImportModelDatabase + { + get { return GetValue(ImportModelDatabaseIndex); } + } + + /// + /// Setting that shows whether Always Encrypted is supported. + /// If false, then reverse engineering and script interpretation of a database with any Always Encrypted object will fail + /// + public static bool SupportAlwaysEncrypted + { + get { return GetValue(SupportAlwaysEncryptedIndex); } + } + + public static bool AlwaysEncryptedWizardMigration + { + get { return GetValue(AlwaysEncryptedWizardMigrationIndex); } + } + + /// + /// Setting that determines whether checks for unsupported object types are performed. + /// If false, unsupported object types will prevent extract from being performed. + /// Default value is false. + /// + public static bool SkipObjectTypeBlocking + { + get { return GetValue(SkipObjectTypeBlockingIndex); } + } + + /// + /// Setting that determines whether the Database Options that store Query Store settings will be left out during package serialization. + /// Default value is false. + /// + public static bool DoNotSerializeQueryStoreSettings + { + get { return GetValue(DoNotSerializeQueryStoreSettingsIndex); } + } + + /// + /// Called by top-of-stack API to setup/configure settings that should be used + /// throughout the API (lower in the stack). The settings are reverted once the returned context + /// has been disposed. + /// + public static IStackSettingsContext CreateSettingsContext() + { + return new StackConfiguration(); + } + + private static T1 GetValue(string configIndex) + { + IAmbientDataDirectAccess config = _defaultSettings; + + return (T1)config.Data[configIndex].Value; + } + + /// + /// Data-transfer object that represents a specific configuration + /// + public class AmbientData : IAmbientDataDirectAccess + { + private readonly Dictionary _configuration; + + public AmbientData() + { + _configuration = new Dictionary(StringComparer.OrdinalIgnoreCase); + _configuration[DatabaseLockTimeoutIndex] = new AmbientValue(5000); + _configuration[QueryTimeoutIndex] = new AmbientValue(60); + _configuration[LongRunningQueryTimeoutIndex] = new AmbientValue(0); + _configuration[AlwaysRetryOnTransientFailureIndex] = new AmbientValue(false); + _configuration[ConnectionRetryHandlerIndex] = new AmbientValue(typeof(Action), null); + _configuration[MaxDataReaderDegreeOfParallelismIndex] = new AmbientValue(8); + _configuration[TraceRowCountFailureIndex] = new AmbientValue(false); // default: throw DacException on rowcount mismatch during import/export data validation + _configuration[TableProgressUpdateIntervalIndex] = new AmbientValue(300); // default: fire heartbeat progress update events every 5 minutes + _configuration[UseOfflineDataReaderIndex] = new AmbientValue(false); + _configuration[StreamBackingStoreForOfflineDataReadingIndex] = new AmbientValue(StreamBackingStore.File); //applicable only when UseOfflineDataReader is set to true + _configuration[MasterReferenceFilePathIndex] = new AmbientValue(typeof(string), null); + // Defect 1210884: Enable an option to allow secondary index, check and fk constraints to stay enabled during data upload with import in DACFX for IES + _configuration[DisableIndexesForDataPhaseIndex] = new AmbientValue(true); + _configuration[ReliableDdlEnabledIndex] = new AmbientValue(false); + _configuration[ImportModelDatabaseIndex] = new AmbientValue(true); + _configuration[SupportAlwaysEncryptedIndex] = new AmbientValue(false); + _configuration[AlwaysEncryptedWizardMigrationIndex] = new AmbientValue(false); + _configuration[SkipObjectTypeBlockingIndex] = new AmbientValue(false); + _configuration[DoNotSerializeQueryStoreSettingsIndex] = new AmbientValue(false); + } + + public string MasterReferenceFilePath + { + get { return (string)_configuration[MasterReferenceFilePathIndex].Value; } + set { _configuration[MasterReferenceFilePathIndex].Value = value; } + } + + public int LockTimeoutMilliSeconds + { + get { return (int)_configuration[DatabaseLockTimeoutIndex].Value; } + set { _configuration[DatabaseLockTimeoutIndex].Value = value; } + } + public int QueryTimeoutSeconds + { + get { return (int)_configuration[QueryTimeoutIndex].Value; } + set { _configuration[QueryTimeoutIndex].Value = value; } + } + public int LongRunningQueryTimeoutSeconds + { + get { return (int)_configuration[LongRunningQueryTimeoutIndex].Value; } + set { _configuration[LongRunningQueryTimeoutIndex].Value = value; } + } + public bool AlwaysRetryOnTransientFailure + { + get { return (bool)_configuration[AlwaysRetryOnTransientFailureIndex].Value; } + set { _configuration[AlwaysRetryOnTransientFailureIndex].Value = value; } + } + public Action ConnectionRetryMessageHandler + { + get { return (Action)_configuration[ConnectionRetryHandlerIndex].Value; } + set { _configuration[ConnectionRetryHandlerIndex].Value = value; } + } + public bool TraceRowCountFailure + { + get { return (bool)_configuration[TraceRowCountFailureIndex].Value; } + set { _configuration[TraceRowCountFailureIndex].Value = value; } + } + public int TableProgressUpdateInterval + { + get { return (int)_configuration[TableProgressUpdateIntervalIndex].Value; } + set { _configuration[TableProgressUpdateIntervalIndex].Value = value; } + } + + public bool UseOfflineDataReader + { + get { return (bool)_configuration[UseOfflineDataReaderIndex].Value; } + set { _configuration[UseOfflineDataReaderIndex].Value = value; } + } + + public StreamBackingStore StreamBackingStoreForOfflineDataReading + { + get { return (StreamBackingStore)_configuration[StreamBackingStoreForOfflineDataReadingIndex].Value; } + set { _configuration[StreamBackingStoreForOfflineDataReadingIndex].Value = value; } + } + + public bool DisableIndexesForDataPhase + { + get { return (bool)_configuration[DisableIndexesForDataPhaseIndex].Value; } + set { _configuration[DisableIndexesForDataPhaseIndex].Value = value; } + } + + public bool ReliableDdlEnabled + { + get { return (bool)_configuration[ReliableDdlEnabledIndex].Value; } + set { _configuration[ReliableDdlEnabledIndex].Value = value; } + } + + public bool ImportModelDatabase + { + get { return (bool)_configuration[ImportModelDatabaseIndex].Value; } + set { _configuration[ImportModelDatabaseIndex].Value = value; } + } + + internal bool SupportAlwaysEncrypted + { + get { return (bool)_configuration[SupportAlwaysEncryptedIndex].Value; } + set { _configuration[SupportAlwaysEncryptedIndex].Value = value; } + } + + internal bool AlwaysEncryptedWizardMigration + { + get { return (bool)_configuration[AlwaysEncryptedWizardMigrationIndex].Value; } + set { _configuration[AlwaysEncryptedWizardMigrationIndex].Value = value; } + } + + internal bool SkipObjectTypeBlocking + { + get { return (bool)_configuration[SkipObjectTypeBlockingIndex].Value; } + set { _configuration[SkipObjectTypeBlockingIndex].Value = value; } + } + + internal bool DoNotSerializeQueryStoreSettings + { + get { return (bool)_configuration[DoNotSerializeQueryStoreSettingsIndex].Value; } + set { _configuration[DoNotSerializeQueryStoreSettingsIndex].Value = value; } + } + + /// + /// Provides a way to bulk populate settings from a dictionary + /// + public void PopulateSettings(IDictionary settingsCollection) + { + if (settingsCollection != null) + { + Dictionary newSettings = new Dictionary(); + + // We know all the values are set on the current configuration + foreach (KeyValuePair potentialPair in settingsCollection) + { + AmbientValue currentValue; + if (_configuration.TryGetValue(potentialPair.Key, out currentValue)) + { + object newValue = potentialPair.Value; + newSettings[potentialPair.Key] = newValue; + } + } + + if (newSettings.Count > 0) + { + foreach (KeyValuePair newSetting in newSettings) + { + _configuration[newSetting.Key].Value = newSetting.Value; + } + } + } + } + + /// + /// Logs the Ambient Settings + /// + public void TraceSettings() + { + // NOTE: logging as warning so we can get this data in the IEService DacFx logs + Logger.Write(LogLevel.Warning, Resources.LoggingAmbientSettings); + + foreach (KeyValuePair setting in _configuration) + { + // Log Ambient Settings + Logger.Write( + LogLevel.Warning, + string.Format( + Resources.AmbientSettingFormat, + setting.Key, + setting.Value == null ? setting.Value : setting.Value.Value)); + } + } + + Dictionary IAmbientDataDirectAccess.Data + { + get { return _configuration; } + } + } + + /// + /// This class is used as value in the dictionary to ensure that the type of value is correct. + /// + private class AmbientValue + { + private readonly Type _type; + private readonly bool _isTypeNullable; + private object _value; + + public AmbientValue(object value) + : this(value == null ? null : value.GetType(), value) + { + } + + public AmbientValue(Type type, object value) + { + if (type == null) + { + throw new ArgumentNullException("type"); + } + _type = type; + _isTypeNullable = !type.GetTypeInfo().IsValueType || Nullable.GetUnderlyingType(type) != null; + Value = value; + } + + public object Value + { + get { return _value; } + set + { + if ((_isTypeNullable && value == null) || _type.GetTypeInfo().IsInstanceOfType(value)) + { + _value = value; + } + else + { + Logger.Write(LogLevel.Error, string.Format(Resources.UnableToAssignValue, value.GetType().FullName, _type.FullName)); + } + } + } + } + + /// + /// This private interface allows pass-through access directly to member data + /// + private interface IAmbientDataDirectAccess + { + Dictionary Data { get; } + } + + /// + /// This class encapsulated the concept of configuration that is set on the stack and + /// flows across multiple threads as part of the logical call context + /// + private sealed class StackConfiguration : IStackSettingsContext + { + private readonly AmbientData _data; + + public StackConfiguration() + { + _data = new AmbientData(); + //CallContext.LogicalSetData(LogicalContextName, _data); + } + + public AmbientData Settings + { + get { return _data; } + } + + public void Dispose() + { + Dispose(true); + } + private void Dispose(bool disposing) + { + //CallContext.LogicalSetData(LogicalContextName, null); + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/CachedServerInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/CachedServerInfo.cs new file mode 100644 index 00000000..2a495a45 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/CachedServerInfo.cs @@ -0,0 +1,137 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Concurrent; +using System.Data; +using System.Data.SqlClient; +using System.Linq; +using Microsoft.SqlTools.EditorServices.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + /// + /// This class caches server information for subsequent use + /// + internal static class CachedServerInfo + { + private struct CachedInfo + { + public bool IsAzure; + public DateTime LastUpdate; + } + + private static ConcurrentDictionary _cache; + private static object _cacheLock; + private const int _maxCacheSize = 1024; + private const int _deleteBatchSize = 512; + + private const int MinimalQueryTimeoutSecondsForAzure = 300; + + static CachedServerInfo() + { + _cache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + _cacheLock = new object(); + } + + public static int GetQueryTimeoutSeconds(IDbConnection connection) + { + string dataSource = SafeGetDataSourceFromConnection(connection); + return GetQueryTimeoutSeconds(dataSource); + } + + public static int GetQueryTimeoutSeconds(string dataSource) + { + //keep existing behavior and return the default ambient settings + //if the provided data source is null or whitespace, or the original + //setting is already 0 which means no limit. + int originalValue = AmbientSettings.QueryTimeoutSeconds; + if (string.IsNullOrWhiteSpace(dataSource) + || (originalValue == 0)) + { + return originalValue; + } + + CachedInfo info; + bool hasFound = _cache.TryGetValue(dataSource, out info); + + if (hasFound && info.IsAzure + && originalValue < MinimalQueryTimeoutSecondsForAzure) + { + return MinimalQueryTimeoutSecondsForAzure; + } + else + { + return originalValue; + } + } + + public static void AddOrUpdateIsAzure(IDbConnection connection, bool isAzure) + { + Validate.IsNotNull(nameof(connection), connection); + + SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(connection.ConnectionString); + AddOrUpdateIsAzure(builder.DataSource, isAzure); + } + + public static void AddOrUpdateIsAzure(string dataSource, bool isAzure) + { + Validate.IsNotNullOrWhitespaceString(nameof(dataSource), dataSource); + CachedInfo info; + bool hasFound = _cache.TryGetValue(dataSource, out info); + + if (hasFound && info.IsAzure == isAzure) + { + return; + } + else + { + lock (_cacheLock) + { + if (! _cache.ContainsKey(dataSource)) + { + //delete a batch of old elements when we try to add a new one and + //the capacity limitation is hit + if (_cache.Keys.Count > _maxCacheSize - 1) + { + var keysToDelete = _cache + .OrderBy(x => x.Value.LastUpdate) + .Take(_deleteBatchSize) + .Select(pair => pair.Key); + + foreach (string key in keysToDelete) + { + _cache.TryRemove(key, out info); + } + } + } + + info.IsAzure = isAzure; + info.LastUpdate = DateTime.UtcNow; + _cache.AddOrUpdate(dataSource, info, (key, oldValue) => info); + } + } + } + + private static string SafeGetDataSourceFromConnection(IDbConnection connection) + { + if (connection == null) + { + return null; + } + + try + { + SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(connection.ConnectionString); + return builder.DataSource; + } + catch + { + Logger.Write(LogLevel.Error, String.Format(Resources.FailedToParseConnectionString, connection.ConnectionString)); + return null; + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/Constants.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/Constants.cs new file mode 100644 index 00000000..53c0f96b --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/Constants.cs @@ -0,0 +1,17 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + /// + /// Contains common constants used throughout ReliableConnection code. + /// + internal static class Constants + { + internal const int UndefinedErrorCode = 0; + + internal const string Local = "(local)"; + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/DataSchemaError.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/DataSchemaError.cs new file mode 100644 index 00000000..143b29ec --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/DataSchemaError.cs @@ -0,0 +1,214 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Globalization; + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + /// + /// This class is used to encapsulate all the information needed by the DataSchemaErrorTaskService to create a corresponding entry in the Visual Studio Error List. + /// A component should add this Error Object to the for such purpose. + /// Errors and their children are expected to be thread-safe. Ideally, this means that + /// the objects are just data-transfer-objects initialized during construction. + /// + [Serializable] + internal class DataSchemaError + { + internal const string DefaultPrefix = "SQL"; + private const int MaxErrorCode = 99999; + protected const int UndefinedErrorCode = 0; + + public DataSchemaError() : this(string.Empty, ErrorSeverity.Unknown) + { + } + + public DataSchemaError(string message, ErrorSeverity severity) + : this(message, string.Empty, severity) + { + } + + public DataSchemaError(string message, Exception innerException, ErrorSeverity severity) + : this(message, innerException, string.Empty, 0, severity) + { + } + + public DataSchemaError(string message, string document, ErrorSeverity severity) + : this(message, document, 0, 0, DefaultPrefix, UndefinedErrorCode, severity) + { + } + + public DataSchemaError(string message, string document, int errorCode, ErrorSeverity severity) + : this(message, document, 0, 0, DefaultPrefix, errorCode, severity) + { + } + + public DataSchemaError(string message, string document, int line, int column, ErrorSeverity severity) + : this(message, document,line, column, DefaultPrefix, UndefinedErrorCode, severity) + { + } + + public DataSchemaError(DataSchemaError source, ErrorSeverity severity) + : this(source.Message, source.Document, source.Line, source.Column, source.Prefix, source.ErrorCode, severity) + { + } + + public DataSchemaError( + Exception exception, + string prefix, + int errorCode, + ErrorSeverity severity) + : this(exception, string.Empty, 0, 0, prefix, errorCode, severity) + { + } + + public DataSchemaError( + string message, + Exception exception, + string prefix, + int errorCode, + ErrorSeverity severity) + : this(message, exception, string.Empty, 0, 0, prefix, errorCode, severity) + { + } + + public DataSchemaError( + Exception exception, + string document, + int line, + int column, + string prefix, + int errorCode, + ErrorSeverity severity) + : this(exception.Message, exception, document, line, column, prefix, errorCode, severity) + { + } + + public DataSchemaError( + string message, + string document, + int line, + int column, + string prefix, + int errorCode, + ErrorSeverity severity) + : this(message, null, document, line, column, prefix, errorCode, severity) + { + } + + public DataSchemaError( + string message, + Exception exception, + string document, + int line, + int column, + string prefix, + int errorCode, + ErrorSeverity severity) + { + if (errorCode > MaxErrorCode || errorCode < 0) + { + throw new ArgumentOutOfRangeException("errorCode"); + } + + Document = document; + Severity = severity; + Line = line; + Column = column; + Message = message; + Exception = exception; + + ErrorCode = errorCode; + Prefix = prefix; + IsPriorityEditable = true; + } + + /// + /// The filename of the error. It corresponds to the File column on the Visual Studio Error List window. + /// + public string Document { get; set; } + + /// + /// The severity of the error + /// + public ErrorSeverity Severity { get; private set; } + + public int ErrorCode { get; private set; } + + /// + /// Line Number of the error + /// + public int Line { get; set; } + + /// + /// Column Number of the error + /// + public int Column { get; set; } + + /// + /// Prefix of the error + /// + public string Prefix { get; private set; } + + /// + /// If the error has any special help topic, this property may hold the ID to the same. + /// + public string HelpKeyword { get; set; } + + /// + /// Exception associated with the error, or null + /// + public Exception Exception { get; set; } + + /// + /// Message + /// + public string Message { get; set; } + + /// + /// Should this message honor the "treat warnings as error" flag? + /// + public Boolean IsPriorityEditable { get; set; } + + /// + /// Represents the error code used in MSBuild output. This is the prefix and the + /// error code + /// + /// + public string BuildErrorCode + { + get { return FormatErrorCode(Prefix, ErrorCode); } + } + + internal Boolean IsBuildErrorCodeDefined + { + get { return (ErrorCode != UndefinedErrorCode); } + } + + /// + /// true if this error is being displayed in ErrorList. More of an Accounting Mechanism to be used internally. + /// + internal bool IsOnDisplay { get; set; } + + internal static string FormatErrorCode(string prefix, int code) + { + return string.Format( + CultureInfo.InvariantCulture, + "{0}{1:d5}", + prefix, + code); + } + + /// + /// String form of this error. + /// NB: This is for debugging only. + /// + /// String form of the error. + public override string ToString() + { + return string.Format(CultureInfo.CurrentCulture, "{0} - {1}({2},{3}): {4}", FormatErrorCode(Prefix, ErrorCode), Document, Line, Column, Message); + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/DbCommandWrapper.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/DbCommandWrapper.cs new file mode 100644 index 00000000..8620f461 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/DbCommandWrapper.cs @@ -0,0 +1,71 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Data; +using System.Data.SqlClient; +using Microsoft.SqlTools.EditorServices.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + + /// + /// Wraps objects that could be a or + /// a , providing common methods across both. + /// + internal sealed class DbCommandWrapper + { + private readonly IDbCommand _command; + private readonly bool _isReliableCommand; + + public DbCommandWrapper(IDbCommand command) + { + Validate.IsNotNull(nameof(command), command); + if (command is ReliableSqlConnection.ReliableSqlCommand) + { + _isReliableCommand = true; + } + else if (!(command is SqlCommand)) + { + throw new InvalidOperationException(Resources.InvalidCommandType); + } + _command = command; + } + + public static bool IsSupportedCommand(IDbCommand command) + { + return command is ReliableSqlConnection.ReliableSqlCommand + || command is SqlCommand; + } + + + public event StatementCompletedEventHandler StatementCompleted + { + add + { + SqlCommand sqlCommand = GetAsSqlCommand(); + sqlCommand.StatementCompleted += value; + } + remove + { + SqlCommand sqlCommand = GetAsSqlCommand(); + sqlCommand.StatementCompleted -= value; + } + } + + /// + /// Gets this as a SqlCommand by casting (if we know it is actually a SqlCommand) + /// or by getting the underlying command (if it's a ReliableSqlCommand) + /// + private SqlCommand GetAsSqlCommand() + { + if (_isReliableCommand) + { + return ((ReliableSqlConnection.ReliableSqlCommand) _command).GetUnderlyingCommand(); + } + return (SqlCommand) _command; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/DbConnectionWrapper.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/DbConnectionWrapper.cs new file mode 100644 index 00000000..c1f1d437 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/DbConnectionWrapper.cs @@ -0,0 +1,113 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Data; +using System.Data.SqlClient; +using Microsoft.SqlTools.EditorServices.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + /// + /// Wraps objects that could be a or + /// a , providing common methods across both. + /// + internal sealed class DbConnectionWrapper + { + private readonly IDbConnection _connection; + private readonly bool _isReliableConnection; + + public DbConnectionWrapper(IDbConnection connection) + { + Validate.IsNotNull(nameof(connection), connection); + if (connection is ReliableSqlConnection) + { + _isReliableConnection = true; + } + else if (!(connection is SqlConnection)) + { + throw new InvalidOperationException(Resources.InvalidConnectionType); + } + + _connection = connection; + } + + public static bool IsSupportedConnection(IDbConnection connection) + { + return connection is ReliableSqlConnection + || connection is SqlConnection; + } + + public event SqlInfoMessageEventHandler InfoMessage + { + add + { + SqlConnection conn = GetAsSqlConnection(); + conn.InfoMessage += value; + } + remove + { + SqlConnection conn = GetAsSqlConnection(); + conn.InfoMessage -= value; + } + } + + public string DataSource + { + get + { + if (_isReliableConnection) + { + return ((ReliableSqlConnection) _connection).DataSource; + } + return ((SqlConnection)_connection).DataSource; + } + } + + public string ServerVersion + { + get + { + if (_isReliableConnection) + { + return ((ReliableSqlConnection)_connection).ServerVersion; + } + return ((SqlConnection)_connection).ServerVersion; + } + } + + /// + /// Gets this as a SqlConnection by casting (if we know it is actually a SqlConnection) + /// or by getting the underlying connection (if it's a ReliableSqlConnection) + /// + public SqlConnection GetAsSqlConnection() + { + if (_isReliableConnection) + { + return ((ReliableSqlConnection) _connection).GetUnderlyingConnection(); + } + return (SqlConnection) _connection; + } + + /* + TODO - IClonable does not exist in .NET Core. + /// + /// Clones the connection and ensures it's opened. + /// If it's a SqlConnection it will clone it, + /// and for ReliableSqlConnection it will clone the underling connection. + /// The reason the entire ReliableSqlConnection is not cloned is that it includes + /// several callbacks and we don't want to try and handle deciding how to clone these + /// yet. + /// + public SqlConnection CloneAndOpenConnection() + { + SqlConnection conn = GetAsSqlConnection(); + SqlConnection clonedConn = ((ICloneable) conn).Clone() as SqlConnection; + clonedConn.Open(); + return clonedConn; + } + */ + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/ErrorSeverity.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/ErrorSeverity.cs new file mode 100644 index 00000000..5cb01c6d --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/ErrorSeverity.cs @@ -0,0 +1,15 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + internal enum ErrorSeverity + { + Unknown = 0, + Error, + Warning, + Message + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/IStackSettingsContext.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/IStackSettingsContext.cs new file mode 100644 index 00000000..a121ab73 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/IStackSettingsContext.cs @@ -0,0 +1,19 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + /// + /// This interface controls the lifetime of settings created as part of the + /// top-of-stack API. Changes made to this context's AmbientData instance will + /// flow to lower in the stack while this object is not disposed. + /// + internal interface IStackSettingsContext : IDisposable + { + AmbientSettings.AmbientData Settings { get; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/ReliableConnectionHelper.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/ReliableConnectionHelper.cs new file mode 100644 index 00000000..5069fdc9 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/ReliableConnectionHelper.cs @@ -0,0 +1,1267 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Security; +using Microsoft.SqlTools.EditorServices.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + internal static class ReliableConnectionHelper + { + private const int PCU1BuildNumber = 2816; + public readonly static SqlConnectionStringBuilder BuilderWithDefaultApplicationName = new SqlConnectionStringBuilder("server=(local);"); + + private const string ServerNameLocalhost = "localhost"; + private const string SqlProviderName = "System.Data.SqlClient"; + + private const string ApplicationIntent = "ApplicationIntent"; + private const string MultiSubnetFailover = "MultiSubnetFailover"; + private const string DacFxApplicationName = "DacFx"; + + // See MSDN documentation for "SERVERPROPERTY (SQL Azure Database)" for "EngineEdition" property: + // http://msdn.microsoft.com/en-us/library/ee336261.aspx + private const int SqlAzureEngineEditionId = 5; + + /// + /// Opens the connection and sets the lock/command timeout and pooling=false. + /// + /// The opened connection + public static IDbConnection OpenConnection(SqlConnectionStringBuilder csb, bool useRetry) + { + csb.Pooling = false; + return OpenConnection(csb.ToString(), useRetry); + } + + /// + /// Opens the connection and sets the lock/command timeout. This routine + /// will assert if pooling!=false. + /// + /// The opened connection + public static IDbConnection OpenConnection(string connectionString, bool useRetry) + { +#if DEBUG + try + { + SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(connectionString); + Debug.Assert(builder.Pooling == false, "Pooling should be false"); + } + catch (Exception ex) + { + Debug.Assert(false, "Invalid connectionstring: " + ex.Message); + } +#endif + + if (AmbientSettings.AlwaysRetryOnTransientFailure == true) + { + useRetry = true; + } + + RetryPolicy commandRetryPolicy, connectionRetryPolicy; + if (useRetry) + { + commandRetryPolicy = RetryPolicyFactory.CreateDefaultSchemaCommandRetryPolicy(useRetry: true); + connectionRetryPolicy = RetryPolicyFactory.CreateDefaultSchemaConnectionRetryPolicy(); + } + else + { + commandRetryPolicy = RetryPolicyFactory.CreateNoRetryPolicy(); + connectionRetryPolicy = RetryPolicyFactory.CreateNoRetryPolicy(); + } + + ReliableSqlConnection connection = new ReliableSqlConnection(connectionString, connectionRetryPolicy, commandRetryPolicy); + + try + { + connection.Open(); + } + catch (Exception ex) + { + + string debugMessage = String.Format(CultureInfo.CurrentCulture, + "Opening connection using connection string '{0}' failed with exception: {1}", connectionString, ex.Message); +#if DEBUG + Debug.WriteLine(debugMessage); +#endif + connection.Dispose(); + throw; + } + + return connection; + } + + /// + /// Opens the connection (if it is not already) and sets + /// the lock/command timeout. + /// + /// The connection to open + public static void OpenConnection(IDbConnection conn) + { + if (conn.State == ConnectionState.Closed) + { + conn.Open(); + } + } + + /// + /// Opens a connection using 'csb' as the connection string. Provide + /// 'usingConnection' to execute T-SQL against the open connection and + /// 'catchException' to handle errors. + /// + /// The connection string used when opening the IDbConnection + /// delegate called when the IDbConnection has been successfully opened + /// delegate called when an exception has occurred. Pass back 'true' to handle the + /// exception, 'false' to throw. If Null is passed in then all exceptions are thrown. + /// Should retry logic be used when opening the connection + public static void OpenConnection( + SqlConnectionStringBuilder csb, + Action usingConnection, + Predicate catchException, + bool useRetry) + { + Validate.IsNotNull(nameof(csb), csb); + Validate.IsNotNull(nameof(usingConnection), usingConnection); + + try + { + // Always disable pooling + csb.Pooling = false; + using (IDbConnection conn = OpenConnection(csb.ConnectionString, useRetry)) + { + usingConnection(conn); + } + } + catch (Exception ex) + { + if (catchException == null || !catchException(ex)) + { + throw; + } + } + } + + /* + TODO - re-enable if we port ConnectionStringSecurer + /// + /// This method provides the provides a connection string configured with the specified database name. + /// This is also an opportunity to decrypt the connection string based on the encryption/decryption strategy. + /// InvalidConnectionStringException could be thrown since this routine attempts to restore the connection + /// string if 'restoreConnectionString' is true. + /// + /// Will only set DatabaseName/ApplicationName if the value is not null. + /// + /// + public static SqlConnectionStringBuilder ConfigureConnectionString( + string connectionString, + string databaseName, + string applicationName, + bool restoreConnectionString = true) + { + if (restoreConnectionString) + { + // Read the connection string through the persistence layer + connectionString = ConnectionStringSecurer.RestoreConnectionString( + connectionString, + SqlProviderName); + } + + SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(connectionString); + + builder.Pooling = false; + builder.MultipleActiveResultSets = false; + + // Cannot set the applicationName/initialCatalog to null but empty string is valid + if (databaseName != null) + { + builder.InitialCatalog = databaseName; + } + + if (applicationName != null) + { + builder.ApplicationName = applicationName; + } + + return builder; + } + */ + + /// + /// Optional 'initializeConnection' routine. This sets the lock and command timeout for the connection. + /// + public static void SetLockAndCommandTimeout(IDbConnection conn) + { + ReliableSqlConnection.SetLockAndCommandTimeout(conn); + } + + /// + /// Opens a IDbConnection, creates a IDbCommand and calls ExecuteNonQuery against the connection. + /// + /// The connection string. + /// The scalar T-SQL command. + /// Optional delegate to initialize the IDbCommand before execution. + /// Default is SqlConnectionHelper.SetCommandTimeout + /// delegate called when an exception has occurred. Pass back 'true' to handle the + /// exception, 'false' to throw. If Null is passed in then all exceptions are thrown. + /// Should a retry policy be used when calling ExecuteNonQuery + /// The number of rows affected + public static object ExecuteNonQuery( + SqlConnectionStringBuilder csb, + string commandText, + Action initializeCommand, + Predicate catchException, + bool useRetry) + { + object retObject = null; + OpenConnection( + csb, + (connection) => + { + retObject = ExecuteNonQuery(connection, commandText, initializeCommand, catchException); + }, + catchException, + useRetry); + + return retObject; + } + + /// + /// Creates a IDbCommand and calls ExecuteNonQuery against the connection. + /// + /// The connection. This must be opened. + /// The scalar T-SQL command. + /// Optional delegate to initialize the IDbCommand before execution. + /// Default is SqlConnectionHelper.SetCommandTimeout + /// Optional exception handling. Pass back 'true' to handle the + /// exception, 'false' to throw. If Null is passed in then all exceptions are thrown. + /// The number of rows affected + [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")] + public static object ExecuteNonQuery( + IDbConnection conn, + string commandText, + Action initializeCommand, + Predicate catchException) + { + Validate.IsNotNull(nameof(conn), conn); + Validate.IsNotNullOrEmptyString(nameof(commandText), commandText); + + IDbCommand cmd = null; + try + { + + Debug.Assert(conn.State == ConnectionState.Open, "connection passed to ExecuteNonQuery should be open."); + + cmd = conn.CreateCommand(); + if (initializeCommand == null) + { + initializeCommand = SetCommandTimeout; + } + initializeCommand(cmd); + + cmd.CommandText = commandText; + cmd.CommandType = CommandType.Text; + + return cmd.ExecuteNonQuery(); + } + catch (Exception ex) + { + if (catchException == null || !catchException(ex)) + { + throw; + } + } + finally + { + if (cmd != null) + { + cmd.Dispose(); + } + } + return null; + } + + /// + /// Creates a IDbCommand and calls ExecuteScalar against the connection. + /// + /// The connection. This must be opened. + /// The scalar T-SQL command. + /// Optional delegate to initialize the IDbCommand before execution. + /// Default is SqlConnectionHelper.SetCommandTimeout + /// Optional exception handling. Pass back 'true' to handle the + /// exception, 'false' to throw. If Null is passed in then all exceptions are thrown. + /// The scalar result + [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")] + public static object ExecuteScalar( + IDbConnection conn, + string commandText, + Action initializeCommand = null, + Predicate catchException = null) + { + Validate.IsNotNull(nameof(conn), conn); + Validate.IsNotNullOrEmptyString(nameof(commandText), commandText); + + IDbCommand cmd = null; + + try + { + Debug.Assert(conn.State == ConnectionState.Open, "connection passed to ExecuteScalar should be open."); + + cmd = conn.CreateCommand(); + if (initializeCommand == null) + { + initializeCommand = SetCommandTimeout; + } + initializeCommand(cmd); + + cmd.CommandText = commandText; + cmd.CommandType = CommandType.Text; + return cmd.ExecuteScalar(); + } + catch (Exception ex) + { + if (catchException == null || !catchException(ex)) + { + throw; + } + } + finally + { + if (cmd != null) + { + cmd.Dispose(); + } + } + return null; + } + + /// + /// Creates a IDbCommand and calls ExecuteReader against the connection. + /// + /// The connection to execute the reader on. This must be opened. + /// The command text to execute + /// A delegate used to read from the reader + /// Optional delegate to initialize the IDbCommand object + /// Optional exception handling. Pass back 'true' to handle the + /// exception, 'false' to throw. If Null is passed in then all exceptions are thrown. + [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")] + public static void ExecuteReader( + IDbConnection conn, + string commandText, + Action readResult, + Action initializeCommand = null, + Predicate catchException = null) + { + Validate.IsNotNull(nameof(conn), conn); + Validate.IsNotNullOrEmptyString(nameof(commandText), commandText); + Validate.IsNotNull(nameof(readResult), readResult); + + IDbCommand cmd = null; + try + { + Debug.Assert(conn.State == ConnectionState.Open, "connection passed to ExecuteReader should be open."); + + cmd = conn.CreateCommand(); + + if (initializeCommand == null) + { + initializeCommand = SetCommandTimeout; + } + + initializeCommand(cmd); + + cmd.CommandText = commandText; + cmd.CommandType = CommandType.Text; + using (IDataReader reader = cmd.ExecuteReader()) + { + readResult(reader); + } + } + catch (Exception ex) + { + if (catchException == null || !catchException(ex)) + { + throw; + } + } + finally + { + if (cmd != null) + { + cmd.Dispose(); + } + } + } + + /// + /// optional 'initializeCommand' routine. This initializes the IDbCommand + /// + /// + public static void SetCommandTimeout(IDbCommand cmd) + { + Validate.IsNotNull(nameof(cmd), cmd); + cmd.CommandTimeout = CachedServerInfo.GetQueryTimeoutSeconds(cmd.Connection); + } + + + /// + /// Return true if the database is an Azure database + /// + /// + /// + public static bool IsCloud(IDbConnection connection) + { + Validate.IsNotNull(nameof(connection), connection); + if (!(connection.State == ConnectionState.Open)) + { + Logger.Write(LogLevel.Warning, Resources.ConnectionPassedToIsCloudShouldBeOpen); + } + + Func executeCommand = commandText => + { + bool result = false; + ExecuteReader(connection, + commandText, + readResult: (reader) => + { + reader.Read(); + int engineEditionId = int.Parse(reader[0].ToString(), CultureInfo.InvariantCulture); + + result = IsCloudEngineId(engineEditionId); + } + ); + return result; + }; + + bool isSqlCloud = false; + try + { + isSqlCloud = executeCommand(SqlConnectionHelperScripts.EngineEdition); + } + catch (SqlException) + { + // The default query contains a WITH (NOLOCK). This doesn't work for Azure DW, so when things don't work out, + // we'll fall back to a version without NOLOCK and try again. + isSqlCloud = executeCommand(SqlConnectionHelperScripts.EngineEditionWithLock); + } + + return isSqlCloud; + } + + private static bool IsCloudEngineId(int engineEditionId) + { + return engineEditionId == SqlAzureEngineEditionId; + } + + /// + /// Handles the exceptions typically thrown when a SQLConnection is being opened + /// + /// True if the exception was handled + public static bool StandardExceptionHandler(Exception ex) + { + Validate.IsNotNull(nameof(ex), ex); + + if (ex is SqlException || + ex is RetryLimitExceededException) + { + return true; + } + if (ex is InvalidCastException || + ex is ArgumentException || // Thrown when a particular connection string property is invalid (i.e. failover parner = "yes") + ex is InvalidOperationException || // thrown when the connection pool is empty and SQL is down + ex is TimeoutException || + ex is SecurityException) + { + return true; + } + + Logger.Write(LogLevel.Error, ex.ToString()); + return false; + } + + /// + /// Returns the default database path. + /// + /// The connection + /// The delegate used to initialize the command + /// The exception handler delegate. If Null is passed in then all exceptions are thrown + public static string GetDefaultDatabaseFilePath( + IDbConnection conn, + Action initializeCommand = null, + Predicate catchException = null) + { + Validate.IsNotNull(nameof(conn), conn); + + string filePath = null; + ServerInfo info = GetServerVersion(conn); + + if (!info.IsCloud) + { + filePath = GetDefaultDatabasePath(conn, SqlConnectionHelperScripts.GetDatabaseFilePathAndName, initializeCommand, catchException); + } + + return filePath; + } + + /// + /// Returns the log path or null + /// + /// The connection + /// The delegate used to initialize the command + /// The exception handler delegate. If Null is passed in then all exceptions are thrown + public static string GetDefaultDatabaseLogPath( + IDbConnection conn, + Action initializeCommand = null, + Predicate catchException = null) + { + Validate.IsNotNull(nameof(conn), conn); + + string logPath = null; + ServerInfo info = GetServerVersion(conn); + + if (!info.IsCloud) + { + logPath = GetDefaultDatabasePath(conn, SqlConnectionHelperScripts.GetDatabaseLogPathAndName, initializeCommand, catchException); + } + + return logPath; + } + + /// + /// Returns the database path or null + /// + /// The connection + /// The command to issue + /// The delegate used to initialize the command + /// The exception handler delegate. If Null is passed in then all exceptions are thrown + private static string GetDefaultDatabasePath( + IDbConnection conn, + string commandText, + Action initializeCommand = null, + Predicate catchException = null) + { + Validate.IsNotNull(nameof(conn), conn); + Validate.IsNotNullOrEmptyString(nameof(commandText), commandText); + + string filePath = ExecuteScalar(conn, commandText, initializeCommand, catchException) as string; + if (!String.IsNullOrWhiteSpace(filePath)) + { + // Remove filename from the filePath + Uri pathUri; + if (Uri.TryCreate(filePath, UriKind.Absolute, out pathUri) == false) + { + // Invalid Uri + return null; + } + + // Get a current directory path relative to the pathUri + // This will remove filename from the uri. + Uri filePathUri = new Uri(pathUri, "."); + // For file uri we need to get LocalPath instead of file:// url + filePath = filePathUri.IsFile ? filePathUri.LocalPath : filePathUri.OriginalString; + } + return filePath; + } + + /// + /// Returns true if the database is readonly. This routine will swallow the exceptions you might expect from SQL using StandardExceptionHandler. + /// + public static bool IsDatabaseReadonly(SqlConnectionStringBuilder builder) + { + Validate.IsNotNull(nameof(builder), builder); + + if (builder == null) + { + throw new ArgumentNullException("builder"); + } + bool isDatabaseReadOnly = false; + + OpenConnection( + builder, + (connection) => + { + string commandText = String.Format(CultureInfo.InvariantCulture, SqlConnectionHelperScripts.CheckDatabaseReadonly, builder.InitialCatalog); + ExecuteReader(connection, + commandText, + readResult: (reader) => + { + if (reader.Read()) + { + string currentSetting = reader.GetString(1); + if (String.Compare(currentSetting, "ON", StringComparison.OrdinalIgnoreCase) == 0) + { + isDatabaseReadOnly = true; + } + } + }); + }, + (ex) => + { + Logger.Write(LogLevel.Error, ex.ToString()); + return StandardExceptionHandler(ex); // handled + }, + useRetry: true); + + return isDatabaseReadOnly; + } + + public class ServerInfo + { + public int ServerMajorVersion; + public int ServerMinorVersion; + public int ServerReleaseVersion; + public int EngineEditionId; + public string ServerVersion; + public string ServerLevel; + public string ServerEdition; + public bool IsCloud; + public int AzureVersion; + + // In SQL 2012 SP1 Selective XML indexes were added. There is bug where upgraded databases from previous versions + // of SQL Server do not have their metadata upgraded to include the xml_index_type column in the sys.xml_indexes view. Because + // of this, we must detect the presence of the column to determine if we can query for Selective Xml Indexes + public bool IsSelectiveXmlIndexMetadataPresent; + + public bool IsAzureV1 + { + get + { + return IsCloud && AzureVersion == 1; + } + } + + public string OsVersion; + } + + public static bool TryGetServerVersion(string connectionString, out ServerInfo serverInfo) + { + serverInfo = null; + SqlConnectionStringBuilder builder; + if (!TryGetConnectionStringBuilder(connectionString, out builder)) + { + return false; + } + + serverInfo = GetServerVersion(builder); + return true; + } + + /// + /// Returns the version of the server. This routine will throw if an exception is encountered. + /// + public static ServerInfo GetServerVersion(SqlConnectionStringBuilder csb) + { + Validate.IsNotNull(nameof(csb), csb); + ServerInfo serverInfo = null; + + OpenConnection( + csb, + (connection) => + { + serverInfo = GetServerVersion(connection); + }, + catchException: null, // Always throw + useRetry: true); + + return serverInfo; + } + + /// + /// Returns the version of the server. This routine will throw if an exception is encountered. + /// + public static ServerInfo GetServerVersion(IDbConnection connection) + { + Validate.IsNotNull(nameof(connection), connection); + if (!(connection.State == ConnectionState.Open)) + { + Logger.Write(LogLevel.Error, "connection passed to GetServerVersion should be open."); + } + + Func getServerInfo = commandText => + { + ServerInfo serverInfo = new ServerInfo(); + ExecuteReader( + connection, + commandText, + delegate (IDataReader reader) + { + reader.Read(); + int engineEditionId = Int32.Parse(reader[0].ToString(), CultureInfo.InvariantCulture); + + serverInfo.EngineEditionId = engineEditionId; + serverInfo.IsCloud = IsCloudEngineId(engineEditionId); + + serverInfo.ServerVersion = reader[1].ToString(); + serverInfo.ServerLevel = reader[2].ToString(); + serverInfo.ServerEdition = reader[3].ToString(); + + if (reader.FieldCount > 4) + { + // Detect the presence of SXI + serverInfo.IsSelectiveXmlIndexMetadataPresent = reader.GetInt32(4) == 1; + } + + // The 'ProductVersion' server property is of the form ##.#[#].####.#, + Version serverVersion = new Version(serverInfo.ServerVersion); + + // The server version is of the form ##.##.####, + serverInfo.ServerMajorVersion = serverVersion.Major; + serverInfo.ServerMinorVersion = serverVersion.Minor; + serverInfo.ServerReleaseVersion = serverVersion.Build; + + if (serverInfo.IsCloud) + { + serverInfo.AzureVersion = serverVersion.Major > 11 ? 2 : 1; + } + + try + { + CachedServerInfo.AddOrUpdateIsAzure(connection, serverInfo.IsCloud); + } + catch (Exception ex) + { + //we don't want to fail the normal flow if any unexpected thing happens + //during caching although it's unlikely. So we just log the exception and ignore it + Logger.Write(LogLevel.Error, Resources.FailedToCacheIsCloud); + Logger.Write(LogLevel.Error, ex.ToString()); + } + }); + + // Also get the OS Version + ExecuteReader( + connection, + SqlConnectionHelperScripts.GetOsVersion, + delegate (IDataReader reader) + { + reader.Read(); + serverInfo.OsVersion = reader[0].ToString(); + }); + + return serverInfo; + }; + + ServerInfo result = null; + try + { + result = getServerInfo(SqlConnectionHelperScripts.EngineEdition); + } + catch (SqlException) + { + // The default query contains a WITH (NOLOCK). This doesn't work for Azure DW, so when things don't work out, + // we'll fall back to a version without NOLOCK and try again. + result = getServerInfo(SqlConnectionHelperScripts.EngineEditionWithLock); + } + + return result; + } + + public static string GetServerName(IDbConnection connection) + { + return new DbConnectionWrapper(connection).DataSource; + } + + public static string ReadServerVersion(IDbConnection connection) + { + return new DbConnectionWrapper(connection).ServerVersion; + } + + /// + /// Converts to a SqlConnection by casting (if we know it is actually a SqlConnection) + /// or by getting the underlying connection (if it's a ReliableSqlConnection) + /// + public static SqlConnection GetAsSqlConnection(IDbConnection connection) + { + return new DbConnectionWrapper(connection).GetAsSqlConnection(); + } + + /* TODO - CloneAndOpenConnection() requires IClonable, which doesn't exist in .NET Core + /// + /// Clones a connection and ensures it's opened. + /// If it's a SqlConnection it will clone it, + /// and for ReliableSqlConnection it will clone the underling connection. + /// The reason the entire ReliableSqlConnection is not cloned is that it includes + /// several callbacks and we don't want to try and handle deciding how to clone these + /// yet. + /// + public static SqlConnection CloneAndOpenConnection(IDbConnection connection) + { + return new DbConnectionWrapper(connection).CloneAndOpenConnection(); + } + */ + + public class ServerAndDatabaseInfo : ServerInfo + { + public int DbCompatibilityLevel; + public string DatabaseName; + } + + private static bool TryGetConnectionStringBuilder(string connectionString, out SqlConnectionStringBuilder builder) + { + builder = null; + + if (String.IsNullOrEmpty(connectionString)) + { + // Connection string is not valid + return false; + } + + // Attempt to initialize the builder + Exception handledEx = null; + try + { + builder = new SqlConnectionStringBuilder(connectionString); + } + catch (KeyNotFoundException ex) + { + handledEx = ex; + } + catch (FormatException ex) + { + handledEx = ex; + } + catch (ArgumentException ex) + { + handledEx = ex; + } + + if (handledEx != null) + { + Logger.Write(LogLevel.Error, String.Format(Resources.ErrorParsingConnectionString, handledEx)); + return false; + } + + return true; + } + + /* + /// + /// Get the version of the server and database using + /// the connection string provided. This routine will + /// throw if an exception is encountered. + /// + /// The connection string used to connect to the database. + /// Basic information about the server + public static bool GetServerAndDatabaseVersion(string connectionString, out ServerAndDatabaseInfo info) + { + bool foundVersion = false; + info = new ServerAndDatabaseInfo { IsCloud = false, ServerMajorVersion = -1, DbCompatibilityLevel = -1, DatabaseName = String.Empty }; + + SqlConnectionStringBuilder builder; + if (!TryGetConnectionStringBuilder(connectionString, out builder)) + { + return false; + } + + // The database name is either the InitialCatalog or the AttachDBFilename. The + // AttachDBFilename is used if an mdf file is specified in the connections dialog. + if (String.IsNullOrEmpty(builder.InitialCatalog) || + String.IsNullOrEmpty(builder.AttachDBFilename)) + { + builder.Pooling = false; + + string tempDatabaseName = String.Empty; + int tempDbCompatibilityLevel = 0; + ServerInfo serverInfo = null; + + OpenConnection( + builder, + (connection) => + { + // Set the lock timeout to 3 seconds + SetLockAndCommandTimeout(connection); + + serverInfo = GetServerVersion(connection); + + tempDatabaseName = (String.IsNullOrEmpty(builder.InitialCatalog) == false) ? + builder.InitialCatalog : builder.AttachDBFilename; + + // If at this point the dbName remained an empty string then + // we should get the database name from the open IDbConnection + if (String.IsNullOrEmpty(tempDatabaseName)) + { + tempDatabaseName = connection.Database; + } + + // SQL Azure does not support custom DBCompat values. + if (!serverInfo.IsAzureV1) + { + SqlParameter databaseNameParameter = new SqlParameter( + "@dbname", + SqlDbType.NChar, + 128, + ParameterDirection.Input, + false, + 0, + 0, + null, + DataRowVersion.Default, + tempDatabaseName); + + object compatibilityLevel; + + using (IDbCommand versionCommand = connection.CreateCommand()) + { + versionCommand.CommandText = "SELECT compatibility_level FROM sys.databases WITH (NOLOCK) WHERE name = @dbname"; + versionCommand.CommandType = CommandType.Text; + versionCommand.Parameters.Add(databaseNameParameter); + compatibilityLevel = versionCommand.ExecuteScalar(); + } + + // value is null if db is not online + foundVersion = compatibilityLevel != null && !(compatibilityLevel is DBNull); + if(foundVersion) + { + tempDbCompatibilityLevel = (byte)compatibilityLevel; + } + else + { + string conString = connection.ConnectionString == null ? "null" : connection.ConnectionString; + string dbName = tempDatabaseName == null ? "null" : tempDatabaseName; + string message = string.Format(CultureInfo.CurrentCulture, + "Querying database compatibility level failed. Connection string: '{0}'. dbname: '{1}'.", + conString, dbName); + Tracer.TraceEvent(TraceEventType.Error, TraceId.CoreServices, message); + } + } + else + { + foundVersion = true; + } + }, + catchException: null, // Always throw + useRetry: true); + + info.IsCloud = serverInfo.IsCloud; + info.ServerMajorVersion = serverInfo.ServerMajorVersion; + info.ServerMinorVersion = serverInfo.ServerMinorVersion; + info.ServerReleaseVersion = serverInfo.ServerReleaseVersion; + info.ServerVersion = serverInfo.ServerVersion; + info.ServerLevel = serverInfo.ServerLevel; + info.ServerEdition = serverInfo.ServerEdition; + info.AzureVersion = serverInfo.AzureVersion; + info.DatabaseName = tempDatabaseName; + info.DbCompatibilityLevel = tempDbCompatibilityLevel; + } + + return foundVersion; + } + */ + + /// + /// Returns true if the authenticating database is master, otherwise false. An example of + /// false is when the user is a contained user connecting to a contained database. + /// + public static bool IsAuthenticatingDatabaseMaster(IDbConnection connection) + { + try + { + const string sqlCommand = + @"use [{0}]; + if (db_id() = 1) + begin + -- contained auth is 0 when connected to master + select 0 + end + else + begin + -- need dynamic sql so that we compile this query only when we know resource db is available + exec('select case when authenticating_database_id = 1 then 0 else 1 end from sys.dm_exec_sessions where session_id = @@SPID') + end"; + + string finalCmd = null; + if (!String.IsNullOrWhiteSpace(connection.Database)) + { + finalCmd = String.Format(CultureInfo.InvariantCulture, sqlCommand, connection.Database); + } + else + { + finalCmd = String.Format(CultureInfo.InvariantCulture, sqlCommand, "master"); + } + + object retValue = ExecuteScalar(connection, finalCmd); + if (retValue != null && retValue.ToString() == "1") + { + // contained auth is 0 when connected to non-master + return false; + } + return true; + } + catch (Exception ex) + { + if (StandardExceptionHandler(ex)) + { + return true; + } + throw; + } + } + + /// + /// Returns true if the authenticating database is master, otherwise false. An example of + /// false is when the user is a contained user connecting to a contained database. + /// + public static bool IsAuthenticatingDatabaseMaster(SqlConnectionStringBuilder builder) + { + bool authIsMaster = true; + OpenConnection( + builder, + usingConnection: (connection) => + { + authIsMaster = IsAuthenticatingDatabaseMaster(connection); + }, + catchException: StandardExceptionHandler, // Don't throw unless it's an unexpected exception + useRetry: true); + return authIsMaster; + } + + /// + /// Returns the form of the server as a it's name - replaces . and (localhost) + /// + public static string GetCompleteServerName(string server) + { + if (String.IsNullOrEmpty(server)) + { + return server; + } + + int nlen = 0; + if (server[0] == '.') + { + nlen = 1; + } + else if (String.Compare(server, Constants.Local, StringComparison.OrdinalIgnoreCase) == 0) + { + nlen = Constants.Local.Length; + } + else if (String.Compare(server, 0, ServerNameLocalhost, 0, ServerNameLocalhost.Length, StringComparison.OrdinalIgnoreCase) == 0) + { + nlen = ServerNameLocalhost.Length; + } + + if (nlen > 0) + { + string strMachine = Environment.MachineName; + if (server.Length == nlen) + return strMachine; + if (server.Length > (nlen + 1) && server[nlen] == '\\') // instance + { + string strRet = strMachine + server.Substring(nlen); + return strRet; + } + } + + return server; + } + + /* + /// + /// Processes a user-supplied connection string and provides a trimmed connection string + /// that eliminates everything except for DataSource, InitialCatalog, UserId, Password, + /// ConnectTimeout, Encrypt, TrustServerCertificate and IntegratedSecurity. + /// + /// When connection string is invalid + public static string TrimConnectionString(string connectionString) + { + Exception handledException; + + try + { + SqlConnectionStringBuilder scsb = new SqlConnectionStringBuilder(connectionString); + return TrimConnectionStringBuilder(scsb).ConnectionString; + } + catch (ArgumentException exception) + { + handledException = exception; + } + catch (KeyNotFoundException exception) + { + handledException = exception; + } + catch (FormatException exception) + { + handledException = exception; + } + + throw new InvalidConnectionStringException(handledException); + } + */ + + /// + /// Sql 2012 PCU1 introduces breaking changes to metadata queries and adds new Selective XML Index support. + /// This method allows components to detect if the represents a build of SQL 2012 after RTM. + /// + public static bool IsVersionGreaterThan2012RTM(ServerInfo _serverInfo) + { + return _serverInfo.ServerMajorVersion > 11 || + // Use the presence of SXI metadata rather than build number as upgrade bugs leave out the SXI metadata for some upgraded databases. + _serverInfo.ServerMajorVersion == 11 && _serverInfo.IsSelectiveXmlIndexMetadataPresent; + } + + + // SQL Server: Defect 1122301: ReliableConnectionHelper does not maintain ApplicationIntent + // The ApplicationIntent and MultiSubnetFailover property is not introduced to .NET until .NET 4.0 update 2 + // However, DacFx is officially depends on .NET 4.0 RTM + // So here we want to support both senarios, on machine with 4.0 RTM installed, it will ignore these 2 properties + // On machine with higher .NET version which included those properties, it will pick them up. + public static void TryAddAlwaysOnConnectionProperties(SqlConnectionStringBuilder userBuilder, SqlConnectionStringBuilder trimBuilder) + { + if (userBuilder.ContainsKey(ApplicationIntent)) + { + trimBuilder[ApplicationIntent] = userBuilder[ApplicationIntent]; + } + + if (userBuilder.ContainsKey(MultiSubnetFailover)) + { + trimBuilder[MultiSubnetFailover] = userBuilder[MultiSubnetFailover]; + } + } + + /* TODO - this relies on porting SqlAuthenticationMethodUtils + /// + /// Processes a user-supplied connection string and provides a trimmed connection string + /// that eliminates everything except for DataSource, InitialCatalog, UserId, Password, + /// ConnectTimeout, Encrypt, TrustServerCertificate, IntegratedSecurity and Pooling. + /// + /// + /// Pooling is always set to false to avoid connections remaining open. + /// + /// When connection string is invalid + public static SqlConnectionStringBuilder TrimConnectionStringBuilder(SqlConnectionStringBuilder userBuilder, Action throwException = null) + { + + Exception handledException; + + if (throwException == null) + { + throwException = (propertyName) => + { + throw new InvalidConnectionStringException(String.Format(CultureInfo.CurrentCulture, Resources.UnsupportedConnectionStringArgument, propertyName)); + }; + } + if (!String.IsNullOrEmpty(userBuilder.AttachDBFilename)) + { + throwException("AttachDBFilename"); + } + if (userBuilder.UserInstance) + { + throwException("User Instance"); + } + + try + { + SqlConnectionStringBuilder trimBuilder = new SqlConnectionStringBuilder(); + + if (String.IsNullOrWhiteSpace(userBuilder.DataSource)) + { + throw new InvalidConnectionStringException(); + } + + trimBuilder.ConnectTimeout = userBuilder.ConnectTimeout; + trimBuilder.DataSource = userBuilder.DataSource; + + if (false == String.IsNullOrWhiteSpace(userBuilder.InitialCatalog)) + { + trimBuilder.InitialCatalog = userBuilder.InitialCatalog; + } + + trimBuilder.IntegratedSecurity = userBuilder.IntegratedSecurity; + + if (!String.IsNullOrWhiteSpace(userBuilder.UserID)) + { + trimBuilder.UserID = userBuilder.UserID; + } + + if (!String.IsNullOrWhiteSpace(userBuilder.Password)) + { + trimBuilder.Password = userBuilder.Password; + } + + trimBuilder.TrustServerCertificate = userBuilder.TrustServerCertificate; + trimBuilder.Encrypt = userBuilder.Encrypt; + + if (String.IsNullOrWhiteSpace(userBuilder.ApplicationName) || + String.Equals(BuilderWithDefaultApplicationName.ApplicationName, userBuilder.ApplicationName, StringComparison.Ordinal)) + { + trimBuilder.ApplicationName = DacFxApplicationName; + } + else + { + trimBuilder.ApplicationName = userBuilder.ApplicationName; + } + + TryAddAlwaysOnConnectionProperties(userBuilder, trimBuilder); + + if (SqlAuthenticationMethodUtils.IsAuthenticationSupported()) + { + SqlAuthenticationMethodUtils.SetAuthentication(userBuilder, trimBuilder); + } + + if (SqlAuthenticationMethodUtils.IsCertificateSupported()) + { + SqlAuthenticationMethodUtils.SetCertificate(userBuilder, trimBuilder); + } + + trimBuilder.Pooling = false; + return trimBuilder; + } + catch (ArgumentException exception) + { + handledException = exception; + } + catch (KeyNotFoundException exception) + { + handledException = exception; + } + catch (FormatException exception) + { + handledException = exception; + } + + throw new InvalidConnectionStringException(handledException); + } + + public static bool TryCreateConnectionStringBuilder(string connectionString, out SqlConnectionStringBuilder builder, out Exception handledException) + { + bool success = false; + builder = null; + handledException = null; + try + { + builder = TrimConnectionStringBuilder(new SqlConnectionStringBuilder(connectionString)); + + success = true; + } + catch (InvalidConnectionStringException e) + { + handledException = e; + } + catch (ArgumentException exception) + { + handledException = exception; + } + catch (KeyNotFoundException exception) + { + handledException = exception; + } + catch (FormatException exception) + { + handledException = exception; + } + finally + { + if (handledException != null) + { + success = false; + } + } + return success; + } + */ + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/ReliableSqlCommand.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/ReliableSqlCommand.cs new file mode 100644 index 00000000..4f051688 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/ReliableSqlCommand.cs @@ -0,0 +1,247 @@ +// +// 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.Data; +using System.Data.Common; +using System.Data.SqlClient; +using System.Diagnostics.Contracts; + +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 + { + internal class ReliableSqlCommand : DbCommand + { + private const int Dummy = 0; + private readonly SqlCommand _command; + + // connection is settable + private ReliableSqlConnection _connection; + + public ReliableSqlCommand() + : this(null, Dummy) + { + } + + public ReliableSqlCommand(ReliableSqlConnection connection) + : this(connection, Dummy) + { + Contract.Requires(connection != null); + } + + private ReliableSqlCommand(ReliableSqlConnection connection, int dummy) + { + if (connection != null) + { + _connection = connection; + _command = connection.CreateSqlCommand(); + } + else + { + _command = new SqlCommand(); + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _command.Dispose(); + } + } + + /// + /// Gets or sets the text command to run against the data source. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")] + public override string CommandText + { + get { return _command.CommandText; } + set { _command.CommandText = value; } + } + + /// + /// Gets or sets the wait time before terminating the attempt to execute a command and generating an error. + /// + public override int CommandTimeout + { + get { return _command.CommandTimeout; } + set { _command.CommandTimeout = value; } + } + + /// + /// Gets or sets a value that specifies how the property is interpreted. + /// + public override CommandType CommandType + { + get { return _command.CommandType; } + set { _command.CommandType = value; } + } + + /// + /// Gets or sets the used by this . + /// + protected override DbConnection DbConnection + { + get + { + return _connection; + } + + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + + ReliableSqlConnection newConnection = value as ReliableSqlConnection; + + if (newConnection == null) + { + throw new InvalidOperationException(Resources.OnlyReliableConnectionSupported); + } + + _connection = newConnection; + _command.Connection = _connection._underlyingConnection; + } + } + + /// + /// Gets the . + /// + protected override DbParameterCollection DbParameterCollection + { + get { return _command.Parameters; } + } + + /// + /// Gets or sets the transaction within which the Command object of a .NET Framework data provider executes. + /// + protected override DbTransaction DbTransaction + { + get { return _command.Transaction; } + set { _command.Transaction = value as SqlTransaction; } + } + + /// + /// Gets or sets a value indicating whether the command object should be visible in a customized interface control. + /// + public override bool DesignTimeVisible + { + get { return _command.DesignTimeVisible; } + set { _command.DesignTimeVisible = value; } + } + + /// + /// Gets or sets how command results are applied to the System.Data.DataRow when + /// used by the System.Data.IDataAdapter.Update(System.Data.DataSet) method of + /// a . + /// + public override UpdateRowSource UpdatedRowSource + { + get { return _command.UpdatedRowSource; } + set { _command.UpdatedRowSource = value; } + } + + /// + /// Attempts to cancels the execution of an . + /// + public override void Cancel() + { + _command.Cancel(); + } + + /// + /// Creates a new instance of an object. + /// + /// An object. + protected override DbParameter CreateDbParameter() + { + return _command.CreateParameter(); + } + + /// + /// Executes an SQL statement against the Connection object of a .NET Framework + /// data provider, and returns the number of rows affected. + /// + /// The number of rows affected. + public override int ExecuteNonQuery() + { + ValidateConnectionIsSet(); + return _connection.ExecuteNonQuery(_command); + } + + /// + /// Executes the against the + /// and builds an using one of the values. + /// + /// One of the values. + /// An object. + protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) + { + ValidateConnectionIsSet(); + return (DbDataReader)_connection.ExecuteReader(_command, behavior); + } + + /// + /// Executes the query, and returns the first column of the first row in the + /// resultset returned by the query. Extra columns or rows are ignored. + /// + /// The first column of the first row in the resultset. + public override object ExecuteScalar() + { + ValidateConnectionIsSet(); + return _connection.ExecuteScalar(_command); + } + + /// + /// Creates a prepared (or compiled) version of the command on the data source. + /// + public override void Prepare() + { + _command.Prepare(); + } + + internal SqlCommand GetUnderlyingCommand() + { + return _command; + } + + private void ValidateConnectionIsSet() + { + if (_connection == null) + { + throw new InvalidOperationException(Resources.ConnectionPropertyNotSet); + } + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/ReliableSqlConnection.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/ReliableSqlConnection.cs new file mode 100644 index 00000000..b949cd54 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/ReliableSqlConnection.cs @@ -0,0 +1,548 @@ +// +// 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 Microsoft.SqlTools.EditorServices.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; + + /// + /// 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(); + } + } + + [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.GetQueryTimeoutSeconds(conn); + cmd.ExecuteNonQuery(); + } + } + + internal static void SetDefaultAnsiSettings(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; + } + + // Configure the connection with proper ANSI settings and lock timeout + using (IDbCommand cmd = conn.CreateCommand()) + { + cmd.CommandTimeout = CachedServerInfo.GetQueryTimeoutSeconds(conn); + cmd.CommandText = @"SET ANSI_NULLS, ANSI_PADDING, ANSI_WARNINGS, ARITHABORT, CONCAT_NULL_YIELDS_NULL, QUOTED_IDENTIFIER ON; +SET NUMERIC_ROUNDABORT OFF;"; + 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() + { + OpenConnection(); + } + + /// + /// 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); + } + + /// + /// Opens a database connection with the settings specified by the ConnectionString and ConnectionRetryPolicy properties. + /// + /// An object representing the open connection. + private SqlConnection OpenConnection() + { + // 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); + }); + + return _underlyingConnection; + } + + public void OnConnectionStateChange(object sender, StateChangeEventArgs e) + { + SqlConnection conn = (SqlConnection)sender; + switch (e.CurrentState) + { + case ConnectionState.Open: + RetreiveSessionId(); + break; + case ConnectionState.Broken: + case ConnectionState.Closed: + _azureSessionId = Guid.Empty; + break; + case ConnectionState.Connecting: + case ConnectionState.Executing: + case ConnectionState.Fetching: + default: + break; + } + } + + private void RetreiveSessionId() + { + try + { + using (IDbCommand command = CreateReliableCommand()) + { + command.CommandText = QueryAzureSessionId; + object result = command.ExecuteScalar(); + + // Only returns a session id for SQL Azure + if (DBNull.Value != result) + { + string sessionId = (string)command.ExecuteScalar(); + _azureSessionId = new Guid(sessionId); + } + } + } + catch (SqlException exception) + { + Logger.Write(LogLevel.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; + 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(); + }); + } + } +} + diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/Resources.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/Resources.cs new file mode 100644 index 00000000..3fcbe225 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/Resources.cs @@ -0,0 +1,149 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + /// + /// Contains string resources used throughout ReliableConnection code. + /// + internal static class Resources + { + internal static string AmbientSettingFormat + { + get + { + return "{0}: {1}"; + } + } + + internal static string ConnectionPassedToIsCloudShouldBeOpen + { + get + { + return "connection passed to IsCloud should be open."; + } + } + + internal static string ConnectionPropertyNotSet + { + get + { + return "Connection property has not been initialized."; + } + } + + internal static string ExceptionCannotBeRetried + { + get + { + return "Exception cannot be retried because of err #{0}:{1}"; + } + } + + internal static string ErrorParsingConnectionString + { + get + { + return "Error parsing connection string {0}"; + } + } + + internal static string FailedToCacheIsCloud + { + get + { + return "failed to cache the server property of IsAzure"; + } + } + + internal static string FailedToParseConnectionString + { + get + { + return "failed to parse the provided connection string: {0}"; + } + } + + internal static string IgnoreOnException + { + get + { + return "Retry number {0}. Ignoring Exception: {1}"; + } + } + + internal static string InvalidCommandType + { + get + { + return "Unsupported command object. Use SqlCommand or ReliableSqlCommand."; + } + } + + internal static string InvalidConnectionType + { + get + { + return "Unsupported connection object. Use SqlConnection or ReliableSqlConnection."; + } + } + + internal static string LoggingAmbientSettings + { + get + { + return "Logging Ambient Settings..."; + } + } + + internal static string Mode + { + get + { + return "Mode"; + } + } + + internal static string OnlyReliableConnectionSupported + { + get + { + return "Connection property can only be set to a value of type ReliableSqlConnection."; + } + } + + internal static string RetryOnException + { + get + { + return "Retry number {0}. Delaying {1} ms before next retry. Exception: {2}"; + } + } + + internal static string ThrottlingTypeInfo + { + get + { + return "ThrottlingTypeInfo"; + } + } + + internal static string UnableToAssignValue + { + get + { + return "Unable to assign the value of type {0} to {1}"; + } + } + + internal static string UnableToRetrieveAzureSessionId + { + get + { + return "Unable to retrieve Azure session-id."; + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryCallbackEventArgs.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryCallbackEventArgs.cs new file mode 100644 index 00000000..1fa26cee --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryCallbackEventArgs.cs @@ -0,0 +1,61 @@ +// +// 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.SQL.CAT.BestPractices.SqlAzure.Framework +// namespace Microsoft.SqlServer.Management.Common + +using System; + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + /// + /// Defines a arguments for event handler which will be invoked whenever a retry condition is encountered. + /// + internal sealed class RetryCallbackEventArgs : EventArgs + { + private readonly int _retryCount; + private readonly Exception _exception; + private readonly TimeSpan _delay; + + public RetryCallbackEventArgs(int retryCount, Exception exception, TimeSpan delay) + { + _retryCount = retryCount; + _exception = exception; + _delay = delay; + } + + public TimeSpan Delay + { + get { return _delay; } + } + + public Exception Exception + { + get { return _exception; } + } + + public int RetryCount + { + get { return _retryCount; } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryLimitExceededException.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryLimitExceededException.cs new file mode 100644 index 00000000..0ae8b4e7 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryLimitExceededException.cs @@ -0,0 +1,38 @@ +// +// 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.SQL.CAT.BestPractices.SqlAzure.Framework +// namespace Microsoft.SqlServer.Management.Common + +using System; + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + /// + /// The special type of exception that provides managed exit from a retry loop. The user code can use this + /// exception to notify the retry policy that no further retry attempts are required. + /// + [Serializable] + internal sealed class RetryLimitExceededException : Exception + { + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.DataTransferDetectionErrorStrategy.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.DataTransferDetectionErrorStrategy.cs new file mode 100644 index 00000000..8876fca7 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.DataTransferDetectionErrorStrategy.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Data.SqlClient; +using Microsoft.SqlTools.EditorServices.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + internal abstract partial class RetryPolicy + { + /// + /// Provides the error detection logic for temporary faults that are commonly found during data transfer. + /// + internal sealed class DataTransferErrorDetectionStrategy : ErrorDetectionStrategyBase, IErrorDetectionStrategy + { + private static readonly DataTransferErrorDetectionStrategy instance = new DataTransferErrorDetectionStrategy(); + + public static DataTransferErrorDetectionStrategy Instance + { + get { return instance; } + } + + protected override bool CanRetrySqlException(SqlException sqlException) + { + // Enumerate through all errors found in the exception. + foreach (SqlError err in sqlException.Errors) + { + RetryPolicyUtils.AppendThrottlingDataIfIsThrottlingError(sqlException, err); + if (RetryPolicyUtils.IsNonRetryableDataTransferError(err.Number)) + { + Logger.Write(LogLevel.Error, string.Format(Resources.ExceptionCannotBeRetried, err.Number, err.Message)); + return false; + } + } + + // Default is to treat all SqlException as retriable. + return true; + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.IErrorDetectionStrategy.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.IErrorDetectionStrategy.cs new file mode 100644 index 00000000..bc591616 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.IErrorDetectionStrategy.cs @@ -0,0 +1,97 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Data.SqlClient; + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + internal abstract partial class RetryPolicy + { + public interface IErrorDetectionStrategy + { + /// + /// Determines whether the specified exception represents a temporary failure that can be compensated by a retry. + /// + /// The exception object to be verified. + /// True if the specified exception is considered as temporary, otherwise false. + bool CanRetry(Exception ex); + + /// + /// Determines whether the specified exception can be ignored. + /// + /// The exception object to be verified. + /// True if the specified exception is considered as non-harmful. + bool ShouldIgnoreError(Exception ex); + } + + /// + /// Base class with common retry logic. The core behavior for retrying non SqlExceptions is the same + /// across retry policies + /// + internal abstract class ErrorDetectionStrategyBase : IErrorDetectionStrategy + { + public bool CanRetry(Exception ex) + { + if (ex != null) + { + SqlException sqlException; + if ((sqlException = ex as SqlException) != null) + { + return CanRetrySqlException(sqlException); + } + if (ex is InvalidOperationException) + { + // Operations can throw this exception if the connection is killed before the write starts to the server + // However if there's an inner SqlException it may be a CLR load failure or other non-transient error + if (ex.InnerException != null + && ex.InnerException is SqlException) + { + return CanRetry(ex.InnerException); + } + return true; + } + if (ex is TimeoutException) + { + return true; + } + } + + return false; + } + + public bool ShouldIgnoreError(Exception ex) + { + if (ex != null) + { + SqlException sqlException; + if ((sqlException = ex as SqlException) != null) + { + return ShouldIgnoreSqlException(sqlException); + } + if (ex is InvalidOperationException) + { + // Operations can throw this exception if the connection is killed before the write starts to the server + // However if there's an inner SqlException it may be a CLR load failure or other non-transient error + if (ex.InnerException != null + && ex.InnerException is SqlException) + { + return ShouldIgnoreError(ex.InnerException); + } + } + } + + return false; + } + + protected virtual bool ShouldIgnoreSqlException(SqlException sqlException) + { + return false; + } + + protected abstract bool CanRetrySqlException(SqlException sqlException); + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.NetworkConnectivityErrorStrategy.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.NetworkConnectivityErrorStrategy.cs new file mode 100644 index 00000000..456426d1 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.NetworkConnectivityErrorStrategy.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Data.SqlClient; + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + internal abstract partial class RetryPolicy + { + /// + /// Provides the error detection logic for temporary faults that are commonly found in SQL Azure. + /// The same errors CAN occur on premise also, but they are not seen as often. + /// + internal sealed class NetworkConnectivityErrorDetectionStrategy : ErrorDetectionStrategyBase, IErrorDetectionStrategy + { + private static NetworkConnectivityErrorDetectionStrategy instance = new NetworkConnectivityErrorDetectionStrategy(); + + public static NetworkConnectivityErrorDetectionStrategy Instance + { + get { return instance; } + } + + protected override bool CanRetrySqlException(SqlException sqlException) + { + // Enumerate through all errors found in the exception. + bool foundRetryableError = false; + foreach (SqlError err in sqlException.Errors) + { + RetryPolicyUtils.AppendThrottlingDataIfIsThrottlingError(sqlException, err); + if (!RetryPolicyUtils.IsRetryableNetworkConnectivityError(err.Number)) + { + // If any error is not retryable then cannot retry + return false; + } + foundRetryableError = true; + } + return foundRetryableError; + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.SqlAzureTemporaryAndIgnorableErrorDetectionStrategy.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.SqlAzureTemporaryAndIgnorableErrorDetectionStrategy.cs new file mode 100644 index 00000000..0cf26070 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.SqlAzureTemporaryAndIgnorableErrorDetectionStrategy.cs @@ -0,0 +1,63 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using System.Data.SqlClient; + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + internal abstract partial class RetryPolicy + { + /// + /// Provides the error detection logic for temporary faults that are commonly found in SQL Azure. + /// This strategy is similar to SqlAzureTemporaryErrorDetectionStrategy, but it exposes ways + /// to accept a certain exception and treat it as passing. + /// For example, if we are retrying, and we get a failure that an object already exists, we might + /// want to consider this as passing since the first execution that has timed out (or failed for some other temporary error) + /// might have managed to create the object. + /// + internal sealed class SqlAzureTemporaryAndIgnorableErrorDetectionStrategy : ErrorDetectionStrategyBase, IErrorDetectionStrategy + { + /// + /// Azure error that can be ignored + /// + private readonly IList ignorableAzureErrors = null; + + public SqlAzureTemporaryAndIgnorableErrorDetectionStrategy(params int[] ignorableErrors) + { + this.ignorableAzureErrors = ignorableErrors; + } + + protected override bool CanRetrySqlException(SqlException sqlException) + { + // Enumerate through all errors found in the exception. + bool foundRetryableError = false; + foreach (SqlError err in sqlException.Errors) + { + RetryPolicyUtils.AppendThrottlingDataIfIsThrottlingError(sqlException, err); + if (!RetryPolicyUtils.IsRetryableAzureError(err.Number)) + { + return false; + } + + foundRetryableError = true; + } + return foundRetryableError; + } + + protected override bool ShouldIgnoreSqlException(SqlException sqlException) + { + int errorNumber = sqlException.Number; + + if (ignorableAzureErrors == null) + { + return false; + } + + return ignorableAzureErrors.Contains(errorNumber); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.SqlAzureTemporaryErrorDetectionStrategy.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.SqlAzureTemporaryErrorDetectionStrategy.cs new file mode 100644 index 00000000..43233e85 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.SqlAzureTemporaryErrorDetectionStrategy.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Data.SqlClient; + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + internal abstract partial class RetryPolicy + { + /// + /// Provides the error detection logic for temporary faults that are commonly found in SQL Azure. + /// The same errors CAN occur on premise also, but they are not seen as often. + /// + internal sealed class SqlAzureTemporaryErrorDetectionStrategy : ErrorDetectionStrategyBase, IErrorDetectionStrategy + { + private static SqlAzureTemporaryErrorDetectionStrategy instance = new SqlAzureTemporaryErrorDetectionStrategy(); + + public static SqlAzureTemporaryErrorDetectionStrategy Instance + { + get { return instance; } + } + + protected override bool CanRetrySqlException(SqlException sqlException) + { + // Enumerate through all errors found in the exception. + bool foundRetryableError = false; + foreach (SqlError err in sqlException.Errors) + { + RetryPolicyUtils.AppendThrottlingDataIfIsThrottlingError(sqlException, err); + if (!RetryPolicyUtils.IsRetryableAzureError(err.Number)) + { + // If any error is not retryable then cannot retry + return false; + } + foundRetryableError = true; + } + return foundRetryableError; + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.ThrottleReason.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.ThrottleReason.cs new file mode 100644 index 00000000..64eb2102 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.ThrottleReason.cs @@ -0,0 +1,357 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Data.SqlClient; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + internal abstract partial class RetryPolicy + { + /// + /// Implements an object holding the decoded reason code returned from SQL Azure when encountering throttling conditions. + /// + [Serializable] + public class ThrottlingReason + { + /// + /// Returns the error number that corresponds to throttling conditions reported by SQL Azure. + /// + public const int ThrottlingErrorNumber = 40501; + + /// + /// Gets an unknown throttling condition in the event the actual throttling condition cannot be determined. + /// + public static ThrottlingReason Unknown + { + get + { + var unknownCondition = new ThrottlingReason() { ThrottlingMode = ThrottlingMode.Unknown }; + unknownCondition.throttledResources.Add(Tuple.Create(ThrottledResourceType.Unknown, ThrottlingType.Unknown)); + + return unknownCondition; + } + } + + /// + /// Maintains a collection of key-value pairs where a key is resource type and a value is the type of throttling applied to the given resource type. + /// + private readonly IList> throttledResources = new List>(9); + + /// + /// Provides a compiled regular expression used for extracting the reason code from the error message. + /// + private static readonly Regex sqlErrorCodeRegEx = new Regex(@"Code:\s*(\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + /// + /// Gets the value that reflects the throttling mode in SQL Azure. + /// + public ThrottlingMode ThrottlingMode + { + get; + private set; + } + + /// + /// Gets the list of resources in SQL Azure that were subject to throttling conditions. + /// + public IEnumerable> ThrottledResources + { + get + { + return this.throttledResources; + } + } + + /// + /// Determines throttling conditions from the specified SQL exception. + /// + /// The object containing information relevant to an error returned by SQL Server when encountering throttling conditions. + /// An instance of the object holding the decoded reason codes returned from SQL Azure upon encountering throttling conditions. + public static ThrottlingReason FromException(SqlException ex) + { + if (ex != null) + { + foreach (SqlError error in ex.Errors) + { + if (error.Number == ThrottlingErrorNumber) + { + return FromError(error); + } + } + } + + return Unknown; + } + + /// + /// Determines the throttling conditions from the specified SQL error. + /// + /// The object containing information relevant to a warning or error returned by SQL Server. + /// An instance of the object holding the decoded reason codes returned from SQL Azure when encountering throttling conditions. + public static ThrottlingReason FromError(SqlError error) + { + if (error != null) + { + var match = sqlErrorCodeRegEx.Match(error.Message); + int reasonCode = 0; + + if (match.Success && Int32.TryParse(match.Groups[1].Value, out reasonCode)) + { + return FromReasonCode(reasonCode); + } + } + + return Unknown; + } + + /// + /// Determines the throttling conditions from the specified reason code. + /// + /// The reason code returned by SQL Azure which contains the throttling mode and the exceeded resource types. + /// An instance of the object holding the decoded reason codes returned from SQL Azure when encountering throttling conditions. + public static ThrottlingReason FromReasonCode(int reasonCode) + { + if (reasonCode > 0) + { + // Decode throttling mode from the last 2 bits. + ThrottlingMode throttlingMode = (ThrottlingMode)(reasonCode & 3); + + var condition = new ThrottlingReason() { ThrottlingMode = throttlingMode }; + + // Shift 8 bits to truncate throttling mode. + int groupCode = reasonCode >> 8; + + // Determine throttling type for all well-known resources that may be subject to throttling conditions. + condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.PhysicalDatabaseSpace, (ThrottlingType)(groupCode & 3))); + condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.PhysicalLogSpace, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); + condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.LogWriteIODelay, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); + condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.DataReadIODelay, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); + condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.CPU, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); + condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.DatabaseSize, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); + condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.Internal, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); + condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.WorkerThreads, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); + condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.Internal, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); + + return condition; + } + else + { + return Unknown; + } + } + + /// + /// Gets a value indicating whether physical data file space throttling was reported by SQL Azure. + /// + public bool IsThrottledOnDataSpace + { + get + { + return this.throttledResources.Where(x => x.Item1 == ThrottledResourceType.PhysicalDatabaseSpace).Count() > 0; + } + } + + /// + /// Gets a value indicating whether physical log space throttling was reported by SQL Azure. + /// + public bool IsThrottledOnLogSpace + { + get + { + return this.throttledResources.Where(x => x.Item1 == ThrottledResourceType.PhysicalLogSpace).Count() > 0; + } + } + + /// + /// Gets a value indicating whether transaction activity throttling was reported by SQL Azure. + /// + public bool IsThrottledOnLogWrite + { + get { return this.throttledResources.Where(x => x.Item1 == ThrottledResourceType.LogWriteIODelay).Count() > 0; } + } + + /// + /// Gets a value indicating whether data read activity throttling was reported by SQL Azure. + /// + public bool IsThrottledOnDataRead + { + get { return this.throttledResources.Where(x => x.Item1 == ThrottledResourceType.DataReadIODelay).Count() > 0; } + } + + /// + /// Gets a value indicating whether CPU throttling was reported by SQL Azure. + /// + public bool IsThrottledOnCPU + { + get { return this.throttledResources.Where(x => x.Item1 == ThrottledResourceType.CPU).Count() > 0; } + } + + /// + /// Gets a value indicating whether database size throttling was reported by SQL Azure. + /// + public bool IsThrottledOnDatabaseSize + { + get { return this.throttledResources.Where(x => x.Item1 == ThrottledResourceType.DatabaseSize).Count() > 0; } + } + + /// + /// Gets a value indicating whether concurrent requests throttling was reported by SQL Azure. + /// + public bool IsThrottledOnWorkerThreads + { + get { return this.throttledResources.Where(x => x.Item1 == ThrottledResourceType.WorkerThreads).Count() > 0; } + } + + /// + /// Gets a value indicating whether throttling conditions were not determined with certainty. + /// + public bool IsUnknown + { + get { return ThrottlingMode == ThrottlingMode.Unknown; } + } + + /// + /// Returns a textual representation the current ThrottlingReason object including the information held with respect to throttled resources. + /// + /// A string that represents the current ThrottlingReason object. + public override string ToString() + { + StringBuilder result = new StringBuilder(); + + result.AppendFormat(Resources.Mode, ThrottlingMode); + + var resources = this.throttledResources.Where(x => x.Item1 != ThrottledResourceType.Internal). + Select, string>(x => String.Format(CultureInfo.CurrentCulture, Resources.ThrottlingTypeInfo, x.Item1, x.Item2)). + OrderBy(x => x).ToArray(); + + result.Append(String.Join(", ", resources)); + + return result.ToString(); + } + } + + #region ThrottlingMode enumeration + /// + /// Defines the possible throttling modes in SQL Azure. + /// + public enum ThrottlingMode + { + /// + /// Corresponds to "No Throttling" throttling mode whereby all SQL statements can be processed. + /// + NoThrottling = 0, + + /// + /// Corresponds to "Reject Update / Insert" throttling mode whereby SQL statements such as INSERT, UPDATE, CREATE TABLE and CREATE INDEX are rejected. + /// + RejectUpdateInsert = 1, + + /// + /// Corresponds to "Reject All Writes" throttling mode whereby SQL statements such as INSERT, UPDATE, DELETE, CREATE, DROP are rejected. + /// + RejectAllWrites = 2, + + /// + /// Corresponds to "Reject All" throttling mode whereby all SQL statements are rejected. + /// + RejectAll = 3, + + /// + /// Corresponds to an unknown throttling mode whereby throttling mode cannot be determined with certainty. + /// + Unknown = -1 + } + #endregion + + #region ThrottlingType enumeration + /// + /// Defines the possible throttling types in SQL Azure. + /// + public enum ThrottlingType + { + /// + /// Indicates that no throttling was applied to a given resource. + /// + None = 0, + + /// + /// Corresponds to a Soft throttling type. Soft throttling is applied when machine resources such as, CPU, IO, storage, and worker threads exceed + /// predefined safety thresholds despite the load balancer’s best efforts. + /// + Soft = 1, + + /// + /// Corresponds to a Hard throttling type. Hard throttling is applied when the machine is out of resources, for example storage space. + /// With hard throttling, no new connections are allowed to the databases hosted on the machine until resources are freed up. + /// + Hard = 2, + + /// + /// Corresponds to an unknown throttling type in the event when the throttling type cannot be determined with certainty. + /// + Unknown = 3 + } + #endregion + + #region ThrottledResourceType enumeration + /// + /// Defines the types of resources in SQL Azure which may be subject to throttling conditions. + /// + public enum ThrottledResourceType + { + /// + /// Corresponds to "Physical Database Space" resource which may be subject to throttling. + /// + PhysicalDatabaseSpace = 0, + + /// + /// Corresponds to "Physical Log File Space" resource which may be subject to throttling. + /// + PhysicalLogSpace = 1, + + /// + /// Corresponds to "Transaction Log Write IO Delay" resource which may be subject to throttling. + /// + LogWriteIODelay = 2, + + /// + /// Corresponds to "Database Read IO Delay" resource which may be subject to throttling. + /// + DataReadIODelay = 3, + + /// + /// Corresponds to "CPU" resource which may be subject to throttling. + /// + CPU = 4, + + /// + /// Corresponds to "Database Size" resource which may be subject to throttling. + /// + DatabaseSize = 5, + + /// + /// Corresponds to "SQL Worker Thread Pool" resource which may be subject to throttling. + /// + WorkerThreads = 7, + + /// + /// Corresponds to an internal resource which may be subject to throttling. + /// + Internal = 6, + + /// + /// Corresponds to an unknown resource type in the event when the actual resource cannot be determined with certainty. + /// + Unknown = -1 + } + #endregion + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.cs new file mode 100644 index 00000000..9b554841 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.cs @@ -0,0 +1,542 @@ +// +// 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.SQL.CAT.BestPractices.SqlAzure.Framework +// namespace Microsoft.SqlServer.Management.Common + +using System; +using System.Data.SqlClient; +using System.Diagnostics; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.Threading; +using Microsoft.SqlTools.EditorServices.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + /// + /// Implements a policy defining and implementing the retry mechanism for unreliable actions. + /// + internal abstract partial class RetryPolicy + { + /// + /// Defines a callback delegate which will be invoked whenever a retry condition is encountered. + /// + /// The state of current retry attempt. + internal delegate void RetryCallbackDelegate(RetryState retryState); + + /// + /// Defines a callback delegate which will be invoked whenever an error is ignored on retry. + /// + /// The state of current retry attempt. + internal delegate void IgnoreErrorCallbackDelegate(RetryState retryState); + + private readonly IErrorDetectionStrategy _errorDetectionStrategy; + + protected RetryPolicy(IErrorDetectionStrategy strategy) + { + Contract.Assert(strategy != null); + + _errorDetectionStrategy = strategy; + this.FastFirstRetry = true; + + //TODO Defect 1078447 Validate whether CommandTimeout needs to be used differently in schema/data scenarios + this.CommandTimeoutInSeconds = AmbientSettings.LongRunningQueryTimeoutSeconds; + } + + /// + /// An instance of a callback delegate which will be invoked whenever a retry condition is encountered. + /// + public event RetryCallbackDelegate RetryOccurred; + + /// + /// An instance of a callback delegate which will be invoked whenever an error is ignored on retry. + /// + public event IgnoreErrorCallbackDelegate IgnoreErrorOccurred; + + /// + /// Gets or sets a value indicating whether or not the very first retry attempt will be made immediately + /// whereas the subsequent retries will remain subject to retry interval. + /// + public bool FastFirstRetry { get; set; } + + /// + /// Gets or sets the timeout in seconds of sql commands + /// + public int CommandTimeoutInSeconds + { + get; + set; + } + + /// + /// Gets the error detection strategy of this retry policy + /// + internal IErrorDetectionStrategy ErrorDetectionStrategy + { + get + { + return _errorDetectionStrategy; + } + } + + /// + /// We should only ignore errors if they happen after the first retry. + /// This flag is used to allow the ignore even on first try, for testing purposes. + /// + /// + /// This flag is currently being used for TESTING PURPOSES ONLY. + /// + internal bool ShouldIgnoreOnFirstTry + { + get; + set; + } + + protected static bool IsLessThanMaxRetryCount(int currentRetryCount, int maxRetryCount) + { + return currentRetryCount <= maxRetryCount; + } + + /// + /// Repetitively executes the specified action while it satisfies the current retry policy. + /// + /// A delegate representing the executable action which doesn't return any results. + /// Cancellation token to cancel action between retries. + public void ExecuteAction(Action action, CancellationToken? token = null) + { + ExecuteAction( + _ => action(), token); + } + + /// + /// Repetitively executes the specified action while it satisfies the current retry policy. + /// + /// A delegate representing the executable action which doesn't return any results. + /// Cancellation token to cancel action between retries. + public void ExecuteAction(Action action, CancellationToken? token = null) + { + ExecuteAction( + retryState => + { + action(retryState); + return null; + }, token); + } + + /// + /// Repetitively executes the specified action while it satisfies the current retry policy. + /// + /// The type of result expected from the executable action. + /// A delegate representing the executable action which returns the result of type T. + /// Cancellation token to cancel action between retries. + /// The result from the action. + public T ExecuteAction(Func func, CancellationToken? token = null) + { + return ExecuteAction( + _ => func(), token); + } + + /// + /// Repetitively executes the specified action while it satisfies the current retry policy. + /// + /// The type of result expected from the executable action. + /// A delegate representing the executable action which returns the result of type R. + /// Cancellation token to cancel action between retries. + /// The result from the action. + public R ExecuteAction(Func func, CancellationToken? token = null) + { + RetryState retryState = CreateRetryState(); + + if (token != null) + { + token.Value.ThrowIfCancellationRequested(); + } + + while (true) + { + try + { + return func(retryState); + } + catch (RetryLimitExceededException limitExceededEx) + { + // The user code can throw a RetryLimitExceededException to force the exit from the retry loop. + // The RetryLimitExceeded exception can have an inner exception attached to it. This is the exception + // which we will have to throw up the stack so that callers can handle it. + if (limitExceededEx.InnerException != null) + { + throw limitExceededEx.InnerException; + } + + return default(R); + } + catch (Exception ex) + { + retryState.LastError = ex; + + if (retryState.RetryCount > 0 || this.ShouldIgnoreOnFirstTry) + { + // If we can ignore this error, then break out of the loop and consider this execution as passing + // We return the default value for the type R + if (ShouldIgnoreError(retryState)) + { + OnIgnoreErrorOccurred(retryState); + return default(R); + } + } + + retryState.RetryCount++; + + if (!ShouldRetry(retryState)) + { + throw; + } + } + + OnRetryOccurred(retryState); + + if ((retryState.RetryCount > 1 || !FastFirstRetry) && !retryState.IsDelayDisabled) + { + Thread.Sleep(retryState.Delay); + } + + // check for cancellation after delay. + if (token != null) + { + token.Value.ThrowIfCancellationRequested(); + } + } + } + + protected virtual RetryState CreateRetryState() + { + return new RetryState(); + } + + public bool IsRetryableException(Exception ex) + { + return ErrorDetectionStrategy.CanRetry(ex); + } + + public bool ShouldRetry(RetryState retryState) + { + bool canRetry = ErrorDetectionStrategy.CanRetry(retryState.LastError); + bool shouldRetry = canRetry + && ShouldRetryImpl(retryState); + + Logger.Write(LogLevel.Error, + string.Format( + CultureInfo.InvariantCulture, + "Retry requested: Retry count = {0}. Delay = {1}, SQL Error Number = {2}, Can retry error = {3}, Will retry = {4}", + retryState.RetryCount, + retryState.Delay, + GetErrorNumber(retryState.LastError), + canRetry, + shouldRetry)); + + // Perform an extra check in the delay interval. Should prevent from accidentally ending up with the value of -1 which will block a thread indefinitely. + // In addition, any other negative numbers will cause an ArgumentOutOfRangeException fault which will be thrown by Thread.Sleep. + if (retryState.Delay.TotalMilliseconds < 0) + { + retryState.Delay = TimeSpan.Zero; + } + return shouldRetry; + } + + public bool ShouldIgnoreError(RetryState retryState) + { + bool shouldIgnoreError = ErrorDetectionStrategy.ShouldIgnoreError(retryState.LastError); + + Logger.Write(LogLevel.Error, + string.Format( + CultureInfo.InvariantCulture, + "Ignore Error requested: Retry count = {0}. Delay = {1}, SQL Error Number = {2}, Should Ignore Error = {3}", + retryState.RetryCount, + retryState.Delay, + GetErrorNumber(retryState.LastError), + shouldIgnoreError)); + + return shouldIgnoreError; + } + + /* TODO - Error code does not exist in SqlException for .NET Core + private static int? GetErrorCode(Exception ex) + { + SqlException sqlEx= ex as SqlException; + if (sqlEx == null) + { + return null; + } + + return sqlEx.ErrorCode; + } + */ + + private static int? GetErrorNumber(Exception ex) + { + SqlException sqlEx = ex as SqlException; + if (sqlEx == null) + { + return null; + } + + return sqlEx.Number; + } + + protected abstract bool ShouldRetryImpl(RetryState retryState); + + /// + /// Notifies the subscribers whenever a retry condition is encountered. + /// + /// The state of current retry attempt. + protected virtual void OnRetryOccurred(RetryState retryState) + { + var retryOccurred = RetryOccurred; + if (retryOccurred != null) + { + retryOccurred(retryState); + } + } + + /// + /// Notifies the subscribers whenever an error is ignored on retry. + /// + /// The state of current retry attempt. + protected virtual void OnIgnoreErrorOccurred(RetryState retryState) + { + var ignoreErrorOccurred = IgnoreErrorOccurred; + if (ignoreErrorOccurred != null) + { + ignoreErrorOccurred(retryState); + } + } + + internal class FixedDelayPolicy : RetryPolicy + { + private readonly int _maxRetryCount; + private readonly TimeSpan _intervalBetweenRetries; + + /// + /// Constructs a new instance of the TRetryPolicy class with the specified number of retry attempts and time interval between retries. + /// + /// The to use when checking whether an error is retryable + /// The max number of retry attempts. Should be 1-indexed. + /// The interval between retries. + public FixedDelayPolicy(IErrorDetectionStrategy strategy, int maxRetryCount, TimeSpan intervalBetweenRetries) + : base(strategy) + { + Contract.Assert(maxRetryCount >= 0, "maxRetryCount cannot be a negative number"); + Contract.Assert(intervalBetweenRetries.Ticks >= 0, "intervalBetweenRetries cannot be negative"); + + _maxRetryCount = maxRetryCount; + _intervalBetweenRetries = intervalBetweenRetries; + } + + protected override bool ShouldRetryImpl(RetryState retryState) + { + Contract.Assert(retryState != null); + + if (IsLessThanMaxRetryCount(retryState.RetryCount, _maxRetryCount)) + { + retryState.Delay = _intervalBetweenRetries; + return true; + } + + retryState.Delay = TimeSpan.Zero; + return false; + } + } + + internal class ProgressiveRetryPolicy : RetryPolicy + { + private readonly int _maxRetryCount; + private readonly TimeSpan _initialInterval; + private readonly TimeSpan _increment; + + /// + /// Constructs a new instance of the TRetryPolicy class with the specified number of retry attempts and parameters defining the progressive delay between retries. + /// + /// The to use when checking whether an error is retryable + /// The maximum number of retry attempts. Should be 1-indexed. + /// The initial interval which will apply for the first retry. + /// The incremental time value which will be used for calculating the progressive delay between retries. + public ProgressiveRetryPolicy(IErrorDetectionStrategy strategy, int maxRetryCount, TimeSpan initialInterval, TimeSpan increment) + : base(strategy) + { + Contract.Assert(maxRetryCount >= 0, "maxRetryCount cannot be a negative number"); + Contract.Assert(initialInterval.Ticks >= 0, "retryInterval cannot be negative"); + Contract.Assert(increment.Ticks >= 0, "retryInterval cannot be negative"); + + _maxRetryCount = maxRetryCount; + _initialInterval = initialInterval; + _increment = increment; + } + + protected override bool ShouldRetryImpl(RetryState retryState) + { + Contract.Assert(retryState != null); + + if (IsLessThanMaxRetryCount(retryState.RetryCount, _maxRetryCount)) + { + retryState.Delay = TimeSpan.FromMilliseconds(_initialInterval.TotalMilliseconds + (_increment.TotalMilliseconds * (retryState.RetryCount - 1))); + return true; + } + + retryState.Delay = TimeSpan.Zero; + return false; + } + } + + internal class ExponentialDelayRetryPolicy : RetryPolicy + { + private readonly int _maxRetryCount; + private readonly double _intervalFactor; + private readonly TimeSpan _minInterval; + private readonly TimeSpan _maxInterval; + + /// + /// Constructs a new instance of the TRetryPolicy class with the specified number of retry attempts and parameters defining the progressive delay between retries. + /// + /// The to use when checking whether an error is retryable + /// The maximum number of retry attempts. + /// Controls the speed at which the delay increases - the retryCount is raised to this power as + /// part of the function + /// Minimum interval between retries. The basis for all backoff calculations + /// Maximum interval between retries. Backoff will not take longer than this period. + public ExponentialDelayRetryPolicy(IErrorDetectionStrategy strategy, int maxRetryCount, double intervalFactor, TimeSpan minInterval, TimeSpan maxInterval) + : base(strategy) + { + Contract.Assert(maxRetryCount >= 0, "maxRetryCount cannot be a negative number"); + Contract.Assert(intervalFactor > 1, "intervalFactor Must be > 1 so that the delay increases exponentially"); + Contract.Assert(minInterval.Ticks >= 0, "minInterval cannot be negative"); + Contract.Assert(maxInterval.Ticks >= 0, "maxInterval cannot be negative"); + Contract.Assert(maxInterval.Ticks >= minInterval.Ticks, "maxInterval must be greater than minInterval"); + + _maxRetryCount = maxRetryCount; + _intervalFactor = intervalFactor; + _minInterval = minInterval; + _maxInterval = maxInterval; + } + + protected override bool ShouldRetryImpl(RetryState retryState) + { + Contract.Assert(retryState != null); + + if (IsLessThanMaxRetryCount(retryState.RetryCount, _maxRetryCount)) + { + retryState.Delay = RetryPolicyUtils.CalcExponentialRetryDelay(retryState.RetryCount, _intervalFactor, _minInterval, _maxInterval); + return true; + } + + retryState.Delay = TimeSpan.Zero; + return false; + } + } + + internal class TimeBasedRetryPolicy : RetryPolicy + { + private readonly TimeSpan _minTotalRetryTimeLimit; + private readonly TimeSpan _maxTotalRetryTimeLimit; + private readonly double _totalRetryTimeLimitRate; + + private readonly TimeSpan _minInterval; + private readonly TimeSpan _maxInterval; + private readonly double _intervalFactor; + + private readonly Stopwatch _stopwatch; + + public TimeBasedRetryPolicy( + IErrorDetectionStrategy strategy, + TimeSpan minTotalRetryTimeLimit, + TimeSpan maxTotalRetryTimeLimit, + double totalRetryTimeLimitRate, + TimeSpan minInterval, + TimeSpan maxInterval, + double intervalFactor) + : base(strategy) + { + Contract.Assert(minTotalRetryTimeLimit.Ticks >= 0); + Contract.Assert(maxTotalRetryTimeLimit.Ticks >= minTotalRetryTimeLimit.Ticks); + Contract.Assert(totalRetryTimeLimitRate >= 0); + + Contract.Assert(minInterval.Ticks >= 0); + Contract.Assert(maxInterval.Ticks >= minInterval.Ticks); + Contract.Assert(intervalFactor >= 1); + + _minTotalRetryTimeLimit = minTotalRetryTimeLimit; + _maxTotalRetryTimeLimit = maxTotalRetryTimeLimit; + _totalRetryTimeLimitRate = totalRetryTimeLimitRate; + + _minInterval = minInterval; + _maxInterval = maxInterval; + _intervalFactor = intervalFactor; + + _stopwatch = Stopwatch.StartNew(); + } + + protected override bool ShouldRetryImpl(RetryState retryStateObj) + { + Contract.Assert(retryStateObj is RetryStateEx); + RetryStateEx retryState = (RetryStateEx)retryStateObj; + + // Calculate the delay as exponential value based on the number of retries. + retryState.Delay = + RetryPolicyUtils.CalcExponentialRetryDelay( + retryState.RetryCount, + _intervalFactor, + _minInterval, + _maxInterval); + + // Add the delay to the total retry time + retryState.TotalRetryTime = retryState.TotalRetryTime + retryState.Delay; + + // Calculate the maximum total retry time depending on how long ago was the task (this retry policy) started. + // Longer running tasks are less eager to abort since, more work is has been done. + TimeSpan totalRetryTimeLimit = checked(TimeSpan.FromMilliseconds( + Math.Max( + Math.Min( + _stopwatch.ElapsedMilliseconds * _totalRetryTimeLimitRate, + _maxTotalRetryTimeLimit.TotalMilliseconds), + _minTotalRetryTimeLimit.TotalMilliseconds))); + + if (retryState.TotalRetryTime <= totalRetryTimeLimit) + { + return true; + } + + retryState.Delay = TimeSpan.Zero; + return false; + } + + protected override RetryState CreateRetryState() + { + return new RetryStateEx { TotalRetryTime = TimeSpan.Zero }; + } + + private sealed class RetryStateEx : RetryState + { + public TimeSpan TotalRetryTime { get; set; } + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicyFactory.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicyFactory.cs new file mode 100644 index 00000000..bed9680f --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicyFactory.cs @@ -0,0 +1,459 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Diagnostics; +using System.Globalization; +using Microsoft.SqlTools.EditorServices.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + internal sealed class RetryPolicyDefaults + { + /// + /// The default number of retry attempts. + /// + public const int DefaulSchemaRetryCount = 6; + + /// + /// The default number of retry attempts for create database. + /// + public const int DefaultCreateDatabaseRetryCount = 5; + + /// + /// The default amount of time defining an interval between retries. + /// + public static readonly TimeSpan DefaultSchemaMinInterval = TimeSpan.FromSeconds(2.75); + + /// + /// The default factor to use when determining exponential backoff between retries. + /// + public const double DefaultBackoffIntervalFactor = 2.0; + + /// + /// The default maximum time between retries. + /// + public static readonly TimeSpan DefaultMaxRetryInterval = TimeSpan.FromSeconds(60); + + /// + /// The default number of retry attempts. + /// + public static readonly int DefaultDataCommandRetryCount = 5; + + /// + /// The default number of retry attempts for a connection related error + /// + public static readonly int DefaultConnectionRetryCount = 6; + + /// + /// The default amount of time defining an interval between retries. + /// + public static readonly TimeSpan DefaultDataMinInterval = TimeSpan.FromSeconds(1.0); + + /// + /// The default amount of time defining a time increment between retry attempts in the progressive delay policy. + /// + public static readonly TimeSpan DefaultProgressiveRetryIncrement = TimeSpan.FromMilliseconds(500); + } + + /// + /// Implements a collection of the RetryPolicyInfo elements holding retry policy settings. + /// + internal sealed class RetryPolicyFactory + { + /// + /// Returns a default policy that does no retries, it just invokes action exactly once. + /// + public static readonly RetryPolicy NoRetryPolicy = RetryPolicyFactory.CreateNoRetryPolicy(); + + /// + /// Returns a default policy that does no retries, it just invokes action exactly once. + /// + public static readonly RetryPolicy PrimaryKeyViolationRetryPolicy = RetryPolicyFactory.CreatePrimaryKeyCommandRetryPolicy(); + + /// + /// Implements a strategy that ignores any transient errors. + /// Internal for testing purposes only + /// + internal sealed class TransientErrorIgnoreStrategy : RetryPolicy.IErrorDetectionStrategy + { + private static readonly TransientErrorIgnoreStrategy _instance = new TransientErrorIgnoreStrategy(); + + public static TransientErrorIgnoreStrategy Instance + { + get { return _instance; } + } + + public bool CanRetry(Exception ex) + { + return false; + } + + public bool ShouldIgnoreError(Exception ex) + { + return false; + } + } + + /// + /// Creates and returns a default Retry Policy for Schema based operations. + /// + /// An instance of class. + internal static RetryPolicy CreateDefaultSchemaCommandRetryPolicy(bool useRetry, int retriesPerPhase = RetryPolicyDefaults.DefaulSchemaRetryCount) + { + RetryPolicy policy; + + if (useRetry) + { + policy = new RetryPolicy.ExponentialDelayRetryPolicy( + RetryPolicy.SqlAzureTemporaryErrorDetectionStrategy.Instance, + retriesPerPhase, + RetryPolicyDefaults.DefaultBackoffIntervalFactor, + RetryPolicyDefaults.DefaultSchemaMinInterval, + RetryPolicyDefaults.DefaultMaxRetryInterval); + policy.FastFirstRetry = false; + } + else + { + policy = CreateNoRetryPolicy(); + } + + return policy; + } + + /// + /// Creates and returns a default Retry Policy for Schema based connection operations. + /// + /// The RetryOccured event is wired to raise an RaiseAmbientRetryMessage message for a connection retry. + /// An instance of class. + internal static RetryPolicy CreateSchemaConnectionRetryPolicy(int retriesPerPhase) + { + RetryPolicy policy = new RetryPolicy.ExponentialDelayRetryPolicy( + RetryPolicy.SqlAzureTemporaryErrorDetectionStrategy.Instance, + retriesPerPhase, + RetryPolicyDefaults.DefaultBackoffIntervalFactor, + RetryPolicyDefaults.DefaultSchemaMinInterval, + RetryPolicyDefaults.DefaultMaxRetryInterval); + policy.RetryOccurred += DataConnectionFailureRetry; + return policy; + } + + /// + /// Creates and returns a default Retry Policy for Schema based command operations. + /// + /// The RetryOccured event is wired to raise an RaiseAmbientRetryMessage message for a command retry. + /// An instance of class. + internal static RetryPolicy CreateSchemaCommandRetryPolicy(int retriesPerPhase) + { + RetryPolicy policy = new RetryPolicy.ExponentialDelayRetryPolicy( + RetryPolicy.SqlAzureTemporaryErrorDetectionStrategy.Instance, + retriesPerPhase, + RetryPolicyDefaults.DefaultBackoffIntervalFactor, + RetryPolicyDefaults.DefaultSchemaMinInterval, + RetryPolicyDefaults.DefaultMaxRetryInterval); + policy.FastFirstRetry = false; + policy.RetryOccurred += CommandFailureRetry; + return policy; + } + + /// + /// Creates and returns a Retry Policy for database creation operations. + /// + /// Errors to ignore if they occur after first retry + /// + /// The RetryOccured event is wired to raise an RaiseAmbientRetryMessage message for a command retry. + /// The IgnoreErrorOccurred event is wired to raise an RaiseAmbientIgnoreMessage message for ignore. + /// + /// An instance of class. + internal static RetryPolicy CreateDatabaseCommandRetryPolicy(params int[] ignorableErrorNumbers) + { + RetryPolicy.SqlAzureTemporaryAndIgnorableErrorDetectionStrategy errorDetectionStrategy = + new RetryPolicy.SqlAzureTemporaryAndIgnorableErrorDetectionStrategy(ignorableErrorNumbers); + + // 30, 60, 60, 60, 60 second retries + RetryPolicy policy = new RetryPolicy.ExponentialDelayRetryPolicy( + errorDetectionStrategy, + RetryPolicyDefaults.DefaultCreateDatabaseRetryCount /* maxRetryCount */, + RetryPolicyDefaults.DefaultBackoffIntervalFactor, + TimeSpan.FromSeconds(30) /* minInterval */, + TimeSpan.FromSeconds(60) /* maxInterval */); + + policy.FastFirstRetry = false; + policy.RetryOccurred += CreateDatabaseCommandFailureRetry; + policy.IgnoreErrorOccurred += CreateDatabaseCommandFailureIgnore; + + return policy; + } + + /// + /// Creates and returns an "ignoreable" command Retry Policy. + /// + /// Errors to ignore if they occur after first retry + /// + /// The RetryOccured event is wired to raise an RaiseAmbientRetryMessage message for a command retry. + /// The IgnoreErrorOccurred event is wired to raise an RaiseAmbientIgnoreMessage message for ignore. + /// + /// An instance of class. + internal static RetryPolicy CreateElementCommandRetryPolicy(params int[] ignorableErrorNumbers) + { + Debug.Assert(ignorableErrorNumbers != null); + + RetryPolicy.SqlAzureTemporaryAndIgnorableErrorDetectionStrategy errorDetectionStrategy = + new RetryPolicy.SqlAzureTemporaryAndIgnorableErrorDetectionStrategy(ignorableErrorNumbers); + + RetryPolicy policy = new RetryPolicy.ExponentialDelayRetryPolicy( + errorDetectionStrategy, + RetryPolicyDefaults.DefaulSchemaRetryCount, + RetryPolicyDefaults.DefaultBackoffIntervalFactor, + RetryPolicyDefaults.DefaultSchemaMinInterval, + RetryPolicyDefaults.DefaultMaxRetryInterval); + + policy.FastFirstRetry = false; + policy.RetryOccurred += ElementCommandFailureRetry; + policy.IgnoreErrorOccurred += ElementCommandFailureIgnore; + + return policy; + } + + /// + /// Creates and returns an "primary key violation" command Retry Policy. + /// + /// Errors to ignore if they occur after first retry + /// + /// The RetryOccured event is wired to raise an RaiseAmbientRetryMessage message for a command retry. + /// The IgnoreErrorOccurred event is wired to raise an RaiseAmbientIgnoreMessage message for ignore. + /// + /// An instance of class. + internal static RetryPolicy CreatePrimaryKeyCommandRetryPolicy() + { + RetryPolicy.SqlAzureTemporaryAndIgnorableErrorDetectionStrategy errorDetectionStrategy = + new RetryPolicy.SqlAzureTemporaryAndIgnorableErrorDetectionStrategy(SqlErrorNumbers.PrimaryKeyViolationErrorNumber); + + RetryPolicy policy = new RetryPolicy.ExponentialDelayRetryPolicy( + errorDetectionStrategy, + RetryPolicyDefaults.DefaulSchemaRetryCount, + RetryPolicyDefaults.DefaultBackoffIntervalFactor, + RetryPolicyDefaults.DefaultSchemaMinInterval, + RetryPolicyDefaults.DefaultMaxRetryInterval); + + policy.FastFirstRetry = true; + policy.RetryOccurred += CommandFailureRetry; + policy.IgnoreErrorOccurred += CommandFailureIgnore; + + return policy; + } + + /// + /// Creates a Policy that will never allow retries to occur. + /// + /// + public static RetryPolicy CreateNoRetryPolicy() + { + return new RetryPolicy.FixedDelayPolicy(TransientErrorIgnoreStrategy.Instance, 0, TimeSpan.Zero); + } + + /// + /// Creates a Policy that is optimized for data-related script update operations. + /// This is extremely error tolerant and uses a Time based delay policy that backs + /// off until some overall length of delay has occurred. It is not as long-running + /// as the ConnectionManager data transfer retry policy since that's intended for bulk upload + /// of large amounts of data, whereas this is for individual batch scripts executed by the + /// batch execution engine. + /// + /// + public static RetryPolicy CreateDataScriptUpdateRetryPolicy() + { + return new RetryPolicy.TimeBasedRetryPolicy( + RetryPolicy.DataTransferErrorDetectionStrategy.Instance, + TimeSpan.FromMinutes(7), + TimeSpan.FromMinutes(7), + 0.1, + TimeSpan.FromMilliseconds(250), + TimeSpan.FromSeconds(30), + 1.5); + } + + /// + /// Returns the default retry policy dedicated to handling exceptions with SQL connections + /// + /// The RetryPolicy policy + public static RetryPolicy CreateFastDataRetryPolicy() + { + RetryPolicy retryPolicy = new RetryPolicy.FixedDelayPolicy( + RetryPolicy.NetworkConnectivityErrorDetectionStrategy.Instance, + RetryPolicyDefaults.DefaultDataCommandRetryCount, + TimeSpan.FromMilliseconds(5)); + + retryPolicy.FastFirstRetry = true; + retryPolicy.RetryOccurred += DataConnectionFailureRetry; + return retryPolicy; + } + + /// + /// Returns the default retry policy dedicated to handling exceptions with SQL connections. + /// No logging or other message handler is attached to the policy + /// + /// The RetryPolicy policy + public static RetryPolicy CreateDefaultSchemaConnectionRetryPolicy() + { + return CreateDefaultConnectionRetryPolicy(); + } + + /// + /// Returns the default retry policy dedicated to handling exceptions with SQL connections. + /// Adds an event handler to log and notify listeners of data connection retries + /// + /// The RetryPolicy policy + public static RetryPolicy CreateDefaultDataConnectionRetryPolicy() + { + RetryPolicy retryPolicy = CreateDefaultConnectionRetryPolicy(); + retryPolicy.RetryOccurred += DataConnectionFailureRetry; + return retryPolicy; + } + + /// + /// Returns the default retry policy dedicated to handling exceptions with SQL connections + /// + /// The RetryPolicy policy + public static RetryPolicy CreateDefaultConnectionRetryPolicy() + { + // Note: No longer use Ado.net Connection Pooling and hence do not need TimeBasedRetryPolicy to + // conform to the backoff requirements in this case + RetryPolicy retryPolicy = new RetryPolicy.ExponentialDelayRetryPolicy( + RetryPolicy.NetworkConnectivityErrorDetectionStrategy.Instance, + RetryPolicyDefaults.DefaultConnectionRetryCount, + RetryPolicyDefaults.DefaultBackoffIntervalFactor, + RetryPolicyDefaults.DefaultSchemaMinInterval, + RetryPolicyDefaults.DefaultMaxRetryInterval); + + retryPolicy.FastFirstRetry = true; + return retryPolicy; + } + + /// + /// Returns the default retry policy dedicated to handling retryable conditions with data transfer SQL commands. + /// + /// The RetryPolicy policy + public static RetryPolicy CreateDefaultDataSqlCommandRetryPolicy() + { + RetryPolicy retryPolicy = new RetryPolicy.ExponentialDelayRetryPolicy( + RetryPolicy.SqlAzureTemporaryErrorDetectionStrategy.Instance, + RetryPolicyDefaults.DefaultDataCommandRetryCount, + RetryPolicyDefaults.DefaultBackoffIntervalFactor, + RetryPolicyDefaults.DefaultDataMinInterval, + RetryPolicyDefaults.DefaultMaxRetryInterval); + + retryPolicy.FastFirstRetry = true; + retryPolicy.RetryOccurred += CommandFailureRetry; + return retryPolicy; + } + + /// + /// Returns the default retry policy dedicated to handling retryable conditions with data transfer SQL commands. + /// + /// The RetryPolicy policy + public static RetryPolicy CreateDefaultDataTransferRetryPolicy() + { + RetryPolicy retryPolicy = new RetryPolicy.TimeBasedRetryPolicy( + RetryPolicy.DataTransferErrorDetectionStrategy.Instance, + TimeSpan.FromMinutes(20), + TimeSpan.FromMinutes(240), + 0.1, + TimeSpan.FromMilliseconds(250), + TimeSpan.FromMinutes(2), + 2); + + retryPolicy.FastFirstRetry = true; + retryPolicy.RetryOccurred += CommandFailureRetry; + return retryPolicy; + } + + /// + /// Returns the retry policy to handle data migration for column encryption. + /// + /// The RetryPolicy policy + public static RetryPolicy CreateColumnEncryptionTransferRetryPolicy() + { + RetryPolicy retryPolicy = new RetryPolicy.TimeBasedRetryPolicy( + RetryPolicy.DataTransferErrorDetectionStrategy.Instance, + TimeSpan.FromMinutes(5), + TimeSpan.FromMinutes(5), + 0.1, + TimeSpan.FromMilliseconds(250), + TimeSpan.FromMinutes(2), + 2); + + retryPolicy.FastFirstRetry = true; + retryPolicy.RetryOccurred += CommandFailureRetry; + return retryPolicy; + } + + private static void DataConnectionFailureRetry(RetryState retryState) + { + Logger.Write(LogLevel.Normal, string.Format(CultureInfo.InvariantCulture, + "Connection retry number {0}. Delaying {1} ms before retry. Exception: {2}", + retryState.RetryCount, + retryState.Delay.TotalMilliseconds.ToString(CultureInfo.InvariantCulture), + retryState.LastError.ToString())); + + RetryPolicyUtils.RaiseAmbientRetryMessage(retryState, SqlSchemaModelErrorCodes.ServiceActions.ConnectionRetry); + } + + private static void CommandFailureRetry(RetryState retryState, string commandKeyword) + { + Logger.Write(LogLevel.Normal, string.Format( + CultureInfo.InvariantCulture, + "{0} retry number {1}. Delaying {2} ms before retry. Exception: {3}", + commandKeyword, + retryState.RetryCount, + retryState.Delay.TotalMilliseconds.ToString(CultureInfo.InvariantCulture), + retryState.LastError.ToString())); + + RetryPolicyUtils.RaiseAmbientRetryMessage(retryState, SqlSchemaModelErrorCodes.ServiceActions.CommandRetry); + } + + private static void CommandFailureIgnore(RetryState retryState, string commandKeyword) + { + Logger.Write(LogLevel.Normal, string.Format( + CultureInfo.InvariantCulture, + "{0} retry number {1}. Ignoring failure. Exception: {2}", + commandKeyword, + retryState.RetryCount, + retryState.LastError.ToString())); + + RetryPolicyUtils.RaiseAmbientIgnoreMessage(retryState, SqlSchemaModelErrorCodes.ServiceActions.CommandRetry); + } + + private static void CommandFailureRetry(RetryState retryState) + { + CommandFailureRetry(retryState, "Command"); + } + + private static void CommandFailureIgnore(RetryState retryState) + { + CommandFailureIgnore(retryState, "Command"); + } + + private static void CreateDatabaseCommandFailureRetry(RetryState retryState) + { + CommandFailureRetry(retryState, "Database Command"); + } + + private static void CreateDatabaseCommandFailureIgnore(RetryState retryState) + { + CommandFailureIgnore(retryState, "Database Command"); + } + + private static void ElementCommandFailureRetry(RetryState retryState) + { + CommandFailureRetry(retryState, "Element Command"); + } + + private static void ElementCommandFailureIgnore(RetryState retryState) + { + CommandFailureIgnore(retryState, "Element Command"); + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicyUtils.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicyUtils.cs new file mode 100644 index 00000000..8951a75a --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicyUtils.cs @@ -0,0 +1,476 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Data.SqlClient; +using Microsoft.SqlTools.EditorServices.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + internal static class RetryPolicyUtils + { + /// + /// Approved list of transient errors that should be retryable during Network connection stages + /// + private static readonly HashSet _retryableNetworkConnectivityErrors; + /// + /// Approved list of transient errors that should be retryable on Azure + /// + private static readonly HashSet _retryableAzureErrors; + /// + /// Blocklist of non-transient errors that should stop retry during data transfer operations + /// + private static readonly HashSet _nonRetryableDataTransferErrors; + + static RetryPolicyUtils() + { + _retryableNetworkConnectivityErrors = new HashSet + { + /// A severe error occurred on the current command. The results, if any, should be discarded. + 0, + + //// DBNETLIB Error Code: 20 + //// The instance of SQL Server you attempted to connect to does not support encryption. + (int) ProcessNetLibErrorCode.EncryptionNotSupported, + + //// DBNETLIB Error Code: -2 + //// Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding. + (int)ProcessNetLibErrorCode.Timeout, + + //// SQL Error Code: 64 + //// A connection was successfully established with the server, but then an error occurred during the login process. + //// (provider: TCP Provider, error: 0 - The specified network name is no longer available.) + 64, + + //// SQL Error Code: 233 + //// The client was unable to establish a connection because of an error during connection initialization process before login. + //// Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy + //// to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server. + //// (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) + 233, + + //// SQL Error Code: 10053 + //// A transport-level error has occurred when receiving results from the server. + //// An established connection was aborted by the software in your host machine. + 10053, + + //// SQL Error Code: 10054 + //// A transport-level error has occurred when sending the request to the server. + //// (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) + 10054, + + //// SQL Error Code: 10060 + //// A network-related or instance-specific error occurred while establishing a connection to SQL Server. + //// The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server + //// is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed + //// because the connected party did not properly respond after a period of time, or established connection failed + //// because connected host has failed to respond.)"} + 10060, + + // SQL Error Code: 11001 + // A network-related or instance-specific error occurred while establishing a connection to SQL Server. + // The server was not found or was not accessible. Verify that the instance name is correct and that SQL + // Server is configured to allow remote connections. (provider: TCP Provider, error: 0 - No such host is known.) + 11001, + + //// SQL Error Code: 40613 + //// Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer + //// support, and provide them the session tracing ID of ZZZZZ. + 40613, + }; + + _retryableAzureErrors = new HashSet + { + //// SQL Error Code: 40 + //// Could not open a connection to SQL Server + //// (provider: Named Pipes Provider, error: 40 Could not open a connection to SQL Server) + 40, + + //// SQL Error Code: 121 + //// A transport-level error has occurred when receiving results from the server. + //// (provider: TCP Provider, error: 0 - The semaphore timeout period has expired.) + 121, + + //// SQL Error Code: 913 (noticed intermittently on SNAP runs with connected unit tests) + //// Could not find database ID %d. Database may not be activated yet or may be in transition. Reissue the query once the database is available. + //// If you do not think this error is due to a database that is transitioning its state and this error continues to occur, contact your primary support provider. + //// Please have available for review the Microsoft SQL Server error log and any additional information relevant to the circumstances when the error occurred. + 913, + + //// SQL Error Code: 1205 + //// Transaction (Process ID %d) was deadlocked on %.*ls resources with another process and has been chosen as the deadlock victim. Rerun the transaction. + 1205, + + //// SQL Error Code: 40501 + //// The service is currently busy. Retry the request after 10 seconds. Code: (reason code to be decoded). + RetryPolicy.ThrottlingReason.ThrottlingErrorNumber, + + //// SQL Error Code: 10928 + //// Resource ID: %d. The %s limit for the database is %d and has been reached. + 10928, + + //// SQL Error Code: 10929 + //// Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d and the current usage for the database is %d. + //// However, the server is currently too busy to support requests greater than %d for this database. + 10929, + + //// SQL Error Code: 40143 + //// The service has encountered an error processing your request. Please try again. + 40143, + + //// SQL Error Code: 40197 + //// The service has encountered an error processing your request. Please try again. + 40197, + + //// Sql Error Code: 40549 (not supposed to be used anymore as of Q2 2011) + //// Session is terminated because you have a long-running transaction. Try shortening your transaction. + 40549, + + //// Sql Error Code: 40550 (not supposed to be used anymore as of Q2 2011) + //// The session has been terminated because it has acquired too many locks. Try reading or modifying fewer rows in a single transaction. + 40550, + + //// Sql Error Code: 40551 (not supposed to be used anymore as of Q2 2011) + //// The session has been terminated because of excessive TEMPDB usage. Try modifying your query to reduce the temporary table space usage. + 40551, + + //// Sql Error Code: 40552 (not supposed to be used anymore as of Q2 2011) + //// The session has been terminated because of excessive transaction log space usage. Try modifying fewer rows in a single transaction. + 40552, + + //// Sql Error Code: 40553 (not supposed to be used anymore as of Q2 2011) + //// The session has been terminated because of excessive memory usage. Try modifying your query to process fewer rows. + 40553, + + //// SQL Error Code: 40627 + //// Operation on server YYY and database XXX is in progress. Please wait a few minutes before trying again. + 40627, + + //// SQL Error Code: 40671 (DB CRUD) + //// Unable to '%.*ls' '%.*ls' on server '%.*ls'. Please retry the connection later. + 40671, + + //// SQL Error Code: 40676 (DB CRUD) + //// '%.*ls' request was received but may not be processed completely at this time, + //// please query the sys.dm_operation_status table in the master database for status. + 40676, + + //// SQL Error Code: 45133 + //// A connection failed while the operation was still in progress, and the outcome of the operation is unknown. + 45133, + }; + + foreach(int errorNum in _retryableNetworkConnectivityErrors) + { + _retryableAzureErrors.Add(errorNum); + } + + _nonRetryableDataTransferErrors = new HashSet + { + //// Syntax error + 156, + + //// Cannot insert duplicate key row in object '%.*ls' with unique index '%.*ls'. The duplicate key value is %ls. + 2601, + + //// Violation of %ls constraint '%.*ls'. Cannot insert duplicate key in object '%.*ls'. The duplicate key value is %ls. + 2627, + + //// Cannot find index '%.*ls'. + 2727, + + //// SqlClr stack error + 6522, + + //// Divide by zero error encountered. + 8134, + + //// Could not repair this error. + 8922, + + //// Bug 1110540: This error means the table is corrupted due to hardware failure, so we do not want to retry. + //// Table error: Object ID %d. The text, ntext, or image node at page %S_PGID, slot %d, text ID %I64d is referenced by page %S_PGID, slot %d, but was not seen in the scan. + 8965, + + //// The query processor is unable to produce a plan because the clustered index is disabled. + 8655, + + //// The query processor is unable to produce a plan because table is unavailable because the heap is corrupted + 8674, + + //// SqlClr permission / load error. + //// Example Message: An error occurred in the Microsoft .NET Framework while trying to load assembly + 10314, + + //// '%ls' is not supported in this version of SQL Server. + 40514, + + //// The database 'XYZ' has reached its size quota. Partition or delete data, drop indexes, or consult the documentation for possible resolutions + 40544, + }; + } + + public static bool IsRetryableNetworkConnectivityError(int errorNumber) + { + return _retryableNetworkConnectivityErrors.Contains(errorNumber); + } + + public static bool IsRetryableAzureError(int errorNumber) + { + return _retryableAzureErrors.Contains(errorNumber) || _retryableNetworkConnectivityErrors.Contains(errorNumber); + } + + public static bool IsNonRetryableDataTransferError(int errorNumber) + { + return _nonRetryableDataTransferErrors.Contains(errorNumber); + } + + public static void AppendThrottlingDataIfIsThrottlingError(SqlException sqlException, SqlError error) + { + //// SQL Error Code: 40501 + //// The service is currently busy. Retry the request after 10 seconds. Code: (reason code to be decoded). + if(error.Number == RetryPolicy.ThrottlingReason.ThrottlingErrorNumber) + { + // Decode the reason code from the error message to determine the grounds for throttling. + var condition = RetryPolicy.ThrottlingReason.FromError(error); + + // Attach the decoded values as additional attributes to the original SQL exception. + sqlException.Data[condition.ThrottlingMode.GetType().Name] = condition.ThrottlingMode.ToString(); + sqlException.Data[condition.GetType().Name] = condition; + } + } + /// + /// Calculates the length of time to delay a retry based on the number of retries up to this point. + /// As the number of retries increases, the timeout increases exponentially based on the intervalFactor. + /// Uses default values for the intervalFactor (), minInterval + /// () and maxInterval () + /// + /// Total number of retries including the current retry + /// TimeSpan defining the length of time to delay + internal static TimeSpan CalcExponentialRetryDelayWithSchemaDefaults(int currentRetryCount) + { + return CalcExponentialRetryDelay(currentRetryCount, + RetryPolicyDefaults.DefaultBackoffIntervalFactor, + RetryPolicyDefaults.DefaultSchemaMinInterval, + RetryPolicyDefaults.DefaultMaxRetryInterval); + } + + /// + /// Calculates the length of time to delay a retry based on the number of retries up to this point. + /// As the number of retries increases, the timeout increases exponentially based on the intervalFactor. + /// A very large retry count can cause huge delay, so the maxInterval is used to cap delay time at a sensible + /// upper bound + /// + /// Total number of retries including the current retry + /// Controls the speed at which the delay increases - the retryCount is raised to this power as + /// part of the function + /// Minimum interval between retries. The basis for all backoff calculations + /// Maximum interval between retries. Backoff will not take longer than this period. + /// TimeSpan defining the length of time to delay + internal static TimeSpan CalcExponentialRetryDelay(int currentRetryCount, double intervalFactor, TimeSpan minInterval, TimeSpan maxInterval) + { + try + { + return checked(TimeSpan.FromMilliseconds( + Math.Max( + Math.Min( + Math.Pow(intervalFactor, currentRetryCount - 1) * minInterval.TotalMilliseconds, + maxInterval.TotalMilliseconds + ), + minInterval.TotalMilliseconds) + )); + } + catch (OverflowException) + { + // If numbers are too large, could conceivably overflow the double. + // Since the maxInterval is the largest TimeSpan expected, can safely return this here + return maxInterval; + } + } + + internal static void RaiseAmbientRetryMessage(RetryState retryState, int errorCode) + { + Action retryMsgHandler = AmbientSettings.ConnectionRetryMessageHandler; + if (retryMsgHandler != null) + { + string msg = SqlServerRetryError.FormatRetryMessage( + retryState.RetryCount, + retryState.Delay, + retryState.LastError); + + retryMsgHandler(new SqlServerRetryError( + msg, + retryState.LastError, + retryState.RetryCount, + errorCode, + ErrorSeverity.Warning)); + } + } + + internal static void RaiseAmbientIgnoreMessage(RetryState retryState, int errorCode) + { + Action retryMsgHandler = AmbientSettings.ConnectionRetryMessageHandler; + if (retryMsgHandler != null) + { + string msg = SqlServerRetryError.FormatIgnoreMessage( + retryState.RetryCount, + retryState.LastError); + + retryMsgHandler(new SqlServerRetryError( + msg, + retryState.LastError, + retryState.RetryCount, + errorCode, + ErrorSeverity.Warning)); + } + } + + /// + /// Traces the Schema retry information before raising the retry message + /// + /// + /// + /// + internal static void RaiseSchemaAmbientRetryMessage(RetryState retryState, int errorCode, Guid azureSessionId) + { + Logger.Write(LogLevel.Warning, string.Format( + "Retry occurred: session: {0}; attempt - {1}; delay - {2}; exception - \"{3}\"", + azureSessionId, + retryState.RetryCount, + retryState.Delay, + retryState.LastError + )); + + RaiseAmbientRetryMessage(retryState, errorCode); + } + + #region ProcessNetLibErrorCode enumeration + + /// + /// Error codes reported by the DBNETLIB module. + /// + internal enum ProcessNetLibErrorCode + { + /// + /// Zero bytes were returned + /// + ZeroBytes = -3, + + /// + /// Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding. + /// + Timeout = -2, + + /// + /// An unknown net lib error + /// + Unknown = -1, + + /// + /// Out of memory + /// + InsufficientMemory = 1, + + /// + /// User or machine level access denied + /// + AccessDenied = 2, + + /// + /// Connection was already busy processing another request + /// + ConnectionBusy = 3, + + /// + /// The connection was broken without a proper disconnect + /// + ConnectionBroken = 4, + + /// + /// The connection has reached a limit + /// + ConnectionLimit = 5, + + /// + /// Name resolution failed for the given server name + /// + ServerNotFound = 6, + + /// + /// Network transport could not be found + /// + NetworkNotFound = 7, + + /// + /// A resource required could not be allocated + /// + InsufficientResources = 8, + + /// + /// Network stack denied the request as too busy + /// + NetworkBusy = 9, + + /// + /// Unable to access the requested network + /// + NetworkAccessDenied = 10, + + /// + /// Internal error + /// + GeneralError = 11, + + /// + /// The network mode was set incorrectly + /// + IncorrectMode = 12, + + /// + /// The given name was not found + /// + NameNotFound = 13, + + /// + /// Connection was invalid + /// + InvalidConnection = 14, + + /// + /// A read or write error occurred + /// + ReadWriteError = 15, + + /// + /// Unable to allocate an additional handle + /// + TooManyHandles = 16, + + /// + /// The server reported an error + /// + ServerError = 17, + + /// + /// SSL failed + /// + SSLError = 18, + + /// + /// Encryption failed with an error + /// + EncryptionError = 19, + + /// + /// Remote endpoint does not support encryption + /// + EncryptionNotSupported = 20 + } + + #endregion + + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryState.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryState.cs new file mode 100644 index 00000000..f79ade69 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryState.cs @@ -0,0 +1,86 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + internal class RetryState + { + private int _retryCount = 0; + private TimeSpan _delay = TimeSpan.Zero; + private Exception _lastError = null; + private bool _isDelayDisabled = false; + + /// + /// Gets or sets the current retry attempt count. + /// + public int RetryCount + { + get + { + return _retryCount; + } + set + { + _retryCount = value; + } + } + + /// + /// Gets or sets the delay indicating how long the current thread will be suspended for before the next iteration will be invoked. + /// + public TimeSpan Delay + { + get + { + return _delay; + } + set + { + _delay = value; + } + } + + /// + /// Gets or sets the exception which caused the retry conditions to occur. + /// + public Exception LastError + { + get + { + return _lastError; + } + set + { + _lastError = value; + } + } + + /// + /// Gets or sets a value indicating whether we should ignore delay in order to be able to execute our tests faster + /// + /// Intended for test use ONLY + internal bool IsDelayDisabled + { + get + { + return _isDelayDisabled; + } + set + { + _isDelayDisabled = value; + } + } + + public virtual void Reset() + { + this.IsDelayDisabled = false; + this.RetryCount = 0; + this.Delay = TimeSpan.Zero; + this.LastError = null; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/SqlConnectionHelperScripts.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/SqlConnectionHelperScripts.cs new file mode 100644 index 00000000..421acba8 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/SqlConnectionHelperScripts.cs @@ -0,0 +1,51 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + static class SqlConnectionHelperScripts + { + public const string EngineEdition = "SELECT SERVERPROPERTY('EngineEdition'), SERVERPROPERTY('productversion'), SERVERPROPERTY ('productlevel'), SERVERPROPERTY ('edition'), (SELECT CASE WHEN EXISTS (SELECT TOP 1 1 from [sys].[all_columns] WITH (NOLOCK) WHERE name = N'xml_index_type' AND OBJECT_ID(N'sys.xml_indexes') = object_id) THEN 1 ELSE 0 END AS SXI_PRESENT)"; + public const string EngineEditionWithLock = "SELECT SERVERPROPERTY('EngineEdition'), SERVERPROPERTY('productversion'), SERVERPROPERTY ('productlevel'), SERVERPROPERTY ('edition'), (SELECT CASE WHEN EXISTS (SELECT TOP 1 1 from [sys].[all_columns] WHERE name = N'xml_index_type' AND OBJECT_ID(N'sys.xml_indexes') = object_id) THEN 1 ELSE 0 END AS SXI_PRESENT)"; + + public const string CheckDatabaseReadonly = @"EXEC sp_dboption '{0}', 'read only'"; + + public const string GetDatabaseFilePathAndName = @" +DECLARE @filepath nvarchar(260), + @rc int + +EXEC master.dbo.xp_instance_regread N'HKEY_LOCAL_MACHINE',N'Software\Microsoft\MSSQLServer\MSSQLServer',N'DefaultData', @filepath output, 'no_output' + +IF ((@filepath IS NOT NULL) AND (CHARINDEX(N'\', @filepath, len(@filepath)) = 0)) + SELECT @filepath = @filepath + N'\' + +IF (@filepath IS NULL) + SELECT @filepath = [sdf].[physical_name] + FROM [master].[sys].[database_files] AS [sdf] + WHERE [file_id] = 1 + +SELECT @filepath AS FilePath +"; + + public const string GetDatabaseLogPathAndName = @" +DECLARE @filepath nvarchar(260), + @rc int + +EXEC master.dbo.xp_instance_regread N'HKEY_LOCAL_MACHINE',N'Software\Microsoft\MSSQLServer\MSSQLServer',N'DefaultLog', @filepath output, 'no_output' + +IF ((@filepath IS NOT NULL) AND (CHARINDEX(N'\', @filepath, len(@filepath)) = 0)) + SELECT @filepath = @filepath + N'\' + +IF (@filepath IS NULL) + SELECT @filepath = [ldf].[physical_name] + FROM [master].[sys].[database_files] AS [ldf] + WHERE [file_id] = 2 + +SELECT @filepath AS FilePath +"; + + public const string GetOsVersion = @"SELECT OSVersion = RIGHT(@@version, LEN(@@version)- 3 -charindex (' ON ', @@version))"; + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/SqlErrorNumbers.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/SqlErrorNumbers.cs new file mode 100644 index 00000000..0b48f0e1 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/SqlErrorNumbers.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + /// + /// Constants for SQL Error numbers + /// + internal static class SqlErrorNumbers + { + // Database XYZ already exists. Choose a different database name. + internal const int DatabaseAlreadyExistsErrorNumber = 1801; + + // Cannot drop the database 'x', because it does not exist or you do not have permission. + internal const int DatabaseAlreadyDroppedErrorNumber = 3701; + + // Database 'x' was created\altered successfully, but some properties could not be displayed. + internal const int DatabaseCrudMetadataUpdateErrorNumber = 45166; + + // Violation of PRIMARY KEY constraint 'x'. + // Cannot insert duplicate key in object 'y'. The duplicate key value is (z). + internal const int PrimaryKeyViolationErrorNumber = 2627; + + // There is already an object named 'x' in the database. + internal const int ObjectAlreadyExistsErrorNumber = 2714; + + // Cannot drop the object 'x', because it does not exist or you do not have permission. + internal const int ObjectAlreadyDroppedErrorNumber = 3701; + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/SqlSchemaModelErrorCodes.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/SqlSchemaModelErrorCodes.cs new file mode 100644 index 00000000..4d7a65b7 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/SqlSchemaModelErrorCodes.cs @@ -0,0 +1,465 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + internal static class SqlSchemaModelErrorCodes + { + private const int ParserErrorCodeStartIndex = 46000; + private const int ParserErrorCodeEndIndex = 46499; + + public static bool IsParseErrorCode(int errorCode) + { + return + (errorCode >= ParserErrorCodeStartIndex) && + (errorCode <= ParserErrorCodeEndIndex); + } + + public static bool IsInterpretationErrorCode(int errorCode) + { + return + (errorCode >= Interpretation.InterpretationBaseCode) && + (errorCode <= Interpretation.InterpretationEndCode); + } + + public static bool IsStatementFilterError(int errorCode) + { + return + (errorCode > StatementFilter.StatementFilterBaseCode) && + (errorCode <= StatementFilter.StatementFilterMaxErrorCode); + } + + public static class StatementFilter + { + public const int StatementFilterBaseCode = 70000; + + public const int UnrecognizedStatement = StatementFilterBaseCode + 1; + public const int ServerObject = StatementFilterBaseCode + 2; + public const int AtMostTwoPartName = StatementFilterBaseCode + 3; + public const int AlterTableAddColumn = StatementFilterBaseCode + 4; + public const int ConstraintAll = StatementFilterBaseCode + 5; + public const int TriggerAll = StatementFilterBaseCode + 6; + public const int CreateSchemaWithoutName = StatementFilterBaseCode + 7; + public const int CreateSchemaElements = StatementFilterBaseCode + 8; + public const int AlterAssembly = StatementFilterBaseCode + 9; + public const int CreateStoplist = StatementFilterBaseCode + 10; + public const int UnsupportedPermission = StatementFilterBaseCode + 11; + public const int TopLevelExecuteWithResultSets = StatementFilterBaseCode + 12; + public const int AlterTableAddConstraint = StatementFilterBaseCode + 13; + public const int DatabaseOnlyObjectInServerProject = StatementFilterBaseCode + 14; + public const int UnsupportedBySqlAzure = StatementFilterBaseCode + 15; + public const int UnsupportedSecurityObjectKind = StatementFilterBaseCode + 16; + public const int StatementNotSupportedForCurrentRelease = StatementFilterBaseCode + 17; + public const int ServerPermissionsNotAllowed = StatementFilterBaseCode + 18; + public const int DeprecatedSyntax = StatementFilterBaseCode + 19; + public const int SetRemoteData = StatementFilterBaseCode + 20; + public const int StatementFilterMaxErrorCode = StatementFilterBaseCode + 499; + } + + public static class Interpretation + { + public const int InterpretationBaseCode = 70500; + + public const int InvalidTopLevelStatement = InterpretationBaseCode + 1; + public const int InvalidAssemblySource = InterpretationBaseCode + 2; + public const int InvalidDatabaseName = InterpretationBaseCode + 3; + public const int OnlyTwoPartNameAllowed = InterpretationBaseCode + 4; + public const int SecurityObjectCannotBeNull = InterpretationBaseCode + 5; + public const int UnknownPermission = InterpretationBaseCode + 6; + public const int UnsupportedAll = InterpretationBaseCode + 7; + public const int InvalidColumnList = InterpretationBaseCode + 8; + public const int ColumnsAreNotAllowed = InterpretationBaseCode + 9; + public const int InvalidDataType = InterpretationBaseCode + 10; + public const int InvalidObjectName = InterpretationBaseCode + 11; + public const int InvalidObjectChildName = InterpretationBaseCode + 12; + public const int NoGlobalTemporarySymmetricKey = InterpretationBaseCode + 13; + public const int NoGlobalTemporarySymmetricKey_Warning = InterpretationBaseCode + 14; + public const int NameCannotBeNull = InterpretationBaseCode + 15; + public const int NameCannotBeNull_Warning = InterpretationBaseCode + 16; + public const int InvalidLoginName = InterpretationBaseCode + 17; + public const int InvalidLoginName_Warning = InterpretationBaseCode + 18; + public const int MoreAliasesThanColumns = InterpretationBaseCode + 19; + public const int FewerAliasesThanColumns = InterpretationBaseCode + 20; + public const int InvalidTimestampReturnType = InterpretationBaseCode + 21; + public const int VariableParameterAtTopLevelStatement = InterpretationBaseCode + 22; + public const int CannotCreateTempTable = InterpretationBaseCode + 23; + public const int MultipleNullabilityConstraintError = InterpretationBaseCode + 24; + public const int MultipleNullabilityConstraintWarning = InterpretationBaseCode + 25; + public const int ColumnIsntAllowedForAssemblySource = InterpretationBaseCode + 26; + public const int InvalidUserName = InterpretationBaseCode + 27; + public const int InvalidWindowsLogin = InterpretationBaseCode + 28; + public const int InvalidWindowsLogin_Warning = InterpretationBaseCode + 29; + public const int CannotHaveUsingForPrimaryXmlIndex = InterpretationBaseCode + 30; + public const int UsingIsRequiredForSecondaryXmlIndex = InterpretationBaseCode + 31; + public const int XmlIndexTypeIsRequiredForSecondaryXmlIndex = InterpretationBaseCode + 32; + public const int UnsupportedAlterCryptographicProvider = InterpretationBaseCode + 33; + public const int HttpForSoapOnly = InterpretationBaseCode + 34; + public const int UnknownEventTypeOrGroup = InterpretationBaseCode + 35; + public const int CannotAddLogFileToFilegroup = InterpretationBaseCode + 36; + public const int BuiltInTypeExpected = InterpretationBaseCode + 37; + public const int MissingArgument = InterpretationBaseCode + 38; + public const int InvalidArgument = InterpretationBaseCode + 39; + public const int IncompleteBoundingBoxCoordinates = InterpretationBaseCode + 40; + public const int XMaxLessThanXMin = InterpretationBaseCode + 41; + public const int YMaxLessThanYMin = InterpretationBaseCode + 42; + public const int InvalidCoordinate = InterpretationBaseCode + 43; + public const int InvalidValue = InterpretationBaseCode + 44; + public const int InvalidIdentityValue = InterpretationBaseCode + 45; + public const int InvalidPriorityLevel = InterpretationBaseCode + 46; + public const int TriggerIsNotForEvent = InterpretationBaseCode + 47; + public const int SyntaxError = InterpretationBaseCode + 48; + public const int UnsupportedPintable = InterpretationBaseCode + 49; + public const int DuplicateEventType = InterpretationBaseCode + 50; + public const int ClearAndBasicAreNotAllowed = InterpretationBaseCode + 51; + public const int AssemblyCorruptErrorCode = InterpretationBaseCode + 57; + public const int DynamicQuery = InterpretationBaseCode + 58; + public const int OnlyLcidAllowed = InterpretationBaseCode + 59; + public const int WildCardNotAllowed = InterpretationBaseCode + 60; + public const int CannotBindSchema = InterpretationBaseCode + 61; + public const int TableTypeNotAllowFunctionCall = InterpretationBaseCode + 62; + public const int ColumnNotAllowed = InterpretationBaseCode + 63; + public const int OwnerRequiredForEndpoint = InterpretationBaseCode + 64; + public const int PartitionNumberMustBeInteger = InterpretationBaseCode + 65; + public const int DuplicatedPartitionNumber = InterpretationBaseCode + 66; + public const int FromPartitionGreaterThanToPartition = InterpretationBaseCode + 67; + public const int CannotSpecifyPartitionNumber = InterpretationBaseCode + 68; + public const int MissingColumnNameError = InterpretationBaseCode + 69; + public const int MissingColumnNameWarning = InterpretationBaseCode + 70; + public const int UnknownTableSourceError = InterpretationBaseCode + 71; + public const int UnknownTableSourceWarning = InterpretationBaseCode + 72; + public const int TooManyPartsForCteOrAliasError = InterpretationBaseCode + 73; + public const int TooManyPartsForCteOrAliasWarning = InterpretationBaseCode + 74; + public const int ServerAuditInvalidQueueDelayValue = InterpretationBaseCode + 75; + public const int WrongEventType = InterpretationBaseCode + 76; + public const int CantCreateUddtFromXmlError = InterpretationBaseCode + 77; + public const int CantCreateUddtFromXmlWarning = InterpretationBaseCode + 78; + public const int CantCreateUddtFromUddtError = InterpretationBaseCode + 79; + public const int CantCreateUddtFromUddtWarning = InterpretationBaseCode + 80; + public const int ForReplicationIsNotSupported = InterpretationBaseCode + 81; + public const int TooLongIdentifier = InterpretationBaseCode + 82; + public const int InvalidLanguageTerm = InterpretationBaseCode + 83; + public const int InvalidParameterOrOption = InterpretationBaseCode + 85; + public const int TableLevelForeignKeyWithNoColumnsError = InterpretationBaseCode + 86; + public const int TableLevelForeignKeyWithNoColumnsWarning = InterpretationBaseCode + 87; + public const int ConstraintEnforcementIsIgnored = InterpretationBaseCode + 88; + public const int DeprecatedBackupOption = InterpretationBaseCode + 89; + public const int UndeclaredVariableParameter = InterpretationBaseCode + 90; + public const int UnsupportedAlgorithm = InterpretationBaseCode + 91; + public const int InvalidLanguageNameOrAliasWarning = InterpretationBaseCode + 92; + public const int UnsupportedRevoke = InterpretationBaseCode + 93; + public const int InvalidPermissionTypeAgainstObject = InterpretationBaseCode + 94; + public const int InvalidPermissionObjectType = InterpretationBaseCode + 95; + public const int CannotDetermineSecurableFromPermission = InterpretationBaseCode + 96; + public const int InvalidColumnListForSecurableType = InterpretationBaseCode + 97; + public const int InvalidUserDefaultLanguage = InterpretationBaseCode + 98; + public const int CannotSpecifyGridParameterForAutoGridSpatialIndex = InterpretationBaseCode + 99; + public const int UnsupportedSpatialTessellationScheme = InterpretationBaseCode + 100; + public const int CannotSpecifyBoundingBoxForGeography = InterpretationBaseCode + 101; + public const int InvalidSearchPropertyId = InterpretationBaseCode + 102; + public const int OnlineSpatialIndex = InterpretationBaseCode + 103; + public const int SqlCmdVariableInObjectName = InterpretationBaseCode + 104; + public const int SubqueriesNotAllowed = InterpretationBaseCode + 105; + public const int ArgumentReplaceNotSupported = InterpretationBaseCode + 106; + public const int DuplicateArgument = InterpretationBaseCode + 107; + public const int UnsupportedNoPopulationChangeTrackingOption = InterpretationBaseCode + 108; + public const int UnsupportedResourceManagerLocationProperty = InterpretationBaseCode + 109; + public const int RequiredExternalDataSourceLocationPropertyMissing = InterpretationBaseCode + 110; + public const int UnsupportedSerdeMethodProperty = InterpretationBaseCode + 111; + public const int UnsupportedFormatOptionsProperty = InterpretationBaseCode + 112; + public const int RequiredSerdeMethodPropertyMissing = InterpretationBaseCode + 113; + public const int TableLevelIndexWithNoColumnsError = InterpretationBaseCode + 114; + public const int TableLevelIndexWithNoColumnsWarning = InterpretationBaseCode + 115; + public const int InvalidIndexOption = InterpretationBaseCode + 116; + public const int TypeAndSIDMustBeUsedTogether = InterpretationBaseCode + 117; + public const int TypeCannotBeUsedWithLoginOption = InterpretationBaseCode + 118; + public const int InvalidUserType = InterpretationBaseCode + 119; + public const int InvalidUserSid = InterpretationBaseCode + 120; + public const int InvalidPartitionFunctionDataType = InterpretationBaseCode + 121; + public const int RequiredExternalTableLocationPropertyMissing = InterpretationBaseCode + 122; + public const int UnsupportedRejectSampleValueProperty = InterpretationBaseCode + 123; + public const int RequiredExternalDataSourceDatabasePropertyMissing = InterpretationBaseCode + 124; + public const int RequiredExternalDataSourceShardMapNamePropertyMissing = InterpretationBaseCode + 125; + public const int InvalidPropertyForExternalDataSourceType = InterpretationBaseCode + 126; + public const int UnsupportedExternalDataSourceTypeInCurrentPlatform = InterpretationBaseCode + 127; + public const int UnsupportedExternalTableProperty = InterpretationBaseCode + 128; + public const int MaskingFunctionIsEmpty = InterpretationBaseCode + 129; + public const int InvalidMaskingFunctionFormat = InterpretationBaseCode + 130; + public const int CannotCreateAlwaysEncryptedObject = InterpretationBaseCode + 131; + public const int ExternalTableSchemaOrObjectNameMissing = InterpretationBaseCode + 132; + public const int CannotCreateTemporalTableWithoutHistoryTableName = InterpretationBaseCode + 133; + public const int TemporalPeriodColumnMustNotBeNullable = InterpretationBaseCode + 134; + public const int InterpretationEndCode = InterpretationBaseCode + 499; + } + + public static class ModelBuilder + { + private const int ModelBuilderBaseCode = 71000; + + public const int CannotFindMainElement = ModelBuilderBaseCode + 1; + public const int CannotFindColumnSourceGrantForColumnRevoke = ModelBuilderBaseCode + 2; + public const int AssemblyReferencesNotSupported = ModelBuilderBaseCode + 3; + public const int NoSourceForColumn = ModelBuilderBaseCode + 5; + public const int MoreThanOneStatementPerBatch = ModelBuilderBaseCode + 6; + public const int MaximumSizeExceeded = ModelBuilderBaseCode + 7; + } + + public static class Validation + { + private const int ValidationBaseCode = 71500; + + public const int AllReferencesMustBeResolved = ValidationBaseCode + 1; + public const int AllReferencesMustBeResolved_Warning = ValidationBaseCode + 2; + public const int AssemblyVisibilityRule = ValidationBaseCode + 3; + public const int BreakContinueOnlyInWhile = ValidationBaseCode + 4; + public const int ClrObjectAssemblyReference_InvalidAssembly = ValidationBaseCode + 5; + public const int ClrObjectAssemblyReference = ValidationBaseCode + 6; + public const int ColumnUserDefinedTableType = ValidationBaseCode + 7; + public const int DuplicateName = ValidationBaseCode + 8; + public const int DuplicateName_Warning = ValidationBaseCode + 9; + public const int DuplicateVariableParameterName_TemporaryTable = ValidationBaseCode + 10; + public const int DuplicateVariableParameterName_Variable = ValidationBaseCode + 11; + public const int EndPointRule_DATABASE_MIRRORING = ValidationBaseCode + 12; + public const int EndPointRule_SERVICE_BROKER = ValidationBaseCode + 13; + public const int ForeignKeyColumnTypeNumberMustMatch_NumberOfColumns = ValidationBaseCode + 14; + public const int ForeignKeyColumnTypeNumberMustMatch_TypeMismatch = ValidationBaseCode + 15; + public const int ForeignKeyReferencePKUnique = ValidationBaseCode + 16; + public const int FullTextIndexColumn = ValidationBaseCode + 17; + public const int IdentityColumnValidation_InvalidType = ValidationBaseCode + 18; + public const int IdentityColumnValidation_MoreThanOneIdentity = ValidationBaseCode + 19; + public const int InsertIntoIdentityColumn = ValidationBaseCode + 20; + public const int MatchingSignatureNotFoundInAssembly = ValidationBaseCode + 21; + public const int MatchingTypeNotFoundInAssembly = ValidationBaseCode + 22; + public const int MaxColumnInIndexKey = ValidationBaseCode + 25; + public const int MaxColumnInTable_1024Columns = ValidationBaseCode + 26; + public const int MultiFullTextIndexOnTable = ValidationBaseCode + 28; + public const int NonNullPrimaryKey_NonNullSimpleColumn = ValidationBaseCode + 29; + public const int NonNullPrimaryKey_NotPersistedComputedColumn = ValidationBaseCode + 30; + public const int OneClusteredIndex = ValidationBaseCode + 31; + public const int OneMasterKey = ValidationBaseCode + 32; + public const int OnePrimaryKey = ValidationBaseCode + 33; + public const int PrimaryXMLIndexClustered = ValidationBaseCode + 34; + public const int SelectAssignRetrieval = ValidationBaseCode + 35; + public const int SubroutineParameterReadOnly_NonUDTTReadOnly = ValidationBaseCode + 36; + public const int SubroutineParameterReadOnly_UDTTReadOnly = ValidationBaseCode + 37; + public const int UsingXMLIndex = ValidationBaseCode + 38; + public const int VardecimalOptionRule = ValidationBaseCode + 39; + public const int WildCardExpansion = ValidationBaseCode + 40; + public const int WildCardExpansion_Warning = ValidationBaseCode + 41; + public const int XMLIndexOnlyXMLTypeColumn = ValidationBaseCode + 42; + public const int TableVariablePrefix = ValidationBaseCode + 44; + public const int FileStream_FILESTREAMON = ValidationBaseCode + 45; + public const int FileStream_ROWGUIDCOLUMN = ValidationBaseCode + 46; + public const int MaxColumnInTable100_Columns = ValidationBaseCode + 47; + public const int XMLIndexOnlyXMLTypeColumn_SparseColumnSet = ValidationBaseCode + 48; + public const int ClrObjectAssemblyReference_ParameterTypeMismatch = ValidationBaseCode + 50; + public const int OneDefaultConstraintPerColumn = ValidationBaseCode + 51; + public const int PermissionStatementValidation_DuplicatePermissionOnSecurable = ValidationBaseCode + 52; + public const int PermissionStatementValidation_ConflictingPermissionsOnSecurable = ValidationBaseCode + 53; + public const int PermissionStatementValidation_ConflictingColumnStatements = ValidationBaseCode + 54; + public const int PermissionOnObjectSecurableValidation_InvalidPermissionForObject = ValidationBaseCode + 55; + public const int SequenceValueValidation_ValueOutOfRange = ValidationBaseCode + 56; + public const int SequenceValueValidation_InvalidDataType = ValidationBaseCode + 57; + public const int MismatchedName_Warning = ValidationBaseCode + 58; + public const int DifferentNameCasing_Warning = ValidationBaseCode + 59; + public const int OneClusteredIndexAzure = ValidationBaseCode + 60; + public const int AllExternalReferencesMustBeResolved = ValidationBaseCode + 61; + public const int AllExternalReferencesMustBeResolved_Warning = ValidationBaseCode + 62; + public const int ExternalObjectWildCardExpansion_Warning = ValidationBaseCode + 63; + public const int UnsupportedElementForDataPackage = ValidationBaseCode + 64; + public const int InvalidFileStreamOptions = ValidationBaseCode + 65; + public const int StorageShouldNotSetOnDifferentInstance = ValidationBaseCode + 66; + public const int TableShouldNotHaveStorage = ValidationBaseCode + 67; + public static int MemoryOptimizedObjectsValidation_NonMemoryOptimizedTableCannotBeAccessed = ValidationBaseCode + 68; + public static int MemoryOptimizedObjectsValidation_SyntaxNotSupportedOnHekatonElement = ValidationBaseCode + 69; + public static int MemoryOptimizedObjectsValidation_ValidatePrimaryKeyForSchemaAndDataTables = ValidationBaseCode + 70; + public static int MemoryOptimizedObjectsValidation_ValidatePrimaryKeyForSchemaOnlyTables = ValidationBaseCode + 71; + public static int MemoryOptimizedObjectsValidation_OnlyNotNullableColumnsOnIndexes = ValidationBaseCode + 72; + public static int MemoryOptimizedObjectsValidation_HashIndexesOnlyOnMemoryOptimizedObjects = ValidationBaseCode + 73; + public static int MemoryOptimizedObjectsValidation_OptionOnlyForHashIndexes = ValidationBaseCode + 74; + public static int IncrementalStatisticsValidation_FilterNotSupported = ValidationBaseCode + 75; + public static int IncrementalStatisticsValidation_ViewNotSupported = ValidationBaseCode + 76; + public static int IncrementalStatisticsValidation_IndexNotPartitionAligned = ValidationBaseCode + 77; + public static int AzureV12SurfaceAreaValidation = ValidationBaseCode + 78; + public static int DuplicatedTargetObjectReferencesInSecurityPolicy = ValidationBaseCode + 79; + public static int MultipleSecurityPoliciesOnTargetObject = ValidationBaseCode + 80; + public static int ExportedRowsMayBeIncomplete = ValidationBaseCode + 81; + public static int ExportedRowsMayContainSomeMaskedData = ValidationBaseCode + 82; + public const int EncryptedColumnValidation_EncryptedPrimaryKey = ValidationBaseCode + 83; + public const int EncryptedColumnValidation_EncryptedUniqueColumn = ValidationBaseCode + 84; + public const int EncryptedColumnValidation_EncryptedCheckConstraint = ValidationBaseCode + 85; + public const int EncryptedColumnValidation_PrimaryKeyForeignKeyEncryptionMismatch = ValidationBaseCode + 86; + public const int EncryptedColumnValidation_UnsupportedDataType = ValidationBaseCode + 87; + public const int MemoryOptimizedObjectsValidation_UnSupportedOption = ValidationBaseCode + 88; + public const int MasterKeyExistsForCredential = ValidationBaseCode + 89; + public const int MemoryOptimizedObjectsValidation_InvalidForeignKeyRelationship = ValidationBaseCode + 90; + public const int MemoryOptimizedObjectsValidation_UnsupportedForeignKeyReference = ValidationBaseCode + 91; + public const int EncryptedColumnValidation_RowGuidColumn = ValidationBaseCode + 92; + public const int EncryptedColumnValidation_EncryptedClusteredIndex = ValidationBaseCode + 93; + public const int EncryptedColumnValidation_EncryptedNonClusteredIndex = ValidationBaseCode + 94; + public const int EncryptedColumnValidation_DependentComputedColumn = ValidationBaseCode + 95; + public const int EncryptedColumnValidation_EncryptedFullTextColumn = ValidationBaseCode + 96; + public const int EncryptedColumnValidation_EncryptedSparseColumnSet = ValidationBaseCode + 97; + public const int EncryptedColumnValidation_EncryptedStatisticsColumn = ValidationBaseCode + 98; + public const int EncryptedColumnValidation_EncryptedPartitionColumn = ValidationBaseCode + 99; + public const int EncryptedColumnValidation_PrimaryKeyChangeTrackingColumn = ValidationBaseCode + 100; + public const int EncryptedColumnValidation_ChangeDataCaptureOn = ValidationBaseCode + 101; + public const int EncryptedColumnValidation_FilestreamColumn = ValidationBaseCode + 102; + public const int EncryptedColumnValidation_MemoryOptimizedTable = ValidationBaseCode + 103; + public const int EncryptedColumnValidation_MaskedEncryptedColumn = ValidationBaseCode + 104; + public const int EncryptedColumnValidation_EncryptedIdentityColumn = ValidationBaseCode + 105; + public const int EncryptedColumnValidation_EncryptedDefaultConstraint = ValidationBaseCode + 106; + public const int TemporalValidation_InvalidPeriodSpecification = ValidationBaseCode + 107; + public const int TemporalValidation_MultipleCurrentTables = ValidationBaseCode + 108; + public const int TemporalValidation_SchemaMismatch = ValidationBaseCode + 109; + public const int TemporalValidation_ComputedColumns = ValidationBaseCode + 110; + public const int TemporalValidation_NoAlwaysEncryptedCols = ValidationBaseCode + 111; + public static int IndexesOnExternalTable = ValidationBaseCode + 112; + public static int TriggersOnExternalTable = ValidationBaseCode + 113; + public const int StretchValidation_ExportBlocked = ValidationBaseCode + 114; + public const int StretchValidation_ImportBlocked = ValidationBaseCode + 115; + public const int DeploymentBlocked = ValidationBaseCode + 116; + public const int NoBlockPredicatesTargetingViews = ValidationBaseCode + 117; + public const int SchemaBindingOnSecurityPoliciesValidation = ValidationBaseCode + 118; + public const int SecurityPredicateTargetObjectValidation = ValidationBaseCode + 119; + public const int TemporalValidation_SchemaMismatch_ColumnCount = ValidationBaseCode + 120; + public const int AkvValidation_AuthenticationFailed = ValidationBaseCode + 121; + public const int TemporalValidation_PrimaryKey = ValidationBaseCode + 122; + } + + public static class SqlMSBuild + { + private const int MSBuildBaseCode = 72000; + + public const int FileDoesNotExist = MSBuildBaseCode + 1; + public const int UnknownDeployError = MSBuildBaseCode + 2; + public const int InvalidProperty = MSBuildBaseCode + 3; + public const int CollationError = MSBuildBaseCode + 4; + public const int InvalidSqlClrDefinition = MSBuildBaseCode + 5; + public const int SQL_PrePostFatalParserError = MSBuildBaseCode + 6; + public const int SQL_PrePostSyntaxCheckError = MSBuildBaseCode + 7; + public const int SQL_PrePostVariableError = MSBuildBaseCode + 8; + public const int SQL_CycleError = MSBuildBaseCode + 9; + public const int SQL_NoConnectionStringNoServerVerification = MSBuildBaseCode + 10; + public const int SQL_VardecimalMismatch = MSBuildBaseCode + 11; + public const int SQL_NoAlterFileSystemObject = MSBuildBaseCode + 12; + public const int SQL_SqlCmdVariableOverrideError = MSBuildBaseCode + 13; + public const int SQL_BatchError = MSBuildBaseCode + 14; + public const int SQL_DataLossError = MSBuildBaseCode + 15; + public const int SQL_ExecutionError = MSBuildBaseCode + 16; + public const int SQL_UncheckedConstraint = MSBuildBaseCode + 17; + public const int SQL_UnableToImportElements = MSBuildBaseCode + 18; + public const int SQL_TargetReadOnlyError = MSBuildBaseCode + 19; + public const int SQL_UnsupportedCompatibilityMode = MSBuildBaseCode + 20; + public const int SQL_IncompatibleDSPVersions = MSBuildBaseCode + 21; + public const int SQL_CouldNotLoadSymbols = MSBuildBaseCode + 22; + public const int SQL_ContainmentlMismatch = MSBuildBaseCode + 23; + public const int SQL_PrePostExpectedNoTSqlError = MSBuildBaseCode + 24; + public const int ReferenceErrorCode = MSBuildBaseCode + 25; + public const int FileError = MSBuildBaseCode + 26; + public const int MissingReference = MSBuildBaseCode + 27; + public const int SerializationError = MSBuildBaseCode + 28; + public const int DeploymentContributorVerificationError = MSBuildBaseCode + 29; + public const int Deployment_PossibleRuntimeError = MSBuildBaseCode + 30; + public const int Deployment_BlockingDependency = MSBuildBaseCode + 31; + public const int Deployment_TargetObjectLoss = MSBuildBaseCode + 32; + public const int Deployment_MissingDependency = MSBuildBaseCode + 33; + public const int Deployment_PossibleDataLoss = MSBuildBaseCode + 34; + public const int Deployment_NotSupportedOperation = MSBuildBaseCode + 35; + public const int Deployment_Information = MSBuildBaseCode + 36; + public const int Deployment_UnsupportedDSP = MSBuildBaseCode + 37; + public const int Deployment_SkipManagementScopedChange = MSBuildBaseCode + 38; + public const int StaticCodeAnalysis_GeneralException = MSBuildBaseCode + 39; + public const int StaticCodeAnalysis_ResultsFileIOException = MSBuildBaseCode + 40; + public const int StaticCodeAnalysis_FailToCreateTaskHost = MSBuildBaseCode + 41; + public const int StaticCodeAnalysis_InvalidDataSchemaModel = MSBuildBaseCode + 42; + public const int StaticCodeAnalysis_InvalidElement = MSBuildBaseCode + 43; + public const int Deployment_NoClusteredIndex = MSBuildBaseCode + 44; + public const int Deployment_DetailedScriptExecutionError = MSBuildBaseCode + 45; + } + + public static class Refactoring + { + private const int RefactoringBaseCode = 72500; + + public const int FailedToLoadFile = RefactoringBaseCode + 1; + } + + /// + /// These codes are used to message specific actions for extract and deployment operations. + /// The primary consumer of these codes is the Import/Export service. + /// + public static class ServiceActions + { + public const int ServiceActionsBaseCode = 73000; + public const int ServiceActionsMaxCode = 73000 + 0xFF; + + // Note: These codes are defined so that the lower 3 bits indicate one of three + // event stages: Started (0x01), Done/Complete (0x02), Done/Failed (0x04) + public const int DeployInitializeStart = ServiceActionsBaseCode + 0x01; + public const int DeployInitializeSuccess = ServiceActionsBaseCode + 0x02; + public const int DeployInitializeFailure = ServiceActionsBaseCode + 0x04; + + public const int DeployAnalysisStart = ServiceActionsBaseCode + 0x11; + public const int DeployAnalysisSuccess = ServiceActionsBaseCode + 0x12; + public const int DeployAnalysisFailure = ServiceActionsBaseCode + 0x14; + + public const int DeployExecuteScriptStart = ServiceActionsBaseCode + 0x21; + public const int DeployExecuteScriptSuccess = ServiceActionsBaseCode + 0x22; + public const int DeployExecuteScriptFailure = ServiceActionsBaseCode + 0x24; + + public const int DataImportStart = ServiceActionsBaseCode + 0x41; + public const int DataImportSuccess = ServiceActionsBaseCode + 0x42; + public const int DataImportFailure = ServiceActionsBaseCode + 0x44; + + public const int ExtractSchemaStart = ServiceActionsBaseCode + 0x61; + public const int ExtractSchemaSuccess = ServiceActionsBaseCode + 0x62; + public const int ExtractSchemaFailure = ServiceActionsBaseCode + 0x64; + + public const int ExportVerifyStart = ServiceActionsBaseCode + 0x71; + public const int ExportVerifySuccess = ServiceActionsBaseCode + 0x72; + public const int ExportVerifyFailure = ServiceActionsBaseCode + 0x74; + + public const int ExportDataStart = ServiceActionsBaseCode + 0x81; + public const int ExportDataSuccess = ServiceActionsBaseCode + 0x82; + public const int ExportDataFailure = ServiceActionsBaseCode + 0x84; + + public const int EnableIndexesDataStart = ServiceActionsBaseCode + 0xb1; + public const int EnableIndexesDataSuccess = ServiceActionsBaseCode + 0xb2; + public const int EnableIndexesDataFailure = ServiceActionsBaseCode + 0xb4; + + public const int DisableIndexesDataStart = ServiceActionsBaseCode + 0xc1; + public const int DisableIndexesDataSuccess = ServiceActionsBaseCode + 0xc2; + public const int DisableIndexesDataFailure = ServiceActionsBaseCode + 0xc4; + + public const int EnableIndexDataStart = ServiceActionsBaseCode + 0xd1; + public const int EnableIndexDataSuccess = ServiceActionsBaseCode + 0xd2; + public const int EnableIndexDataFailure = ServiceActionsBaseCode + 0xd4; + + public const int DisableIndexDataStart = ServiceActionsBaseCode + 0xe1; + public const int DisableIndexDataSuccess = ServiceActionsBaseCode + 0xe2; + public const int DisableIndexDataFailure = ServiceActionsBaseCode + 0xe4; + + public const int ColumnEncryptionDataMigrationStart = ServiceActionsBaseCode + 0xf1; + public const int ColumnEncryptionDataMigrationSuccess = ServiceActionsBaseCode + 0xf2; + public const int ColumnEncryptionDataMigrationFailure = ServiceActionsBaseCode + 0xf4; + + // These codes do not set the lower 3 bits + public const int ConnectionRetry = ServiceActionsBaseCode + 0x90; + public const int CommandRetry = ServiceActionsBaseCode + 0x91; + public const int GeneralProgress = ServiceActionsBaseCode + 0x92; + public const int TypeFidelityLoss = ServiceActionsBaseCode + 0x93; + public const int TableProgress = ServiceActionsBaseCode + 0x94; + public const int ImportBlocked = ServiceActionsBaseCode + 0x95; + public const int DataPrecisionLoss = ServiceActionsBaseCode + 0x96; + public const int DataRowCount = ServiceActionsBaseCode + 0x98; + + public const int DataException = ServiceActionsBaseCode + 0xA0; + public const int LogEntry = ServiceActionsBaseCode + 0xA1; + public const int GeneralInfo = ServiceActionsBaseCode + 0xA2; + + + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/SqlServerError.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/SqlServerError.cs new file mode 100644 index 00000000..c58b112d --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/SqlServerError.cs @@ -0,0 +1,74 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + /// + /// Represents an error produced by SQL Server database schema provider + /// + [Serializable] + internal class SqlServerError : DataSchemaError + { + private const string SqlServerPrefix = "SQL"; + private const string DefaultHelpKeyword = "vs.teamsystem.datatools.DefaultErrorMessageHelp"; + + public SqlServerError(string message, string document, ErrorSeverity severity) + : this(message, null, document, 0, 0, Constants.UndefinedErrorCode, severity) + { + } + + public SqlServerError(string message, string document, int errorCode, ErrorSeverity severity) + : this(message, null, document, 0, 0, errorCode, severity) + { + } + + public SqlServerError(Exception exception, string document, int errorCode, ErrorSeverity severity) + : this(exception, document, 0, 0, errorCode, severity) + { + } + + public SqlServerError(string message, string document, int line, int column, ErrorSeverity severity) + : this(message, null, document, line, column, Constants.UndefinedErrorCode, severity) + { + } + + public SqlServerError( + Exception exception, + string document, + int line, + int column, + int errorCode, + ErrorSeverity severity) : + this(exception.Message, exception, document, line, column, errorCode, severity) + { + } + + public SqlServerError( + string message, + string document, + int line, + int column, + int errorCode, + ErrorSeverity severity) : + this(message, null, document, line, column, errorCode, severity) + { + } + + public SqlServerError( + string message, + Exception exception, + string document, + int line, + int column, + int errorCode, + ErrorSeverity severity) : + base(message, exception, document, line, column, SqlServerPrefix, errorCode, severity) + { + this.HelpKeyword = DefaultHelpKeyword; + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/SqlServerRetryError.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/SqlServerRetryError.cs new file mode 100644 index 00000000..35ec6aa1 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/SqlServerRetryError.cs @@ -0,0 +1,54 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Globalization; + +namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection +{ + /// + /// Captures extended information about a specific error and a retry + /// + internal class SqlServerRetryError : SqlServerError + { + private int _retryCount; + private int _errorCode; + + public SqlServerRetryError(string message, Exception ex, int retryCount, int errorCode, ErrorSeverity severity) + : base(ex, message, errorCode, severity) + { + _retryCount = retryCount; + _errorCode = errorCode; + } + + public int RetryCount + { + get { return _retryCount; } + } + + public static string FormatRetryMessage(int retryCount, TimeSpan delay, Exception transientException) + { + string message = string.Format( + CultureInfo.CurrentCulture, + Resources.RetryOnException, + retryCount, + delay.TotalMilliseconds.ToString(CultureInfo.CurrentCulture), + transientException.ToString()); + + return message; + } + + public static string FormatIgnoreMessage(int retryCount, Exception exception) + { + string message = string.Format( + CultureInfo.CurrentCulture, + Resources.IgnoreOnException, + retryCount, + exception.ToString()); + + return message; + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/SqlConnectionFactory.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/SqlConnectionFactory.cs index cffb690d..4cafb290 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Connection/SqlConnectionFactory.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/SqlConnectionFactory.cs @@ -4,7 +4,7 @@ // using System.Data.Common; -using System.Data.SqlClient; +using Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection; namespace Microsoft.SqlTools.ServiceLayer.Connection { @@ -20,7 +20,9 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection /// public DbConnection CreateSqlConnection(string connectionString) { - return new SqlConnection(connectionString); + RetryPolicy connectionRetryPolicy = RetryPolicyFactory.CreateDefaultConnectionRetryPolicy(); + RetryPolicy commandRetryPolicy = RetryPolicyFactory.CreateDefaultConnectionRetryPolicy(); + return new ReliableSqlConnection(connectionString, connectionRetryPolicy, commandRetryPolicy); } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs index b4da32aa..d43840a2 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/LanguageService.cs @@ -5,13 +5,13 @@ using System; using System.Collections.Generic; -using System.Data.SqlClient; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.SqlTools.EditorServices.Utility; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; +using Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection; using Microsoft.SqlTools.ServiceLayer.Hosting; using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts; @@ -418,14 +418,14 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices scriptInfo.BuildingMetadataEvent.WaitOne(LanguageService.OnConnectionWaitTimeout); scriptInfo.BuildingMetadataEvent.Reset(); - var sqlConn = info.SqlConnection as SqlConnection; + var sqlConn = info.SqlConnection as ReliableSqlConnection; if (sqlConn != null) { - ServerConnection serverConn = new ServerConnection(sqlConn); + ServerConnection serverConn = new ServerConnection(sqlConn.GetUnderlyingConnection()); scriptInfo.MetadataDisplayInfoProvider = new MetadataDisplayInfoProvider(); scriptInfo.MetadataProvider = SmoMetadataProvider.CreateConnectedProvider(serverConn); scriptInfo.Binder = BinderProvider.CreateBinder(scriptInfo.MetadataProvider); - scriptInfo.ServerConnection = new ServerConnection(sqlConn); + scriptInfo.ServerConnection = new ServerConnection(sqlConn.GetUnderlyingConnection()); this.ScriptParseInfoMap[info.OwnerUri] = scriptInfo; } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs index 69250afe..98e857ca 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Batch.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.SqlTools.EditorServices.Utility; +using Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; using Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage; @@ -137,10 +138,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution { // Register the message listener to *this instance* of the batch // Note: This is being done to associate messages with batches - SqlConnection sqlConn = conn as SqlConnection; + ReliableSqlConnection sqlConn = conn as ReliableSqlConnection; if (sqlConn != null) { - sqlConn.InfoMessage += StoreDbMessage; + sqlConn.GetUnderlyingConnection().InfoMessage += StoreDbMessage; } // Create a command that we'll use for executing the query diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs index 2da2a15d..74193d8b 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Query.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.SqlServer.Management.SqlParser.Parser; using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; using Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage; using Microsoft.SqlTools.ServiceLayer.SqlContext; @@ -192,11 +193,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution { await conn.OpenAsync(); - SqlConnection sqlConn = conn as SqlConnection; + ReliableSqlConnection sqlConn = conn as ReliableSqlConnection; if (sqlConn != null) { // Subscribe to database informational messages - sqlConn.InfoMessage += OnInfoMessage; + sqlConn.GetUnderlyingConnection().InfoMessage += OnInfoMessage; } // We need these to execute synchronously, otherwise the user will be very unhappy diff --git a/src/Microsoft.SqlTools.ServiceLayer/Utility/Validate.cs b/src/Microsoft.SqlTools.ServiceLayer/Utility/Validate.cs index c788e67b..880716c1 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Utility/Validate.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Utility/Validate.cs @@ -124,13 +124,28 @@ namespace Microsoft.SqlTools.EditorServices.Utility } } + /// + /// Throws ArgumentException if the value is null or an empty string. + /// + /// The name of the parameter being validated. + /// The value of the parameter being validated. + public static void IsNotNullOrEmptyString(string parameterName, string valueToCheck) + { + if (string.IsNullOrEmpty(valueToCheck)) + { + throw new ArgumentException( + "Parameter contains a null, empty, or whitespace string.", + parameterName); + } + } + /// /// Throws ArgumentException if the value is null, an empty string, /// or a string containing only whitespace. /// /// The name of the parameter being validated. /// The value of the parameter being validated. - public static void IsNotNullOrEmptyString(string parameterName, string valueToCheck) + public static void IsNotNullOrWhitespaceString(string parameterName, string valueToCheck) { if (string.IsNullOrWhiteSpace(valueToCheck)) { diff --git a/src/Microsoft.SqlTools.ServiceLayer/Workspace/Workspace.cs b/src/Microsoft.SqlTools.ServiceLayer/Workspace/Workspace.cs index 3099a3d5..070337a7 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Workspace/Workspace.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Workspace/Workspace.cs @@ -61,7 +61,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace /// public ScriptFile GetFile(string filePath) { - Validate.IsNotNullOrEmptyString("filePath", filePath); + Validate.IsNotNullOrWhitespaceString("filePath", filePath); // Resolve the full file path string resolvedFilePath = this.ResolveFilePath(filePath); @@ -153,7 +153,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Workspace /// public ScriptFile GetFileBuffer(string filePath, string initialBuffer) { - Validate.IsNotNullOrEmptyString("filePath", filePath); + Validate.IsNotNullOrWhitespaceString("filePath", filePath); // Resolve the full file path string resolvedFilePath = this.ResolveFilePath(filePath); diff --git a/src/Microsoft.SqlTools.ServiceLayer/project.json b/src/Microsoft.SqlTools.ServiceLayer/project.json index 02e977aa..25466abd 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/project.json +++ b/src/Microsoft.SqlTools.ServiceLayer/project.json @@ -14,6 +14,7 @@ "System.Security.SecureString": "4.0.0", "System.Collections.Specialized": "4.0.1", "System.ComponentModel.TypeConverter": "4.1.0", + "System.Diagnostics.Contracts": "4.0.0", "System.Diagnostics.TraceSource": "4.0.0", "NETStandard.Library": "1.6.0", "Microsoft.NETCore.Runtime.CoreCLR": "1.0.2", diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test/Connection/ConnectionServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.Test/Connection/ConnectionServiceTests.cs index e027e58f..07c202cb 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test/Connection/ConnectionServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.Test/Connection/ConnectionServiceTests.cs @@ -256,7 +256,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Connection // check that the connection was successful Assert.NotEmpty(connectionResult.ConnectionId); - Assert.Null(connectionResult.Messages); } ///