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

Configurable Retry Logic - Preview 1 #693

Merged
merged 48 commits into from
Mar 4, 2021
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
d86ed3a
Configurable Retry Logic core draft design
DavoudEshtehari Aug 14, 2020
185c8cd
Improve
DavoudEshtehari Sep 24, 2020
f4e1a24
add comment & prune
DavoudEshtehari Sep 25, 2020
366c21a
Use abstract instead interface
DavoudEshtehari Sep 26, 2020
9b344fc
Merge remote-tracking branch 'UpStream/master' into ConfigurableRetry
DavoudEshtehari Sep 26, 2020
f0ada60
improve retry condition & configs
DavoudEshtehari Oct 5, 2020
eab4106
Add parameter options & generalize the pre-condition
DavoudEshtehari Oct 13, 2020
9909fe5
Add unit tests
DavoudEshtehari Oct 13, 2020
868e576
Support the Netfx & NetCore with common source code
DavoudEshtehari Oct 15, 2020
8c6d8f8
Merge remote-tracking branch 'UpStream/master' into ConfigurableRetry
DavoudEshtehari Oct 15, 2020
b653999
Fix tests
DavoudEshtehari Oct 15, 2020
ee837b6
Merge remote-tracking branch 'UpStream/master' into ConfigurableRetry
DavoudEshtehari Oct 19, 2020
bc09f5f
Fix tests
DavoudEshtehari Oct 20, 2020
cd9b42d
Skip active issues
DavoudEshtehari Oct 20, 2020
da06ca9
Fix diagnostic tests
DavoudEshtehari Oct 23, 2020
d8a556c
Skip active issues
DavoudEshtehari Oct 23, 2020
ccfbdc1
Improvement & Tests
DavoudEshtehari Oct 29, 2020
83182d6
Backport issue 829 (#841)
Dec 5, 2020
e6ba4ed
Merge remote-tracking branch 'UpStream/master' into ConfigurableRetry
Dec 11, 2020
dbd7a4b
Port fix (#848)
karinazhou Dec 11, 2020
f8c274f
Fix | Fix TCP Keep Alive missing call (#855)
cheenamalhotra Dec 18, 2020
54b8eb8
Update release notes for v2.1.1 (#857)
cheenamalhotra Dec 18, 2020
91ec47c
Merge remote-tracking branch 'UpStream/2.1-servicing' into Configurab…
Dec 30, 2020
c362aa8
Add documents + Improvement
Jan 9, 2021
5b6c70f
Merge remote-tracking branch 'UpStream/master' into ConfigurableRetry
Jan 9, 2021
8299116
Update building guide
Jan 9, 2021
738da14
Merge remote-tracking branch 'UpStream/master' into ConfigurableRetry
Jan 14, 2021
415cc7f
Increase code coverage
Jan 20, 2021
d385863
Supports a configuration file
Feb 15, 2021
f6a417d
Merge remote-tracking branch 'UpStream/master' into ConfigurableRetry
Feb 15, 2021
fcc5890
Merge remote-tracking branch 'UpSream/master' into ConfigurableRetry
Feb 22, 2021
8ab901b
Update src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/Data…
DavoudEshtehari Feb 24, 2021
9cd227b
Apply suggestions from documents code review
DavoudEshtehari Feb 24, 2021
0194460
Apply suggestions from code review
DavoudEshtehari Feb 26, 2021
54afb7c
Merge remote-tracking branch 'UpSream/master' into ConfigurableRetry
Feb 26, 2021
451118d
Merge branch 'ConfigurableRetry' of github.com:DavoudEshtehari/SqlCli…
Feb 26, 2021
85a848e
Improved configuration manager
Feb 26, 2021
d64e43d
Merge remote-tracking branch 'UpStream/master' into ConfigurableRetry
Feb 27, 2021
fe75ca9
Address comments
Feb 27, 2021
dc6eda7
Apply suggestions from code review
DavoudEshtehari Feb 27, 2021
b181059
Address comments
Mar 1, 2021
4b567bc
Merge remote-tracking branch 'UpStream/master' into ConfigurableRetry
Mar 1, 2021
03c4f4a
Address comments
Mar 1, 2021
ad11253
Address comment
Mar 2, 2021
087fa76
Apply suggestions from code review
DavoudEshtehari Mar 2, 2021
0164582
Apply suggestions from code review
DavoudEshtehari Mar 3, 2021
7760254
Address comments
Mar 3, 2021
738ab94
Address comments
Mar 3, 2021
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
6 changes: 6 additions & 0 deletions BUILDGUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,12 @@ Scaled decimal parameter truncation can be enabled by enabling the below AppCont

**"Switch.Microsoft.Data.SqlClient.TruncateScaledDecimal"**

## Enabling configurable retry logic

To use this feature, you must enable the following AppContext switch at application startup:

**"Switch.Microsoft.Data.SqlClient.EnableRetryLogic"**

## Debugging SqlClient on Linux from Windows

For enhanced developer experience, we support debugging SqlClient on Linux from Windows, using the project "**Microsoft.Data.SqlClient.DockerLinuxTest**" that requires "Container Tools" to be enabled in Visual Studio. You may import configuration: [VS19Components.vsconfig](./tools/vsconfig/VS19Components.vsconfig) if not enabled already.
Expand Down
105 changes: 105 additions & 0 deletions doc/samples/SqlConfigurableRetryLogic_OpenConnection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using System;
// <Snippet1>
using Microsoft.Data.SqlClient;

/// Detecting retriable exceptions is a vital part of the retry pattern.
/// Before applying retry logic it is important to investigate exceptions and choose a retry provider that best fits your scenario.
/// First, log your exceptions and find transient faults.
/// The purpose of this sample is to illustrate how to use this feature and the condition might not be realistic.
class RetryLogicSample
{
private const string DefaultDB = "Northwind";
private const string CnnStringFormat = "Server=localhost; Initial Catalog={0}; Integrated Security=true; pooling=false;";
private const string DropDatabaseFormat = "DROP DATABASE {0}";

// For general use
private static SqlConnection s_generalConnection = new SqlConnection(string.Format(CnnStringFormat, DefaultDB));

static void Main(string[] args)
{
// 1. Define the retry logic parameters
var options = new SqlRetryLogicOption()
{
NumberOfTries = 5,
MaxTimeInterval = TimeSpan.FromSeconds(20),
DeltaTime = TimeSpan.FromSeconds(1)
};

// 2. Create a retry provider
var provider = SqlConfigurableRetryFactory.CreateExponentialRetryProvider(options);

// define the retrying event to report the execution attempts
provider.Retrying += (object s, SqlRetryingEventArgs e) =>
{
int attempts = e.RetryCount + 1;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"attempt {attempts} - current delay time:{e.Delay} \n");
Console.ForegroundColor = ConsoleColor.DarkGray;
if (e.Exceptions[e.Exceptions.Count - 1] is SqlException ex)
{
Console.WriteLine($"{ex.Number}-{ex.Message}\n");
}
else
{
Console.WriteLine($"{e.Exceptions[e.Exceptions.Count - 1].Message}\n");
}

// It is not a good practice to do time-consuming tasks inside the retrying event which blocks the running task.
// Use parallel programming patterns to mitigate it.
if (e.RetryCount == provider.RetryLogic.NumberOfTries - 1)
{
Console.WriteLine("This is the last chance to execute the command before throwing the exception.");
Console.WriteLine("Press Enter when you're ready:");
Console.ReadLine();
Console.WriteLine("continue ...");
}
};

// Open the general connection.
s_generalConnection.Open();

try
{
// Assume the database is creating and other services are going to connect to it.
RetryConnection(provider);
}
catch
{
// exception is thrown if connecting to the database isn't successful.
throw;
}
}

private static void ExecuteCommand(SqlConnection cn, string command)
{
using var cmd = cn.CreateCommand();
cmd.CommandText = command;
cmd.ExecuteNonQuery();
}

private static void RetryConnection(SqlRetryLogicBaseProvider provider)
{
// Change this if you already have a database with the same name in your database.
string dbName = "Invalid_DB_Open";

// Create a connection to an invalid database.
using var cnn = new SqlConnection(string.Format(CnnStringFormat, dbName));
// 3. Assign the `provider` to the connection
cnn.RetryLogicProvider = provider;
Console.WriteLine($"Connecting to the [{dbName}] ...");
// Manually execute the following command in SSMS to create the invalid database while the SqlConnection is attempting to connect to it.
// >> CREATE DATABASE Invalid_DB_Open;
Console.WriteLine($"Manually, run the 'CREATE DATABASE {dbName};' in the SQL Server before exceeding the {provider.RetryLogic.NumberOfTries} attempts.");
// the connection tries to connect to the database 5 times
Console.WriteLine("The first attempt, before getting into the retry logic.");
cnn.Open();
Console.WriteLine($"Connected to the [{dbName}] successfully.");

cnn.Close();

// Drop it after test
ExecuteCommand(s_generalConnection, string.Format(DropDatabaseFormat, dbName));
Console.WriteLine($"The [{dbName}] is removed.");
}
}
// </Snippet1>
243 changes: 243 additions & 0 deletions doc/samples/SqlConfigurableRetryLogic_SqlCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
using System;
using System.Threading.Tasks;
using Microsoft.Data.SqlClient;

class RetryLogicSample
{
// <Snippet1>
/// Detecting retriable exceptions is a vital part of the retry pattern.
/// Before applying retry logic it is important to investigate exceptions and choose a retry provider that best fits your scenario.
/// First, log your exceptions and find transient faults.
/// The purpose of this sample is to illustrate how to use this feature and the condition might not be realistic.

private const string DefaultDB = "Northwind";
private const string CnnStringFormat = "Server=localhost; Initial Catalog={0}; Integrated Security=true; pooling=false;";
private const string DropDatabaseFormat = "DROP DATABASE {0}";
private const string CreateDatabaseFormat = "CREATE DATABASE {0}";

// For general use
private static SqlConnection s_generalConnection = new SqlConnection(string.Format(CnnStringFormat, DefaultDB));

static void Main(string[] args)
{
// 1. Define the retry logic parameters
var options = new SqlRetryLogicOption()
{
NumberOfTries = 5,
MaxTimeInterval = TimeSpan.FromSeconds(20),
DeltaTime = TimeSpan.FromSeconds(1),
AuthorizedSqlCondition = null,
// error number 3702 : Cannot drop database "xxx" because it is currently in use.
TransientErrors = new int[] {3702}
};

// 2. Create a retry provider
var provider = SqlConfigurableRetryFactory.CreateExponentialRetryProvider(options);

// define the retrying event to report execution attempts
provider.Retrying += (object s, SqlRetryingEventArgs e) =>
{
int attempts = e.RetryCount + 1;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"attempt {attempts} - current delay time:{e.Delay} \n");
Console.ForegroundColor = ConsoleColor.DarkGray;
if (e.Exceptions[e.Exceptions.Count - 1] is SqlException ex)
{
Console.WriteLine($"{ex.Number}-{ex.Message}\n");
}
else
{
Console.WriteLine($"{e.Exceptions[e.Exceptions.Count - 1].Message}\n");
}

// It is not good practice to do time-consuming tasks inside the retrying event which blocks the running task.
// Use parallel programming patterns to mitigate it.
if (e.RetryCount == provider.RetryLogic.NumberOfTries - 1)
{
Console.WriteLine("This is the last chance to execute the command before throwing the exception.");
Console.WriteLine("Press Enter when you're ready:");
Console.ReadLine();
Console.WriteLine("continue ...");
}
};

// Open a general connection.
s_generalConnection.Open();

try
{
// Assume the database is creating and other services are going to connect to it.
RetryCommand(provider);
}
catch
{
// exception is thrown if connecting to the database isn't successful.
throw;
}
}

private static void ExecuteCommand(SqlConnection cn, string command)
{
using var cmd = cn.CreateCommand();
cmd.CommandText = command;
cmd.ExecuteNonQuery();
}

private static void FindActiveSessions(SqlConnection cnn, string dbName)
{
using var cmd = cnn.CreateCommand();
cmd.CommandText = "DECLARE @query NVARCHAR(max) = '';" + Environment.NewLine +
$"SELECT @query = @query + 'KILL ' + CAST(spid as varchar(50)) + ';' FROM sys.sysprocesses WHERE dbid = DB_ID('{dbName}')" + Environment.NewLine +
"SELECT @query AS Active_sessions;";
var reader = cmd.ExecuteReader();
if (reader.Read())
{
Console.ForegroundColor = ConsoleColor.Green;
Console.Write($">> Execute the '{reader.GetString(0)}' command in SQL Server to unblock the running task.");
Console.ResetColor();
}
reader.Close();
}
// </Snippet1>
// <Snippet2>
private static void RetryCommand(SqlRetryLogicBaseProvider provider)
Copy link
Member

Choose a reason for hiding this comment

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

Can you add some comments to distinguish these private static void RetryCommand(SqlRetryLogicBaseProvider provider) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You can find the description in the related SqlCommand.xml file. There is a common explanation for this series of samples. Feel free to suggest here If you need something more.

{
// Change this if you already have a database with the same name in your database.
string dbName = "RetryCommand_TestDatabase";

// Subscribe a new event on retry event and discover the active sessions on a database
EventHandler<SqlRetryingEventArgs> retryEvent = (object s, SqlRetryingEventArgs e) =>
{
// Run just at first execution
if (e.RetryCount == 1)
{
FindActiveSessions(s_generalConnection, dbName);
Console.WriteLine($"Before exceeding {provider.RetryLogic.NumberOfTries} attempts.");
}
};

provider.Retrying += retryEvent;

// Create a new database.
ExecuteCommand(s_generalConnection, string.Format(CreateDatabaseFormat, dbName));
Console.WriteLine($"The '{dbName}' database is created.");

// Open a connection to the newly created database to block it from being dropped.
using var blockingCnn = new SqlConnection(string.Format(CnnStringFormat, dbName));
blockingCnn.Open();
Console.WriteLine($"Established a connection to '{dbName}' to block it from being dropped.");

Console.WriteLine($"Dropping `{dbName}`...");
// Try to drop the new database.
RetryCommandSync(provider, dbName);

Console.WriteLine("Command executed successfully.");

provider.Retrying -= retryEvent;
}

private static void RetryCommandSync(SqlRetryLogicBaseProvider provider, string dbName)
{
using var cmd = s_generalConnection.CreateCommand();
cmd.CommandText = string.Format(DropDatabaseFormat, dbName);
// 3. Assign the `provider` to the command
cmd.RetryLogicProvider = provider;
Console.WriteLine("The first attempt, before getting into the retry logic.");
cmd.ExecuteNonQuery();
}
// </Snippet2>
// <Snippet3>
private static void RetryCommand(SqlRetryLogicBaseProvider provider)
{
// Change this if you already have a database with the same name in your database.
string dbName = "RetryCommand_TestDatabase";

// Subscribe to the retry event and discover active sessions in a database
EventHandler<SqlRetryingEventArgs> retryEvent = (object s, SqlRetryingEventArgs e) =>
{
// Run just at first execution
if (e.RetryCount == 1)
{
FindActiveSessions(s_generalConnection, dbName);
Console.WriteLine($"Before exceeding {provider.RetryLogic.NumberOfTries} attempts.");
}
};

provider.Retrying += retryEvent;

// Create a new database.
ExecuteCommand(s_generalConnection, string.Format(CreateDatabaseFormat, dbName));
Console.WriteLine($"The '{dbName}' database is created.");

// Open a connection to the newly created database to block it from being dropped.
using var blockingCnn = new SqlConnection(string.Format(CnnStringFormat, dbName));
blockingCnn.Open();
Console.WriteLine($"Established a connection to '{dbName}' to block it from being dropped.");

Console.WriteLine("Dropping the database...");
// Try to drop the new database.
RetryCommandAsync(provider, dbName).Wait();

Console.WriteLine("Command executed successfully.");

provider.Retrying -= retryEvent;
}

private static async Task RetryCommandAsync(SqlRetryLogicBaseProvider provider, string dbName)
{
using var cmd = s_generalConnection.CreateCommand();
cmd.CommandText = string.Format(DropDatabaseFormat, dbName);
// 3. Assign the `provider` to the command
cmd.RetryLogicProvider = provider;
Console.WriteLine("The first attempt, before getting into the retry logic.");
await cmd.ExecuteNonQueryAsync();
}
// </Snippet3>
// <Snippet4>
private static void RetryCommand(SqlRetryLogicBaseProvider provider)
{
// Change this if you already have a database with the same name in your database.
string dbName = "RetryCommand_TestDatabase";

// Subscribe to the retry event and discover the active sessions in a database
EventHandler<SqlRetryingEventArgs> retryEvent = (object s, SqlRetryingEventArgs e) =>
{
// Run just at first execution
if (e.RetryCount == 1)
{
FindActiveSessions(s_generalConnection, dbName);
Console.WriteLine($"Before exceeding {provider.RetryLogic.NumberOfTries} attempts.");
}
};

provider.Retrying += retryEvent;

// Create a new database.
ExecuteCommand(s_generalConnection, string.Format(CreateDatabaseFormat, dbName));
Console.WriteLine($"The '{dbName}' database is created.");

// Open a connection to the newly created database to block it from being dropped.
using var blockingCnn = new SqlConnection(string.Format(CnnStringFormat, dbName));
blockingCnn.Open();
Console.WriteLine($"Established a connection to '{dbName}' to block it from being dropped.");

Console.WriteLine("Dropping the database...");
// Try to drop the new database.
RetryCommandBeginExecuteAsync(provider, dbName).Wait();

Console.WriteLine("Command executed successfully.");

provider.Retrying -= retryEvent;
}

private static async Task RetryCommandBeginExecuteAsync(SqlRetryLogicBaseProvider provider, string dbName)
{
using var cmd = s_generalConnection.CreateCommand();
cmd.CommandText = string.Format(DropDatabaseFormat, dbName);
// Execute the BeginExecuteXXX and EndExecuteXXX functions by using Task.Factory.FromAsync().
// Apply the retry logic by using the ExecuteAsync function of the configurable retry logic provider.
Console.WriteLine("The first attempt, before getting into the retry logic.");
await provider.ExecuteAsync(cmd, () => Task.Factory.FromAsync(cmd.BeginExecuteNonQuery(), cmd.EndExecuteNonQuery));
}
// </Snippet4>
}
25 changes: 25 additions & 0 deletions doc/samples/SqlConfigurableRetryLogic_SqlRetryLogicOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using System.Text.RegularExpressions;
using Microsoft.Data.SqlClient;

class RetryLogicSample
{
static void Main(string[] args)
{
// <Snippet1>
var RetryLogicOption = new SqlRetryLogicOption()
{
NumberOfTries = 5,
// Declare the error number 102 as a transient error to apply the retry logic when it occurs.
TransientErrors = new int[] { 102 },
// When a SqlCommand executes out of a transaction,
// the retry logic will apply if it contains a 'select' keyword.
AuthorizedSqlCondition = x => string.IsNullOrEmpty(x)
|| Regex.IsMatch(x, @"\b(SELECT)\b", RegexOptions.IgnoreCase),
DeltaTime = TimeSpan.FromSeconds(1),
MaxTimeInterval = TimeSpan.FromSeconds(60),
MinTimeInterval = TimeSpan.FromSeconds(3)
};
// </Snippet1>
}
}
Loading