Skip to content

Commit

Permalink
Configurable Retry Logic - Preview 1 (#693)
Browse files Browse the repository at this point in the history
* 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
DavoudEshtehari authored Mar 4, 2021
1 parent b7e714b commit a2a0649
Show file tree
Hide file tree
Showing 48 changed files with 4,266 additions and 37 deletions.
6 changes: 6 additions & 0 deletions BUILDGUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,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 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>
245 changes: 245 additions & 0 deletions doc/samples/SqlConfigurableRetryLogic_SqlCommand.cs
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 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

0 comments on commit a2a0649

Please sign in to comment.