Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Add retry logic to connection open and command execution #307

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion src/Microsoft.Data.SqlClient.sln
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 15
# Visual Studio 15
VisualStudioVersion = 15.0.27004.2010
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Data.SqlClient", "Microsoft.Data.SqlClient\netfx\src\Microsoft.Data.SqlClient.csproj", "{407890AC-9876-4FEF-A6F1-F36A876BAADE}"
Expand Down Expand Up @@ -48,6 +48,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Data.SqlClient.Al
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "add-ons", "add-ons", "{C9726AED-D6A3-4AAC-BA04-92DD1F079594}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RetryLogic", "Microsoft.Data.SqlClient\tests\ManualTests\RetryLogic\RetryLogic\RetryLogic.csproj", "{9825BDE1-D15A-44BA-BF22-C44427E5CF22}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -591,6 +593,42 @@ Global
{9073ABEF-92E0-4702-BB23-2C99CEF9BDD7}.Release|x64.Build.0 = Release|Any CPU
{9073ABEF-92E0-4702-BB23-2C99CEF9BDD7}.Release|x86.ActiveCfg = Release|Any CPU
{9073ABEF-92E0-4702-BB23-2C99CEF9BDD7}.Release|x86.Build.0 = Release|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.Debug|x64.ActiveCfg = Debug|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.Debug|x64.Build.0 = Debug|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.Debug|x86.ActiveCfg = Debug|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.Debug|x86.Build.0 = Debug|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.net46-Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.net46-Debug|Any CPU.Build.0 = Debug|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.net46-Debug|x64.ActiveCfg = Debug|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.net46-Debug|x64.Build.0 = Debug|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.net46-Debug|x86.ActiveCfg = Debug|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.net46-Debug|x86.Build.0 = Debug|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.net46-Release|Any CPU.ActiveCfg = Release|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.net46-Release|Any CPU.Build.0 = Release|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.net46-Release|x64.ActiveCfg = Release|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.net46-Release|x64.Build.0 = Release|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.net46-Release|x86.ActiveCfg = Release|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.net46-Release|x86.Build.0 = Release|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.netcoreapp2.1-Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.netcoreapp2.1-Debug|Any CPU.Build.0 = Debug|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.netcoreapp2.1-Debug|x64.ActiveCfg = Debug|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.netcoreapp2.1-Debug|x64.Build.0 = Debug|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.netcoreapp2.1-Debug|x86.ActiveCfg = Debug|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.netcoreapp2.1-Debug|x86.Build.0 = Debug|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.netcoreapp2.1-Release|Any CPU.ActiveCfg = Release|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.netcoreapp2.1-Release|Any CPU.Build.0 = Release|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.netcoreapp2.1-Release|x64.ActiveCfg = Release|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.netcoreapp2.1-Release|x64.Build.0 = Release|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.netcoreapp2.1-Release|x86.ActiveCfg = Release|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.netcoreapp2.1-Release|x86.Build.0 = Release|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.Release|Any CPU.Build.0 = Release|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.Release|x64.ActiveCfg = Release|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.Release|x64.Build.0 = Release|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.Release|x86.ActiveCfg = Release|Any CPU
{9825BDE1-D15A-44BA-BF22-C44427E5CF22}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -613,6 +651,7 @@ Global
{771F3F1E-7A68-4A9D-ADA8-A24F1D5BE71D} = {3FDD425C-FE01-4B56-863E-1FCDD0677CF5}
{412BCCC8-19F6-489A-B594-E9A506816155} = {771F3F1E-7A68-4A9D-ADA8-A24F1D5BE71D}
{9073ABEF-92E0-4702-BB23-2C99CEF9BDD7} = {C9726AED-D6A3-4AAC-BA04-92DD1F079594}
{9825BDE1-D15A-44BA-BF22-C44427E5CF22} = {0CC4817A-12F3-4357-912C-09315FAAD008}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {01D48116-37A2-4D33-B9EC-94793C702431}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,7 @@ private static NameValuePair ParseInternal(Dictionary<string, string> parsetable
string realkeyname = null != synonyms ?
(synonyms.TryGetValue(keyname, out synonym) ? synonym : null) :
keyname;

if (!IsKeyNameValid(realkeyname))
{
throw ADP.KeywordNotSupported(keyname);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,19 @@
<Reference Include="System.Data" />
</ItemGroup>
<ItemGroup>
<Compile Include="Microsoft\Data\SqlClient\Reliability\SqlRetryLimitExceededException.cs" />
<Compile Include="Microsoft\Data\SqlClient\Reliability\SqlAsyncExecution.cs" />
<Compile Include="Microsoft\Data\SqlClient\Reliability\ExponentialBackoff.cs" />
<Compile Include="Microsoft\Data\SqlClient\Reliability\FixedInterval.cs" />
<Compile Include="Microsoft\Data\SqlClient\Reliability\Incremental.cs" />
<Compile Include="Microsoft\Data\SqlClient\Reliability\ITransientErrorDetectionStrategy.cs" />
<Compile Include="Microsoft\Data\SqlClient\Reliability\Resources.Designer.cs" />
<Compile Include="Microsoft\Data\SqlClient\Reliability\SqlRetryingEventArgs.cs" />
<Compile Include="Microsoft\Data\SqlClient\Reliability\SqlRetryPolicy.cs" />
<Compile Include="Microsoft\Data\SqlClient\Reliability\SqlRetryPolicy.Generic.cs" />
<Compile Include="Microsoft\Data\SqlClient\Reliability\SqlRetryStrategy.cs" />
<Compile Include="Microsoft\Data\SqlClient\Reliability\SqlTransientErrorDetectionStrategy.cs" />
<Compile Include="Microsoft\Data\SqlClient\Reliability\SqlThrottlingCondition.cs" />
<Compile Include="Resources\SR.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,44 @@ internal static Exception InvalidConnectRetryIntervalValue()
return Argument(System.SRHelper.GetString(SR.SQLCR_InvalidConnectRetryIntervalValue));
}

// Retry Logic
internal static Exception InvalidRetryStrategy()
{
return Argument(System.SRHelper.GetString(SR.SQLCR_InvalidRetryStrategy));
}
internal static Exception InvalidRetryCount()
{
return Argument(System.SRHelper.GetString(SR.SQLCR_InvalidRetryCount));
}
internal static Exception InvalidRetryInterval()
{
return Argument(System.SRHelper.GetString(SR.SQLCR_InvalidRetryInterval));
}
internal static Exception InvalidRetryIncrement()
{
return Argument(System.SRHelper.GetString(SR.SQLCR_InvalidRetryIncrement));
}
internal static Exception InvalidRetryMinBackoff()
{
return Argument(System.SRHelper.GetString(SR.SQLCR_InvalidRetryMinBackoff));
}
internal static Exception InvalidRetryMaxBackoff()
{
return Argument(System.SRHelper.GetString(SR.SQLCR_InvalidRetryMaxBackoff));
}
internal static Exception InvalidRetryDeltaBackoff()
{
return Argument(System.SRHelper.GetString(SR.SQLCR_InvalidRetryDeltaBackoff));
}
internal static Exception InvalidRetryLogFilePath()
{
return Argument(System.SRHelper.GetString(SR.SQLCR_InvalidRetryLogFilePath));
}
internal static Exception InvalidRetriableErrors()
{
return Argument(System.SRHelper.GetString(SR.SQLCR_InvalidRetriableErrors));
}

//
// : DbDataReader
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,23 @@ internal static partial class DbConnectionStringDefaults
internal const string TransactionBinding = "Implicit Unbind";
internal const int ConnectRetryCount = 1;
internal const int ConnectRetryInterval = 10;

// Retry Logic
internal const string RetryStrategy = "None";
internal const int RetryCount = 3;

internal const int RetryIntervalSec = 10;
internal const int RetryIncrementSec = 10;

internal const int RetryMinBackoffSec = 10;
internal const int RetryMaxBackoffSec = 10;
internal const int RetryDeltaBackoffSec = 10;

internal const bool RetryFastFirst = false;

internal const string RetryLogFilePath = "";
internal const string RetriableErrors = "";

internal static readonly SqlAuthenticationMethod Authentication = SqlAuthenticationMethod.NotSpecified;
internal const SqlConnectionColumnEncryptionSetting ColumnEncryptionSetting = SqlConnectionColumnEncryptionSetting.Disabled;
internal const string EnclaveAttestationUrl = "";
Expand Down Expand Up @@ -679,6 +696,19 @@ internal static partial class DbConnectionStringKeywords
internal const string WorkstationID = "Workstation ID";
internal const string ConnectRetryCount = "ConnectRetryCount";
internal const string ConnectRetryInterval = "ConnectRetryInterval";

// Retry Logic
internal const string RetryStrategy = "RetryStrategy";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are so many connection string options. And they dont seem descriptive enough for anyone to look at the keyword and get an idea of where these might apply. Are these meant to be for connections only ? Should they be prefixed with Connect?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree there are many, but effectively different retry strategies have different attributes. For example, for "FixedInterval" you only need a "RetryInterval" attribute, while for "Incremental" you need a "RetryIncrement" as well, and for "ExponentialBackoff" you need to specify min, max and delta. On purpose i was not prefixing them with Connect to avoid any confusion with existing Connection Resiliency parameters.

internal const string RetryCount = "RetryCount";
internal const string RetryInterval = "RetryInterval";
internal const string RetryIncrement = "RetryIncrement";
internal const string RetryMinBackoff = "RetryMinBackoff";
internal const string RetryMaxBackoff = "RetryMaxBackoff";
internal const string RetryDeltaBackoff = "RetryDeltaBackoff";
internal const string RetryFastFirst = "RetryFastFirst";
internal const string RetryLogFilePath = "RetryLogFilePath";
internal const string RetriableErrors = "RetriableErrors";

internal const string Authentication = "Authentication";
internal const string ColumnEncryptionSetting = "Column Encryption Setting";
internal const string EnclaveAttestationUrl = "Enclave Attestation Url";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;

namespace Microsoft.Data.SqlClient.Reliability
{

/// <summary>
/// A retry strategy with backoff parameters for calculating the exponential delay between retries.
/// </summary>
public class ExponentialBackoff : SqlRetryStrategy
{
private readonly int retryCount;
private readonly TimeSpan minBackoff;
private readonly TimeSpan maxBackoff;
private readonly TimeSpan deltaBackoff;

/// <summary>
/// Initializes a new instance of the <see cref="ExponentialBackoff"/> class.
/// </summary>
public ExponentialBackoff()
: this(DefaultClientRetryCount, DefaultMinBackoff, DefaultMaxBackoff, DefaultDeltaBackoff)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="ExponentialBackoff"/> class with the specified name and retry settings.
/// </summary>
/// <param name="retryCount">The maximum number of retry attempts.</param>
/// <param name="minBackoff">The minimum backoff time</param>
/// <param name="maxBackoff">The maximum backoff time.</param>
/// <param name="deltaBackoff">The value that will be used to calculate a random delta in the exponential delay between retries.</param>
public ExponentialBackoff(int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff)
: this(retryCount, minBackoff, maxBackoff, deltaBackoff, DefaultFirstFastRetry)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="ExponentialBackoff"/> class with the specified name, retry settings, and fast retry option.
/// </summary>
/// <param name="retryCount">The maximum number of retry attempts.</param>
/// <param name="minBackoff">The minimum backoff time</param>
/// <param name="maxBackoff">The maximum backoff time.</param>
/// <param name="deltaBackoff">The value that will be used to calculate a random delta in the exponential delay between retries.</param>
/// <param name="firstFastRetry">true to immediately retry in the first attempt; otherwise, false. The subsequent retries will remain subject to the configured retry interval.</param>
public ExponentialBackoff(int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff, bool firstFastRetry)
: base(firstFastRetry)
{
this.retryCount = retryCount;
this.minBackoff = minBackoff;
this.maxBackoff = maxBackoff;
this.deltaBackoff = deltaBackoff;
}

/// <summary>
/// Returns the corresponding ShouldRetry delegate.
/// </summary>
/// <returns>The ShouldRetry delegate.</returns>
public override ShouldRetry GetShouldRetry()
{
return delegate(int currentRetryCount, Exception lastException, out TimeSpan retryInterval)
{
if (currentRetryCount < this.retryCount)
{
var random = new Random();

var delta = (int)((Math.Pow(2.0, currentRetryCount) - 1.0) * random.Next((int)(this.deltaBackoff.TotalMilliseconds * 0.8), (int)(this.deltaBackoff.TotalMilliseconds * 1.2)));
var interval = (int)Math.Min(checked(this.minBackoff.TotalMilliseconds + delta), this.maxBackoff.TotalMilliseconds);

retryInterval = TimeSpan.FromMilliseconds(interval);

return true;
}

retryInterval = TimeSpan.Zero;
return false;
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;

namespace Microsoft.Data.SqlClient.Reliability
{

/// <summary>
/// Represents a retry strategy with a specified number of retry attempts and a default, fixed time interval between retries.
/// </summary>
public class FixedInterval : SqlRetryStrategy
{
private readonly int retryCount;
private readonly TimeSpan retryInterval;

/// <summary>
/// Initializes a new instance of the <see cref="FixedInterval"/> class.
/// </summary>
public FixedInterval()
: this(DefaultClientRetryCount)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="FixedInterval"/> class with the specified number of retry attempts.
/// </summary>
/// <param name="retryCount">The number of retry attempts.</param>
public FixedInterval(int retryCount)
: this(retryCount, DefaultRetryInterval)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="FixedInterval"/> class with the specified number of retry attempts, time interval, and retry strategy.
/// </summary>
/// <param name="retryCount">The number of retry attempts.</param>
/// <param name="retryInterval">The time interval between retries.</param>
public FixedInterval(int retryCount, TimeSpan retryInterval)
: this(retryCount, retryInterval, DefaultFirstFastRetry)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="FixedInterval"/> class with the specified number of retry attempts, time interval, retry strategy, and fast start option.
/// </summary>
/// <param name="retryCount">The number of retry attempts.</param>
/// <param name="retryInterval">The time interval between retries.</param>
/// <param name="firstFastRetry">true to immediately retry in the first attempt; otherwise, false. The subsequent retries will remain subject to the configured retry interval.</param>
public FixedInterval(int retryCount, TimeSpan retryInterval, bool firstFastRetry)
: base(firstFastRetry)
{
this.retryCount = retryCount;
this.retryInterval = retryInterval;
}

/// <summary>
/// Returns the corresponding ShouldRetry delegate.
/// </summary>
/// <returns>The ShouldRetry delegate.</returns>
public override ShouldRetry GetShouldRetry()
{
if (this.retryCount == 0)
{
return delegate(int currentRetryCount, Exception lastException, out TimeSpan interval)
{
interval = TimeSpan.Zero;
return false;
};
}

return delegate(int currentRetryCount, Exception lastException, out TimeSpan interval)
{
if (currentRetryCount < this.retryCount)
{
interval = this.retryInterval;
return true;
}

interval = TimeSpan.Zero;
return false;
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;

namespace Microsoft.Data.SqlClient.Reliability
{
/// <summary>
/// Defines an interface that must be implemented by custom components responsible for detecting specific transient conditions.
/// </summary>
public interface ITransientErrorDetectionStrategy
{
/// <summary>
/// Determines whether the specified exception represents a transient failure that can be compensated by a retry.
/// </summary>
/// <param name="ex">The exception object to be verified.</param>
/// <returns>true if the specified exception is considered as transient; otherwise, false.</returns>
bool IsTransient(Exception ex);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we anticipate anything apart from SqlException to be retryable ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As of now, we're retrying on selected SqlException and TimeoutException types.


/// <summary>
/// Determines whether the specified exception represents a transient failure that can be compensated by a retry.
/// </summary>
/// <returns>true if the specified exception is considered as transient; otherwise, false.</returns>
List<int> RetriableErrors
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List<int> should not occur in a public interface. Likely, some immutable value should be returned. Also, it seems that the idea that errors are identified by a set of integers is specific to SqlClient. Other providers might identify errors though other types such as long, enum, string or large ranges of integers.

{
get;
}
}
}
Loading