Adding fault handling extensions for IDbCommand.

Adding unit tests to verify retry strategies.
Adding a RetryPolicy factory for our standard configuration.
This commit is contained in:
Morten Christensen
2013-02-05 14:31:55 -01:00
parent ef20f228ec
commit 31a5363387
9 changed files with 257 additions and 17 deletions

View File

@@ -62,7 +62,8 @@ namespace Umbraco.Core.Persistence
//double check
if (_globalInstance == null)
{
_globalInstance = string.IsNullOrEmpty(_providerName) == false && string.IsNullOrEmpty(_providerName) == false
_globalInstance = string.IsNullOrEmpty(_connectionString) == false &&
string.IsNullOrEmpty(_providerName) == false
? new UmbracoDatabase(_connectionString, _providerName)
: new UmbracoDatabase(_connectionStringName);
}
@@ -74,10 +75,11 @@ namespace Umbraco.Core.Persistence
//we have an http context, so only create one per request
if (!HttpContext.Current.Items.Contains(typeof(DefaultDatabaseFactory)))
{
HttpContext.Current.Items.Add(typeof (DefaultDatabaseFactory),
string.IsNullOrEmpty(_providerName) == false && string.IsNullOrEmpty(_providerName) == false
? new UmbracoDatabase(_connectionString, _providerName)
: new UmbracoDatabase(_connectionStringName));
HttpContext.Current.Items.Add(typeof (DefaultDatabaseFactory),
string.IsNullOrEmpty(_connectionString) == false &&
string.IsNullOrEmpty(_providerName) == false
? new UmbracoDatabase(_connectionString, _providerName)
: new UmbracoDatabase(_connectionStringName));
}
return (UmbracoDatabase)HttpContext.Current.Items[typeof(DefaultDatabaseFactory)];
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Threading;
using Umbraco.Core.Logging;
using Umbraco.Core.Persistence.FaultHandling.Strategies;
namespace Umbraco.Core.Persistence.FaultHandling
@@ -198,6 +199,9 @@ namespace Umbraco.Core.Persistence.FaultHandling
{
if (this.Retrying != null)
{
LogHelper.Info<RetryPolicy>(string.Format("Retrying - Count: {0}, Delay: {1}, Exception: {2}",
retryCount, delay.TotalMilliseconds, lastError.Message));
this.Retrying(this, new RetryingEventArgs(retryCount, delay, lastError));
}
}

View File

@@ -1,7 +1,57 @@
namespace Umbraco.Core.Persistence.FaultHandling
using Umbraco.Core.Persistence.FaultHandling.Strategies;
namespace Umbraco.Core.Persistence.FaultHandling
{
public class RetryPolicyFactory
/// <summary>
/// Provides a factory class for instantiating application-specific retry policies.
/// </summary>
public static class RetryPolicyFactory
{
public static RetryPolicy GetDefaultSqlConnectionRetryPolicyByConnectionString(string connectionString)
{
//Is this really the best way to determine if the database is an Azure database?
return connectionString.Contains("database.windows.net")
? GetDefaultSqlAzureConnectionRetryPolicy()
: GetDefaultSqlConnectionRetryPolicy();
}
public static RetryPolicy GetDefaultSqlConnectionRetryPolicy()
{
var retryStrategy = RetryStrategy.DefaultExponential;
var retryPolicy = new RetryPolicy(new NetworkConnectivityErrorDetectionStrategy(), retryStrategy);
return retryPolicy;
}
public static RetryPolicy GetDefaultSqlAzureConnectionRetryPolicy()
{
var retryStrategy = RetryStrategy.DefaultExponential;
var retryPolicy = new RetryPolicy(new SqlAzureTransientErrorDetectionStrategy(), retryStrategy);
return retryPolicy;
}
public static RetryPolicy GetDefaultSqlCommandRetryPolicyByConnectionString(string connectionString)
{
//Is this really the best way to determine if the database is an Azure database?
return connectionString.Contains("database.windows.net")
? GetDefaultSqlAzureCommandRetryPolicy()
: GetDefaultSqlCommandRetryPolicy();
}
public static RetryPolicy GetDefaultSqlCommandRetryPolicy()
{
var retryStrategy = RetryStrategy.DefaultFixed;
var retryPolicy = new RetryPolicy(new NetworkConnectivityErrorDetectionStrategy(), retryStrategy);
return retryPolicy;
}
public static RetryPolicy GetDefaultSqlAzureCommandRetryPolicy()
{
var retryStrategy = RetryStrategy.DefaultFixed;
var retryPolicy = new RetryPolicy(new SqlAzureTransientErrorDetectionStrategy(), retryStrategy);
return retryPolicy;
}
}
}

View File

