-
Notifications
You must be signed in to change notification settings - Fork 299
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Configurable Retry Logic - Preview 1 (#693)
* Configurable Retry Logic (preview 1): - Applicable by SqlConnection and SqlCommand. - Supports four internal retry providers (None-retriable, Fixed, Incremental, and Exponential). - Providing APIs to support customized implementations. - Supports configuration file to configure default retry logics per SqlConnection and SqlCommand. - Supports internal and external retry providers through the configuration file.
- Loading branch information
1 parent
b7e714b
commit a2a0649
Showing
48 changed files
with
4,266 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
105 changes: 105 additions & 0 deletions
105
doc/samples/SqlConfigurableRetryLogic_OpenConnection.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 being created 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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,245 @@ | ||
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 | ||
{ | ||
s_generalConnection.Close(); | ||
// exception is thrown if connecting to the database isn't successful. | ||
throw; | ||
} | ||
s_generalConnection.Close(); | ||
} | ||
|
||
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) | ||
{ | ||
// 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
25
doc/samples/SqlConfigurableRetryLogic_SqlRetryLogicOptions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
} | ||
} |
Oops, something went wrong.