Skip to content

Commit a2a0649

Browse files
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.
1 parent b7e714b commit a2a0649

File tree

48 files changed

+4266
-37
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+4266
-37
lines changed

BUILDGUIDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,12 @@ Scaled decimal parameter truncation can be enabled by enabling the below AppCont
236236

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

239+
## Enabling configurable retry logic
240+
241+
To use this feature, you must enable the following AppContext switch at application startup:
242+
243+
**"Switch.Microsoft.Data.SqlClient.EnableRetryLogic"**
244+
239245
## Debugging SqlClient on Linux from Windows
240246

241247
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.
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using System;
2+
// <Snippet1>
3+
using Microsoft.Data.SqlClient;
4+
5+
/// Detecting retriable exceptions is a vital part of the retry pattern.
6+
/// Before applying retry logic it is important to investigate exceptions and choose a retry provider that best fits your scenario.
7+
/// First, log your exceptions and find transient faults.
8+
/// The purpose of this sample is to illustrate how to use this feature and the condition might not be realistic.
9+
class RetryLogicSample
10+
{
11+
private const string DefaultDB = "Northwind";
12+
private const string CnnStringFormat = "Server=localhost; Initial Catalog={0}; Integrated Security=true; pooling=false;";
13+
private const string DropDatabaseFormat = "DROP DATABASE {0}";
14+
15+
// For general use
16+
private static SqlConnection s_generalConnection = new SqlConnection(string.Format(CnnStringFormat, DefaultDB));
17+
18+
static void Main(string[] args)
19+
{
20+
// 1. Define the retry logic parameters
21+
var options = new SqlRetryLogicOption()
22+
{
23+
NumberOfTries = 5,
24+
MaxTimeInterval = TimeSpan.FromSeconds(20),
25+
DeltaTime = TimeSpan.FromSeconds(1)
26+
};
27+
28+
// 2. Create a retry provider
29+
var provider = SqlConfigurableRetryFactory.CreateExponentialRetryProvider(options);
30+
31+
// define the retrying event to report the execution attempts
32+
provider.Retrying += (object s, SqlRetryingEventArgs e) =>
33+
{
34+
int attempts = e.RetryCount + 1;
35+
Console.ForegroundColor = ConsoleColor.Yellow;
36+
Console.WriteLine($"attempt {attempts} - current delay time:{e.Delay} \n");
37+
Console.ForegroundColor = ConsoleColor.DarkGray;
38+
if (e.Exceptions[e.Exceptions.Count - 1] is SqlException ex)
39+
{
40+
Console.WriteLine($"{ex.Number}-{ex.Message}\n");
41+
}
42+
else
43+
{
44+
Console.WriteLine($"{e.Exceptions[e.Exceptions.Count - 1].Message}\n");
45+
}
46+
47+
// It is not a good practice to do time-consuming tasks inside the retrying event which blocks the running task.
48+
// Use parallel programming patterns to mitigate it.
49+
if (e.RetryCount == provider.RetryLogic.NumberOfTries - 1)
50+
{
51+
Console.WriteLine("This is the last chance to execute the command before throwing the exception.");
52+
Console.WriteLine("Press Enter when you're ready:");
53+
Console.ReadLine();
54+
Console.WriteLine("continue ...");
55+
}
56+
};
57+
58+
// Open the general connection.
59+
s_generalConnection.Open();
60+
61+
try
62+
{
63+
// Assume the database is being created and other services are going to connect to it.
64+
RetryConnection(provider);
65+
}
66+
catch
67+
{
68+
// exception is thrown if connecting to the database isn't successful.
69+
throw;
70+
}
71+
}
72+
73+
private static void ExecuteCommand(SqlConnection cn, string command)
74+
{
75+
using var cmd = cn.CreateCommand();
76+
cmd.CommandText = command;
77+
cmd.ExecuteNonQuery();
78+
}
79+
80+
private static void RetryConnection(SqlRetryLogicBaseProvider provider)
81+
{
82+
// Change this if you already have a database with the same name in your database.
83+
string dbName = "Invalid_DB_Open";
84+
85+
// Create a connection to an invalid database.
86+
using var cnn = new SqlConnection(string.Format(CnnStringFormat, dbName));
87+
// 3. Assign the `provider` to the connection
88+
cnn.RetryLogicProvider = provider;
89+
Console.WriteLine($"Connecting to the [{dbName}] ...");
90+
// Manually execute the following command in SSMS to create the invalid database while the SqlConnection is attempting to connect to it.
91+
// >> CREATE DATABASE Invalid_DB_Open;
92+
Console.WriteLine($"Manually, run the 'CREATE DATABASE {dbName};' in the SQL Server before exceeding the {provider.RetryLogic.NumberOfTries} attempts.");
93+
// the connection tries to connect to the database 5 times
94+
Console.WriteLine("The first attempt, before getting into the retry logic.");
95+
cnn.Open();
96+
Console.WriteLine($"Connected to the [{dbName}] successfully.");
97+
98+
cnn.Close();
99+
100+
// Drop it after test
101+
ExecuteCommand(s_generalConnection, string.Format(DropDatabaseFormat, dbName));
102+
Console.WriteLine($"The [{dbName}] is removed.");
103+
}
104+
}
105+
// </Snippet1>
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Microsoft.Data.SqlClient;
4+
5+
class RetryLogicSample
6+
{
7+
// <Snippet1>
8+
/// Detecting retriable exceptions is a vital part of the retry pattern.
9+
/// Before applying retry logic it is important to investigate exceptions and choose a retry provider that best fits your scenario.
10+
/// First, log your exceptions and find transient faults.
11+
/// The purpose of this sample is to illustrate how to use this feature and the condition might not be realistic.
12+
13+
private const string DefaultDB = "Northwind";
14+
private const string CnnStringFormat = "Server=localhost; Initial Catalog={0}; Integrated Security=true; pooling=false;";
15+
private const string DropDatabaseFormat = "DROP DATABASE {0}";
16+
private const string CreateDatabaseFormat = "CREATE DATABASE {0}";
17+
18+
// For general use
19+
private static SqlConnection s_generalConnection = new SqlConnection(string.Format(CnnStringFormat, DefaultDB));
20+
21+
static void Main(string[] args)
22+
{
23+
// 1. Define the retry logic parameters
24+
var options = new SqlRetryLogicOption()
25+
{
26+
NumberOfTries = 5,
27+
MaxTimeInterval = TimeSpan.FromSeconds(20),
28+
DeltaTime = TimeSpan.FromSeconds(1),
29+
AuthorizedSqlCondition = null,
30+
// error number 3702 : Cannot drop database "xxx" because it is currently in use.
31+
TransientErrors = new int[] {3702}
32+
};
33+
34+
// 2. Create a retry provider
35+
var provider = SqlConfigurableRetryFactory.CreateExponentialRetryProvider(options);
36+
37+
// define the retrying event to report execution attempts
38+
provider.Retrying += (object s, SqlRetryingEventArgs e) =>
39+
{
40+
int attempts = e.RetryCount + 1;
41+
Console.ForegroundColor = ConsoleColor.Yellow;
42+
Console.WriteLine($"attempt {attempts} - current delay time:{e.Delay} \n");
43+
Console.ForegroundColor = ConsoleColor.DarkGray;
44+
if (e.Exceptions[e.Exceptions.Count - 1] is SqlException ex)
45+
{
46+
Console.WriteLine($"{ex.Number}-{ex.Message}\n");
47+
}
48+
else
49+
{
50+
Console.WriteLine($"{e.Exceptions[e.Exceptions.Count - 1].Message}\n");
51+
}
52+
53+
// It is not good practice to do time-consuming tasks inside the retrying event which blocks the running task.
54+
// Use parallel programming patterns to mitigate it.
55+
if (e.RetryCount == provider.RetryLogic.NumberOfTries - 1)
56+
{
57+
Console.WriteLine("This is the last chance to execute the command before throwing the exception.");
58+
Console.WriteLine("Press Enter when you're ready:");
59+
Console.ReadLine();
60+
Console.WriteLine("continue ...");
61+
}
62+
};
63+
64+
// Open a general connection.
65+
s_generalConnection.Open();
66+
67+
try
68+
{
69+
// Assume the database is creating and other services are going to connect to it.
70+
RetryCommand(provider);
71+
}
72+
catch
73+
{
74+
s_generalConnection.Close();
75+
// exception is thrown if connecting to the database isn't successful.
76+
throw;
77+
}
78+
s_generalConnection.Close();
79+
}
80+
81+
private static void ExecuteCommand(SqlConnection cn, string command)
82+
{
83+
using var cmd = cn.CreateCommand();
84+
cmd.CommandText = command;
85+
cmd.ExecuteNonQuery();
86+
}
87+
88+
private static void FindActiveSessions(SqlConnection cnn, string dbName)
89+
{
90+
using var cmd = cnn.CreateCommand();
91+
cmd.CommandText = "DECLARE @query NVARCHAR(max) = '';" + Environment.NewLine +
92+
$"SELECT @query = @query + 'KILL ' + CAST(spid as varchar(50)) + ';' FROM sys.sysprocesses WHERE dbid = DB_ID('{dbName}')" + Environment.NewLine +
93+
"SELECT @query AS Active_sessions;";
94+
var reader = cmd.ExecuteReader();
95+
if (reader.Read())
96+
{
97+
Console.ForegroundColor = ConsoleColor.Green;
98+
Console.Write($">> Execute the '{reader.GetString(0)}' command in SQL Server to unblock the running task.");
99+
Console.ResetColor();
100+
}
101+
reader.Close();
102+
}
103+
// </Snippet1>
104+
// <Snippet2>
105+
private static void RetryCommand(SqlRetryLogicBaseProvider provider)
106+
{
107+
// Change this if you already have a database with the same name in your database.
108+
string dbName = "RetryCommand_TestDatabase";
109+
110+
// Subscribe a new event on retry event and discover the active sessions on a database
111+
EventHandler<SqlRetryingEventArgs> retryEvent = (object s, SqlRetryingEventArgs e) =>
112+
{
113+
// Run just at first execution
114+
if (e.RetryCount == 1)
115+
{
116+
FindActiveSessions(s_generalConnection, dbName);
117+
Console.WriteLine($"Before exceeding {provider.RetryLogic.NumberOfTries} attempts.");
118+
}
119+
};
120+
121+
provider.Retrying += retryEvent;
122+
123+
// Create a new database.
124+
ExecuteCommand(s_generalConnection, string.Format(CreateDatabaseFormat, dbName));
125+
Console.WriteLine($"The '{dbName}' database is created.");
126+
127+
// Open a connection to the newly created database to block it from being dropped.
128+
using var blockingCnn = new SqlConnection(string.Format(CnnStringFormat, dbName));
129+
blockingCnn.Open();
130+
Console.WriteLine($"Established a connection to '{dbName}' to block it from being dropped.");
131+
132+
Console.WriteLine($"Dropping `{dbName}`...");
133+
// Try to drop the new database.
134+
RetryCommandSync(provider, dbName);
135+
136+
Console.WriteLine("Command executed successfully.");
137+
138+
provider.Retrying -= retryEvent;
139+
}
140+
141+
private static void RetryCommandSync(SqlRetryLogicBaseProvider provider, string dbName)
142+
{
143+
using var cmd = s_generalConnection.CreateCommand();
144+
cmd.CommandText = string.Format(DropDatabaseFormat, dbName);
145+
// 3. Assign the `provider` to the command
146+
cmd.RetryLogicProvider = provider;
147+
Console.WriteLine("The first attempt, before getting into the retry logic.");
148+
cmd.ExecuteNonQuery();
149+
}
150+
// </Snippet2>
151+
// <Snippet3>
152+
private static void RetryCommand(SqlRetryLogicBaseProvider provider)
153+
{
154+
// Change this if you already have a database with the same name in your database.
155+
string dbName = "RetryCommand_TestDatabase";
156+
157+
// Subscribe to the retry event and discover active sessions in a database
158+
EventHandler<SqlRetryingEventArgs> retryEvent = (object s, SqlRetryingEventArgs e) =>
159+
{
160+
// Run just at first execution
161+
if (e.RetryCount == 1)
162+
{
163+
FindActiveSessions(s_generalConnection, dbName);
164+
Console.WriteLine($"Before exceeding {provider.RetryLogic.NumberOfTries} attempts.");
165+
}
166+
};
167+
168+
provider.Retrying += retryEvent;
169+
170+
// Create a new database.
171+
ExecuteCommand(s_generalConnection, string.Format(CreateDatabaseFormat, dbName));
172+
Console.WriteLine($"The '{dbName}' database is created.");
173+
174+
// Open a connection to the newly created database to block it from being dropped.
175+
using var blockingCnn = new SqlConnection(string.Format(CnnStringFormat, dbName));
176+
blockingCnn.Open();
177+
Console.WriteLine($"Established a connection to '{dbName}' to block it from being dropped.");
178+
179+
Console.WriteLine("Dropping the database...");
180+
// Try to drop the new database.
181+
RetryCommandAsync(provider, dbName).Wait();
182+
183+
Console.WriteLine("Command executed successfully.");
184+
185+
provider.Retrying -= retryEvent;
186+
}
187+
188+
private static async Task RetryCommandAsync(SqlRetryLogicBaseProvider provider, string dbName)
189+
{
190+
using var cmd = s_generalConnection.CreateCommand();
191+
cmd.CommandText = string.Format(DropDatabaseFormat, dbName);
192+
// 3. Assign the `provider` to the command
193+
cmd.RetryLogicProvider = provider;
194+
Console.WriteLine("The first attempt, before getting into the retry logic.");
195+
await cmd.ExecuteNonQueryAsync();
196+
}
197+
// </Snippet3>
198+
// <Snippet4>
199+
private static void RetryCommand(SqlRetryLogicBaseProvider provider)
200+
{
201+
// Change this if you already have a database with the same name in your database.
202+
string dbName = "RetryCommand_TestDatabase";
203+
204+
// Subscribe to the retry event and discover the active sessions in a database
205+
EventHandler<SqlRetryingEventArgs> retryEvent = (object s, SqlRetryingEventArgs e) =>
206+
{
207+
// Run just at first execution
208+
if (e.RetryCount == 1)
209+
{
210+
FindActiveSessions(s_generalConnection, dbName);
211+
Console.WriteLine($"Before exceeding {provider.RetryLogic.NumberOfTries} attempts.");
212+
}
213+
};
214+
215+
provider.Retrying += retryEvent;
216+
217+
// Create a new database.
218+
ExecuteCommand(s_generalConnection, string.Format(CreateDatabaseFormat, dbName));
219+
Console.WriteLine($"The '{dbName}' database is created.");
220+
221+
// Open a connection to the newly created database to block it from being dropped.
222+
using var blockingCnn = new SqlConnection(string.Format(CnnStringFormat, dbName));
223+
blockingCnn.Open();
224+
Console.WriteLine($"Established a connection to '{dbName}' to block it from being dropped.");
225+
226+
Console.WriteLine("Dropping the database...");
227+
// Try to drop the new database.
228+
RetryCommandBeginExecuteAsync(provider, dbName).Wait();
229+
230+
Console.WriteLine("Command executed successfully.");
231+
232+
provider.Retrying -= retryEvent;
233+
}
234+
235+
private static async Task RetryCommandBeginExecuteAsync(SqlRetryLogicBaseProvider provider, string dbName)
236+
{
237+
using var cmd = s_generalConnection.CreateCommand();
238+
cmd.CommandText = string.Format(DropDatabaseFormat, dbName);
239+
// Execute the BeginExecuteXXX and EndExecuteXXX functions by using Task.Factory.FromAsync().
240+
// Apply the retry logic by using the ExecuteAsync function of the configurable retry logic provider.
241+
Console.WriteLine("The first attempt, before getting into the retry logic.");
242+
await provider.ExecuteAsync(cmd, () => Task.Factory.FromAsync(cmd.BeginExecuteNonQuery(), cmd.EndExecuteNonQuery));
243+
}
244+
// </Snippet4>
245+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
using System.Text.RegularExpressions;
3+
using Microsoft.Data.SqlClient;
4+
5+
class RetryLogicSample
6+
{
7+
static void Main(string[] args)
8+
{
9+
// <Snippet1>
10+
var RetryLogicOption = new SqlRetryLogicOption()
11+
{
12+
NumberOfTries = 5,
13+
// Declare the error number 102 as a transient error to apply the retry logic when it occurs.
14+
TransientErrors = new int[] { 102 },
15+
// When a SqlCommand executes out of a transaction,
16+
// the retry logic will apply if it contains a 'select' keyword.
17+
AuthorizedSqlCondition = x => string.IsNullOrEmpty(x)
18+
|| Regex.IsMatch(x, @"\b(SELECT)\b", RegexOptions.IgnoreCase),
19+
DeltaTime = TimeSpan.FromSeconds(1),
20+
MaxTimeInterval = TimeSpan.FromSeconds(60),
21+
MinTimeInterval = TimeSpan.FromSeconds(3)
22+
};
23+
// </Snippet1>
24+
}
25+
}

0 commit comments

Comments
 (0)