@@ -0,0 +1,152 @@
using System.Data;
using Umbraco.Core.Persistence.FaultHandling;
namespace Umbraco.Core.Persistence
{
/// <summary>
/// Provides a set of extension methods adding retry capabilities into the standard <see cref="System.Data.IDbConnection"/> implementation, which is used in PetaPoco.
/// </summary>
public static class PetaPocoCommandExtensions
{
#region ExecuteNonQueryWithRetry method implementations
/// <summary>
/// Executes a Transact-SQL statement against the connection and returns the number of rows affected. Uses the default retry policy when executing the command.
/// </summary>
/// <param name="command">The command object that is required as per extension method declaration.</param>
/// <returns>The number of rows affected.</returns>
public static int ExecuteNonQueryWithRetry(this IDbCommand command)
{
var connectionString = command.Connection.ConnectionString ?? string.Empty;
return ExecuteNonQueryWithRetry(command, RetryPolicyFactory.GetDefaultSqlCommandRetryPolicyByConnectionString(connectionString));
}
/// <summary>
/// Executes a Transact-SQL statement against the connection and returns the number of rows affected. Uses the specified retry policy when executing the command.
/// </summary>
/// <param name="command">The command object that is required as per extension method declaration.</param>
/// <param name="retryPolicy">The retry policy defining whether to retry a command if a connection fails while executing the command.</param>
/// <returns>The number of rows affected.</returns>
public static int ExecuteNonQueryWithRetry(this IDbCommand command, RetryPolicy retryPolicy)
{
var connectionString = command.Connection.ConnectionString ?? string.Empty;
return ExecuteNonQueryWithRetry(command, retryPolicy, RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(connectionString));
}
/// <summary>
/// Executes a Transact-SQL statement against the connection and returns the number of rows affected. Uses the specified retry policy when executing the command.
/// Uses a separate specified retry policy when establishing a connection.
/// </summary>
/// <param name="command">The command object that is required as per extension method declaration.</param>
/// <param name="cmdRetryPolicy">The command retry policy defining whether to retry a command if it fails while executing.</param>
/// <param name="conRetryPolicy">The connection retry policy defining whether to re-establish a connection if it drops while executing the command.</param>
/// <returns>The number of rows affected.</returns>
public static int ExecuteNonQueryWithRetry(this IDbCommand command, RetryPolicy cmdRetryPolicy, RetryPolicy conRetryPolicy)
{
//GuardConnectionIsNotNull(command);
// Check if retry policy was specified, if not, use the default retry policy.
return (cmdRetryPolicy ?? RetryPolicy.NoRetry).ExecuteAction(() =>
{
var hasOpenConnection = EnsureValidConnection(command, conRetryPolicy);
try
{
return command.ExecuteNonQuery();
}
finally
{
if (hasOpenConnection && command.Connection != null && command.Connection.State == ConnectionState.Open)
{
//Connection is closed in PetaPoco, so no need to do it here (?)
//command.Connection.Close();
}
}
});
}
#endregion
#region ExecuteScalarWithRetry method implementations
/// <summary>
/// Executes the query, and returns the first column of the first row in the result set returned by the query. Additional columns or rows are ignored.
/// Uses the default retry policy when executing the command.
/// </summary>
/// <param name="command">The command object that is required as per extension method declaration.</param>
/// <returns> The first column of the first row in the result set, or a null reference if the result set is empty. Returns a maximum of 2033 characters.</returns>
public static object ExecuteScalarWithRetry(this IDbCommand command)
{
var connectionString = command.Connection.ConnectionString ?? string.Empty;
return ExecuteScalarWithRetry(command, RetryPolicyFactory.GetDefaultSqlCommandRetryPolicyByConnectionString(connectionString));
}
/// <summary>
/// Executes the query, and returns the first column of the first row in the result set returned by the query. Additional columns or rows are ignored.
/// Uses the specified retry policy when executing the command.
/// </summary>
/// <param name="command">The command object that is required as per extension method declaration.</param>
/// <param name="retryPolicy">The retry policy defining whether to retry a command if a connection fails while executing the command.</param>
/// <returns> The first column of the first row in the result set, or a null reference if the result set is empty. Returns a maximum of 2033 characters.</returns>
public static object ExecuteScalarWithRetry(this IDbCommand command, RetryPolicy retryPolicy)
{
var connectionString = command.Connection.ConnectionString ?? string.Empty;
return ExecuteScalarWithRetry(command, retryPolicy, RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(connectionString));
}
/// <summary>
/// Executes the query, and returns the first column of the first row in the result set returned by the query. Additional columns or rows are ignored.
/// Uses the specified retry policy when executing the command. Uses a separate specified retry policy when establishing a connection.
/// </summary>
/// <param name="command">The command object that is required as per extension method declaration.</param>
/// <param name="cmdRetryPolicy">The command retry policy defining whether to retry a command if it fails while executing.</param>
/// <param name="conRetryPolicy">The connection retry policy defining whether to re-establish a connection if it drops while executing the command.</param>
/// <returns> The first column of the first row in the result set, or a null reference if the result set is empty. Returns a maximum of 2033 characters.</returns>
public static object ExecuteScalarWithRetry(this IDbCommand command, RetryPolicy cmdRetryPolicy, RetryPolicy conRetryPolicy)
{
//GuardConnectionIsNotNull(command);
// Check if retry policy was specified, if not, use the default retry policy.
return (cmdRetryPolicy ?? RetryPolicy.NoRetry).ExecuteAction(() =>
{
var hasOpenConnection = EnsureValidConnection(command, conRetryPolicy);
try
{
return command.ExecuteScalar();
}
finally
{
if (hasOpenConnection && command.Connection != null && command.Connection.State == ConnectionState.Open)
{
//Connection is closed in PetaPoco, so no need to do it here (?)
//command.Connection.Close();
}
}
});
}
#endregion
/// <summary>
/// Ensure a valid connection in case a connection hasn't been opened by PetaPoco (which shouldn't be possible by the way).
/// </summary>
/// <param name="command"></param>
/// <param name="retryPolicy"></param>
/// <returns></returns>
private static bool EnsureValidConnection(IDbCommand command, RetryPolicy retryPolicy)
{
if (command != null)
{
//GuardConnectionIsNotNull(command);
// Verify whether or not the connection is valid and is open. This code may be retried therefore
// it is important to ensure that a connection is re-established should it have previously failed.
if (command.Connection.State != ConnectionState.Open)
{
// Attempt to open the connection using the retry policy that matches the policy for SQL commands.
command.Connection.OpenWithRetry(retryPolicy);
return true;
}
}
return false;
}
}
}

