A few days ago I wrote a blog post about gaining a better understanding of the new connection resiliency feature of EF6. The feature is implemented using an implementation of a new class, IDbExecutionStrategy.
The SQl Server provider thatās bundled with EF6 has a strategy aimed at Windows Azure SQL Database (aka SQL Azure) that is designed to retry a command if a transient connection error is thrown. My blog post listed the transient error messages.
I wanted to see this in action but itās not simple to make a transient error occur. There was a blog post with sample code for creating a deadlock, but then Glenn Condron on the EF team suggested just throwing the error by leveraging the new EF6 ability to intercept commands & queries headed to the database. It can also intercept data coming back.
A few people have asked how I did this, so Iāll share my setup here.
I played with that a bit and realized that the SqlAzureExecutionStrategy required more than just the correct error code to trigger the retries. It needs a SQLException to throw that error. And a little more banging led me to realize that you canāt just instantiate a SqlException, it has to be done via reflection.
But I donāt give up easily. I found some helpful examples for doing this though it still wasnāt simple. Maybe things have changed, but I finally tweaked teh sample code enough to get what I needed.
Still, I was unsuccessful because EF was first doing itās initialization tasks and the SqlAzureExecutionStrategy was not responding to errors thrown by those commands. I had to filter those commands out. Then finally, it worked!
There may be an easier way. I know Glenn does this differently. What I have worked out is witnessing the retries but since thatās all I watned to see, Iām not worried about having one of those retries be successful. I know that in the real world, as itās retrying and the transient connection kicks back in, one of those retries will get through.
So to test out the feature you need four puzzle pieces.
1) Intercept the (non initialization/migration) command
2) Use DbConfiguration to make sure the interception is happening
3) Use DbConfiguration to set up the SqlAzureExecutionStrategy
4) Run an integration test that will attempt one or more queries on the database via EF.
Step 1) Intercept the command and throw the correct error
For this I created a new class, TransientFailureCausingCommandInterceptor, that inherits from IDbCommandInterceptor. There are a number of methods to override. The only one Iām interested in is ReaderExecutingā¦i.e. a read command is about to execute.
public class TransientFailureCausingCommandInterceptor : IDbCommandInterceptor
{
public void ReaderExecuting(
DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
{
}
etcā¦
In the command , I filter out commands that are for the database initialization and migration work, in case they are executing.
For any other commands, I write out some info to Debug so I know that a query is about to execute, then I use my helper class to create a fake SQL Exception that throws one of the transient error codes that SqlAzureExecutionStrategy looks for: 10053.
public void ReaderExecuting(
DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
{
if (!(command.CommandText.Contains("serverproperty")
|| command.CommandText.Contains("_MigrationHistory")))
{
Debug.WriteLine("throwing fake exception from interceptor");
throw SqlExceptionFaker.Error10053;
}
}
The SqlExceptionFaker is the magic for throwing a SqlException. It is a twist onĀ this example I found from Microsoft MVP, Chris Pietschmann. I found some other ways of doing this but , in my opinion, Chrisā was the best of the solutions that I found.
using System.Collections;
using System.Data.SqlClient;
using System.Runtime.Serialization;
namespace SqlExceptions
{
public static class SqlExceptionFaker
{
private static SqlException _error10053;
public static SqlException Error10053
{
get
{
if (_error10053 == null)
{
_error10053 = Generate(SqlExceptionNumber.TransportLevelReceiving);
}
return _error10053;
}
}
public enum SqlExceptionNumber : int
{
TimeoutExpired = -2,
EncryptionNotSupported = 20,
LoginError = 64,
ConnectionInitialization = 233,
TransportLevelReceiving = 10053,
TransportLevelSending = 10054,
EstablishingConnection = 10060,
ProcessingRequest = 40143,
ServiceBusy = 40501,
DatabaseOrServerNotAvailable = 40613
}
public static SqlException Generate(SqlExceptionNumber errorNumber)
{
return SqlExceptionFaker.Generate((int) errorNumber);
}
public static SqlException Generate(int errorNumber)
{
var ex = (SqlException) FormatterServices.GetUninitializedObject(typeof (SqlException));
var errors = GenerateSqlErrorCollection(errorNumber);
SetPrivateFieldValue(ex, "_errors", errors);
return ex;
}
private static SqlErrorCollection GenerateSqlErrorCollection(int errorNumber)
{
var t = typeof (SqlErrorCollection);
var col = (SqlErrorCollection) FormatterServices.GetUninitializedObject(t);
SetPrivateFieldValue(col, "errors", new ArrayList());
var sqlError = GenerateSqlError(errorNumber);
var method = t.GetMethod(
"Add",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance );
method.Invoke(col, new object[] {sqlError});
return col;
}
private static SqlError GenerateSqlError(int errorNumber)
{
var sqlError = (SqlError) FormatterServices.GetUninitializedObject(typeof (SqlError));
SetPrivateFieldValue(sqlError, "number", errorNumber);
SetPrivateFieldValue(sqlError, "message", errorNumber.ToString());
SetPrivateFieldValue(sqlError, "procedure", string.Empty);
SetPrivateFieldValue(sqlError, "server", string.Empty);
SetPrivateFieldValue(sqlError, "source", string.Empty);
SetPrivateFieldValue(sqlError, "win32ErrorCode", errorNumber);
return sqlError;
}
private static void SetPrivateFieldValue(object obj, string field, object val)
{
var member = obj.GetType().GetField(
field,
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance
);
member.SetValue(obj, val);
}
}
}
Phew!
Step 2) Force the context to use this interceptor
This happens in the lovely new DbConfiguration class. Read more about it here.
public class CustomDbConfiguration : DbConfiguration
{
public CustomDbConfiguration()
{
AddInterceptor(new CasinoModel.TransientFailureCausingCommandInterceptor());
}
}
Step 3) Make sure your DbConfiguraiton class is wired up in app/web.config in the EntityConnection section:
<entityFramework codeConfigurationType="DataLayer.DbConfigurations.CustomDbConfiguration,CasinoModel">
. . .
</entityFramework>
Step 4) Test
This is a test that works with my model. Notice that my test specifies a connection string. That ensures that my context is using the SQL Azure connection I set up in my config file.
[TestMethod, TestCategory("Connection Resiliency")]
public void CanHitSqlAzureDbWithTransientFailure()
{
int slotcount = 0;
using (var context = new CasinoSlotsModel(connectionStringName: "CasinoHotelsAzure"))
{
foreach (var casino in context.Casinos)
{
context.Entry(casino).Collection(c => c.SlotMachines).Load();
slotcount += casino.SlotMachines.Count;
}
}
Assert.AreNotEqual(0, slotcount);
}
The test fails because an exception was thrown ā the SqlException that I faked. Thatās because an error is getting thrown. When I check the output
The SqlException caused a series of exceptions in response. Notice the nice message from EntityException: āconsider using a SqlAzureExecutionStrategyā.
Result Message:
Test method AutomatedTests.UnitTest1.CanHitSqlAzureDbWithTransientFailure threw exception:
System.Data.DataException: An exception occurred while initializing the database. See the InnerException for details. —> System.Data.Entity.Core.EntityException: An exception has been raised that is likely due to a transient failure. If you are connecting to a SQL Azure database consider using SqlAzureExecutionStrategy. —>
System.Data.Entity.Core.EntityCommandExecutionException: An error occurred while executing the command definition. See the inner exception for details. —>
System.Data.SqlClient.SqlException: Exception of type ‘System.Data.SqlClient.SqlException’ was thrown.
Looking at the failed testās OUTPUT I see the text spit out by the ReaderExecuting method:
The method was only hit once, so I see my message only once.
Step 5) Add in the SqlAzureConnectionStretegy to the DbConfiguration file:
public CustomDbConfiguration()
{
AddInterceptor(new CasinoModel.TransientFailureCausingCommandInterceptor());
SetExecutionStrategy
(SqlProviderServices.ProviderInvariantName, () => new SqlAzureExecutionStrategy());
}
Using the default settings of SqlAzureExecutionStrategy will cause 5 retries.
Step 6) Run the test again:
The test hangs for a lot longer than fails. Why longer? The retries! I get 6 messages. The first is the initial problem and the other 5 are for each of the 5 retries.
Why does it still fail? Because my setup doesnāt āturn offā the error. Thatās fine. I just want to see that it does actually retry.
The exception is different this time. RetryLimitExceededException, max retries (5) , etcā¦
Result Message:
Test method AutomatedTests.UnitTest1.CanHitSqlAzureDbWithTransientFailure threw exception:
System.Data.DataException: An exception occurred while initializing the database. See the InnerException for details. —> System.Data.Entity.Infrastructure.RetryLimitExceededException: Maximum number of retries (5) exceeded while executing database operations with ‘SqlAzureExecutionStrategy’. See inner exception for the most recent failure. —>
System.Data.Entity.Core.EntityCommandExecutionException: An error occurred while executing the command definition. See the inner exception for details. —>
System.Data.SqlClient.SqlException: Exception of type ‘System.Data.SqlClient.SqlException’ was thrown.
Step 7) Modify the connection retries
Next Iāll change the default of the SqlAzureExecutionStrategy to retry only 3 times.
(SqlProviderServices.ProviderInvariantName, () => new SqlAzureExecutionStrategy(3,new TimeSpan(15)));
Look at the output after I run the test again. Only 3 retries then it gives up.
So thatās it. Iām content to see this feature in action.
I was also happy to see a comment in the previous blog post from a developer who has seen the benefits of this feature already in their production environment.