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:
@@ -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)];
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
152
src/Umbraco.Core/Persistence/PetaPocoCommandExtensions.cs
Normal file
152
src/Umbraco.Core/Persistence/PetaPocoCommandExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user