View File

@@ -1,6 +1,5 @@
using System.Data;
using Umbraco.Core.Persistence.FaultHandling;
using Umbraco.Core.Persistence.FaultHandling.Strategies;
namespace Umbraco.Core.Persistence
{
@@ -17,11 +16,7 @@ namespace Umbraco.Core.Persistence
public static void OpenWithRetry(this IDbConnection connection)
{
var connectionString = connection.ConnectionString ?? string.Empty;
var retryStrategy = new ExponentialBackoff();
//Is this really the best way to determine if the database is an Azure database?
RetryPolicy retryPolicy = connectionString.Contains("database.windows.net")
? new RetryPolicy(new SqlAzureTransientErrorDetectionStrategy(), retryStrategy)
: new RetryPolicy(new NetworkConnectivityErrorDetectionStrategy(), retryStrategy);
var retryPolicy = RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(connectionString);
OpenWithRetry(connection, retryPolicy);
}

View File

@@ -406,6 +406,7 @@
<Compile Include="Persistence\Migrations\Upgrades\TargetVersionSix\UpdateCmsContentVersionTable.cs" />
<Compile Include="Persistence\Migrations\Upgrades\TargetVersionSix\UpdateCmsPropertyTypeGroupTable.cs" />
<Compile Include="Persistence\Migrations\Upgrades\TargetVersionSix\RenameCmsTabTable.cs" />
<Compile Include="Persistence\PetaPocoCommandExtensions.cs" />
<Compile Include="Persistence\PetaPocoConnectionExtensions.cs" />
<Compile Include="Persistence\PetaPocoExtensions.cs" />
<Compile Include="Persistence\PetaPocoSqlExtensions.cs" />

View File

@@ -6,9 +6,6 @@ using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.SqlSyntax;
using Umbraco.Core.Persistence.UnitOfWork;
using Umbraco.Core.Publishing;
using Umbraco.Core.Services;
using Umbraco.Tests.TestHelpers;
namespace Umbraco.Tests.Persistence

View File

@@ -0,0 +1,38 @@
using System.Data.SqlClient;
using NUnit.Framework;
using Umbraco.Core.Persistence;
namespace Umbraco.Tests.Persistence.FaultHandling
{
[TestFixture, NUnit.Framework.Ignore]
public class ConnectionRetryTest
{
[Test]
public void PetaPocoConnection_Cant_Connect_To_SqlDatabase_With_Invalid_User()
{
// Arrange
const string providerName = "System.Data.SqlClient";
const string connectionString = @"server=.\SQLEXPRESS;database=EmptyForTest;user id=x;password=umbraco";
var factory = new DefaultDatabaseFactory(connectionString, providerName);
var database = factory.CreateDatabase();
//Act
Assert.Throws<SqlException>(
() => database.Fetch<dynamic>("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES"));
}
[Test]
public void PetaPocoConnection_Cant_Connect_To_SqlDatabase_Because_Of_Network()
{
// Arrange
const string providerName = "System.Data.SqlClient";
const string connectionString = @"server=.\SQLEXPRESS;database=EmptyForTest;user id=umbraco;password=umbraco";
var factory = new DefaultDatabaseFactory(connectionString, providerName);
var database = factory.CreateDatabase();
//Act
Assert.Throws<SqlException>(
() => database.Fetch<dynamic>("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES"));
}
}
}

View File

@@ -174,6 +174,7 @@
<Compile Include="Migrations\Upgrades\SqlServerUpgradeTest.cs" />
<Compile Include="Migrations\Upgrades\ValidateOlderSchemaTest.cs" />
<Compile Include="Models\MediaXmlTest.cs" />
<Compile Include="Persistence\FaultHandling\ConnectionRetryTest.cs" />
<Compile Include="Persistence\Mappers\MappingResolverTests.cs" />
<Compile Include="Persistence\Querying\ContentRepositorySqlClausesTest.cs" />
<Compile Include="Persistence\Querying\ContentTypeRepositorySqlClausesTest.cs" />