Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix begin transaction retries #257

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 12 additions & 46 deletions source/Nevermore/Transient/DbCommandExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ internal static class DbCommandExtensions
{
public static int ExecuteNonQueryWithRetry(this DbCommand command, RetryPolicy commandRetryPolicy, RetryPolicy connectionRetryPolicy = null, string operationName = "ExecuteNonQuery")
{
GuardConnectionIsNotNull(command);
RetryUtil.GuardConnectionIsNotNull(command.Connection);
var effectiveCommandRetryPolicy = (commandRetryPolicy ?? RetryPolicy.NoRetry).LoggingRetries(operationName);
return effectiveCommandRetryPolicy.ExecuteAction(() =>
{
var weOwnTheConnectionLifetime = EnsureValidConnection(command, connectionRetryPolicy);
var weOwnTheConnectionLifetime = RetryUtil.EnsureValidConnection(command.Connection, connectionRetryPolicy);
try
{
return command.ExecuteNonQuery();
Expand All @@ -29,11 +29,11 @@ public static int ExecuteNonQueryWithRetry(this DbCommand command, RetryPolicy c

public static Task<int> ExecuteNonQueryWithRetryAsync(this DbCommand command, RetryPolicy commandRetryPolicy, RetryPolicy connectionRetryPolicy = null, string operationName = "ExecuteNonQueryAsync", CancellationToken cancellationToken = default)
{
GuardConnectionIsNotNull(command);
RetryUtil.GuardConnectionIsNotNull(command.Connection);
var effectiveCommandRetryPolicy = (commandRetryPolicy ?? RetryPolicy.NoRetry).LoggingRetries(operationName);
return effectiveCommandRetryPolicy.ExecuteAction(async () =>
{
var weOwnTheConnectionLifetime = await EnsureValidConnectionAsync(command, connectionRetryPolicy, cancellationToken).ConfigureAwait(false);
var weOwnTheConnectionLifetime = await RetryUtil.EnsureValidConnectionAsync(command.Connection, connectionRetryPolicy, cancellationToken).ConfigureAwait(false);
try
{
return await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
Expand All @@ -48,11 +48,11 @@ public static Task<int> ExecuteNonQueryWithRetryAsync(this DbCommand command, Re

public static DbDataReader ExecuteReaderWithRetry(this DbCommand command, RetryPolicy commandRetryPolicy, CommandBehavior behavior = CommandBehavior.Default, RetryPolicy connectionRetryPolicy = null, string operationName = "ExecuteReader")
{
GuardConnectionIsNotNull(command);
RetryUtil.GuardConnectionIsNotNull(command.Connection);
var effectiveCommandRetryPolicy = (commandRetryPolicy ?? RetryPolicy.NoRetry).LoggingRetries(operationName);
return effectiveCommandRetryPolicy.ExecuteAction(() =>
{
var weOwnTheConnectionLifetime = EnsureValidConnection(command, connectionRetryPolicy);
var weOwnTheConnectionLifetime = RetryUtil.EnsureValidConnection(command.Connection, connectionRetryPolicy);
try
{
return command.ExecuteReader(behavior);
Expand All @@ -69,12 +69,12 @@ public static DbDataReader ExecuteReaderWithRetry(this DbCommand command, RetryP

public static async Task<DbDataReader> ExecuteReaderWithRetryAsync(this DbCommand command, RetryPolicy commandRetryPolicy, CommandBehavior commandBehavior, CancellationToken cancellationToken, RetryPolicy connectionRetryPolicy = null, string operationName = "ExecuteReader")
{
GuardConnectionIsNotNull(command);
RetryUtil.GuardConnectionIsNotNull(command.Connection);
var effectiveCommandRetryPolicy =
(commandRetryPolicy ?? RetryPolicy.NoRetry).LoggingRetries(operationName);
return await effectiveCommandRetryPolicy.ExecuteActionAsync(async () =>
{
var weOwnTheConnectionLifetime = await EnsureValidConnectionAsync(command, connectionRetryPolicy, cancellationToken).ConfigureAwait(false);
var weOwnTheConnectionLifetime = await RetryUtil.EnsureValidConnectionAsync(command.Connection, connectionRetryPolicy, cancellationToken).ConfigureAwait(false);
try
{
return await command.ExecuteReaderAsync(commandBehavior, cancellationToken).ConfigureAwait(false);
Expand All @@ -91,11 +91,11 @@ public static async Task<DbDataReader> ExecuteReaderWithRetryAsync(this DbComman

public static object ExecuteScalarWithRetry(this DbCommand command, RetryPolicy commandRetryPolicy, RetryPolicy connectionRetryPolicy = null, string operationName = "ExecuteScalar")
{
GuardConnectionIsNotNull(command);
RetryUtil.GuardConnectionIsNotNull(command.Connection);
var effectiveCommandRetryPolicy = (commandRetryPolicy ?? RetryPolicy.NoRetry).LoggingRetries(operationName);
return effectiveCommandRetryPolicy.ExecuteAction(() =>
{
var weOwnTheConnectionLifetime = EnsureValidConnection(command, connectionRetryPolicy);
var weOwnTheConnectionLifetime = RetryUtil.EnsureValidConnection(command.Connection, connectionRetryPolicy);
try
{
return command.ExecuteScalar();
Expand All @@ -110,11 +110,11 @@ public static object ExecuteScalarWithRetry(this DbCommand command, RetryPolicy

public static async Task<object> ExecuteScalarWithRetryAsync(this DbCommand command, RetryPolicy commandRetryPolicy, RetryPolicy connectionRetryPolicy = null, string operationName = "ExecuteScalar", CancellationToken cancellationToken = default)
{
GuardConnectionIsNotNull(command);
RetryUtil.GuardConnectionIsNotNull(command.Connection);
var effectiveCommandRetryPolicy = (commandRetryPolicy ?? RetryManager.Instance.GetDefaultSqlCommandRetryPolicy()).LoggingRetries(operationName);
return await effectiveCommandRetryPolicy.ExecuteActionAsync(async () =>
{
var weOwnTheConnectionLifetime = await EnsureValidConnectionAsync(command, connectionRetryPolicy, cancellationToken).ConfigureAwait(false);
var weOwnTheConnectionLifetime = await RetryUtil.EnsureValidConnectionAsync(command.Connection, connectionRetryPolicy, cancellationToken).ConfigureAwait(false);
try
{
return await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
Expand All @@ -126,39 +126,5 @@ public static async Task<object> ExecuteScalarWithRetryAsync(this DbCommand comm
}
}).ConfigureAwait(false);
}

static void GuardConnectionIsNotNull(DbCommand command)
{
if (command.Connection == null)
throw new InvalidOperationException("Connection property has not been initialized.");
}

/// <summary>
/// Ensures the command either has an existing open connection, or we will open one for it.
/// </summary>
/// <returns>True if we opened the connection (indicating we own its lifetime), False if the connection was already open (indicating someone else owns its lifetime)</returns>
static bool EnsureValidConnection(DbCommand command, RetryPolicy retryPolicy)
{
if (command == null) return false;

GuardConnectionIsNotNull(command);

if (command.Connection.State == ConnectionState.Open) return false;

command.Connection.OpenWithRetry(retryPolicy);
return true;
}

static async Task<bool> EnsureValidConnectionAsync(DbCommand command, RetryPolicy retryPolicy, CancellationToken cancellationToken)
{
if (command == null) return false;

GuardConnectionIsNotNull(command);

if (command.Connection.State == ConnectionState.Open) return false;

await command.Connection.OpenWithRetryAsync(retryPolicy, cancellationToken).ConfigureAwait(false);
return true;
}
}
}
45 changes: 45 additions & 0 deletions source/Nevermore/Transient/RetryUtil.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;
using System.Data;
using System.Data.Common;
using System.Threading;
using System.Threading.Tasks;

namespace Nevermore.Transient
{
internal static class RetryUtil
{
public static void GuardConnectionIsNotNull(DbConnection connection)
{
if (connection == null)
throw new InvalidOperationException("Connection property has not been initialized.");
}

/// <summary>
/// Ensures the command either has an existing open connection, or we will open one for it.
/// </summary>
/// <returns>True if we opened the connection (indicating we own its lifetime), False if the connection was already open (indicating someone else owns its lifetime)</returns>
public static bool EnsureValidConnection(DbConnection connection, RetryPolicy retryPolicy)
{
if (connection == null) return false;

GuardConnectionIsNotNull(connection);

if (connection.State == ConnectionState.Open) return false;

connection.OpenWithRetry(retryPolicy);
return true;
}

public static async Task<bool> EnsureValidConnectionAsync(DbConnection connection, RetryPolicy retryPolicy, CancellationToken cancellationToken)
{
if (connection == null) return false;

GuardConnectionIsNotNull(connection);

if (connection.State == ConnectionState.Open) return false;

await connection.OpenWithRetryAsync(retryPolicy, cancellationToken).ConfigureAwait(false);
return true;
}
}
}
7 changes: 5 additions & 2 deletions source/Nevermore/Transient/TransactionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.Data.Common;
using Microsoft.Data.SqlClient;


namespace Nevermore.Transient
{
public static class TransactionExtensions
Expand All @@ -14,7 +13,11 @@ public static DbTransaction BeginTransactionWithRetry(this SqlConnection connect

public static DbTransaction BeginTransactionWithRetry(this SqlConnection connection, IsolationLevel isolationLevel, string sqlServerTransactionName, RetryPolicy retryPolicy)
{
return (retryPolicy ?? RetryPolicy.NoRetry).LoggingRetries("Beginning Database Transaction").ExecuteAction(() => connection.BeginTransaction(isolationLevel, sqlServerTransactionName));
return (retryPolicy ?? RetryPolicy.NoRetry).LoggingRetries("Beginning Database Transaction").ExecuteAction(() =>
{
RetryUtil.EnsureValidConnection(connection, retryPolicy);
return connection.BeginTransaction(isolationLevel, sqlServerTransactionName);
});
}
}
}