Skip to content

Commit

Permalink
improve retry condition & configs
Browse files Browse the repository at this point in the history
* Skip retry in a transactional command
* Skip retry in an unauthorized SQL statements
* Apply a custom transient faults list
* Configurable retry provider factory for pre-defined strategies
  • Loading branch information
DavoudEshtehari committed Oct 6, 2020
1 parent 9b344fc commit f0ada60
Show file tree
Hide file tree
Showing 11 changed files with 328 additions and 175 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,7 @@
<Compile Include="Microsoft\Data\Reliability\Common\SqlRetryingEventArgs.cs" />
<Compile Include="Microsoft\Data\Reliability\Common\SqlRetryLogicBase.cs" />
<Compile Include="Microsoft\Data\Reliability\Common\SqlRetryLogicBaseProvider.cs" />
<Compile Include="Microsoft\Data\Reliability\SqlConfigurableRetryLogicProviders.cs" />
<Compile Include="Microsoft\Data\Reliability\SqlConfigurableRetryFactory.cs" />
<Compile Include="Microsoft\Data\Reliability\Common\SqlRetryIntervalBaseEnumerator.cs" />
<Compile Include="Microsoft\Data\Reliability\SqlRetryIntervalEnumerators.cs" />
<Compile Include="Microsoft\Data\Reliability\Common\SqlRetryLogicProvider.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,35 @@
// See the LICENSE file in the project root for more information.

using System;
using System.Transactions;

namespace Microsoft.Data.SqlClient.Reliability
namespace Microsoft.Data.SqlClient
{
internal class SqlRetryLogic : SqlRetryLogicBase
internal sealed class SqlRetryLogic : SqlRetryLogicBase
{
private const int firstCounter = 1;
private const int firstCounter = 0;

public SqlRetryLogic(int numberOfTries, SqlRetryIntervalBaseEnumerator enumerator, Predicate<Exception> transientPredicate)
public Predicate<string> PreCondition { get; private set; }

public SqlRetryLogic(int numberOfTries,
SqlRetryIntervalBaseEnumerator enumerator,
Predicate<Exception> transientPredicate,
Predicate<string> preCondition)
{
Validate(numberOfTries, enumerator, transientPredicate);

NumberOfTries = numberOfTries;
RetryIntervalEnumerator = enumerator;
TransientPredicate = transientPredicate;
PreCondition = preCondition;
Current = firstCounter;
}

public SqlRetryLogic(int numberOfTries, SqlRetryIntervalBaseEnumerator enumerator, Predicate<Exception> transientPredicate)
: this(numberOfTries, enumerator, transientPredicate, null)
{
}

public SqlRetryLogic(SqlRetryIntervalBaseEnumerator enumerator, Predicate<Exception> transientPredicate = null)
: this(firstCounter, enumerator, transientPredicate ?? (_ => false))
{
Expand Down Expand Up @@ -50,7 +62,8 @@ private void Validate(int numberOfTries, SqlRetryIntervalBaseEnumerator enumerat
public override bool TryNextInterval(out TimeSpan intervalTime)
{
intervalTime = TimeSpan.Zero;
bool result = Current < NumberOfTries;
// First try has occurred before starting the retry process.
bool result = Current < NumberOfTries - 1;

if (result)
{
Expand All @@ -61,5 +74,19 @@ public override bool TryNextInterval(out TimeSpan intervalTime)
}
return result;
}

public override bool RetryCondition(object sender)
{
bool result = true;

if(sender is SqlCommand command)
{
result = Transaction.Current == null // check TransactionScope
&& command.Transaction == null // check SqlTransaction on a SqlCommand
&& (PreCondition == null || PreCondition.Invoke(command.CommandText)); // if it contains an invalid command to retry
}

return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ public abstract class SqlRetryLogicBase
/// </summary>
public Predicate<Exception> TransientPredicate { get; protected set; }

/// <summary>
/// Pre-retry validation regarding to the sender state.
/// </summary>
/// <param name="sender">Sender object</param>
/// <returns>True if the sender is authorized to retry the operation</returns>
public virtual bool RetryCondition(object sender) => true;

/// <summary>
/// Try to get the next interval time by the enumerator if the counter does not exceed from the number of retries.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,28 @@ public abstract class SqlRetryLogicBaseProvider
/// Executes a function with a TResult type.
/// </summary>
/// <typeparam name="TResult">The function return type</typeparam>
/// <param name="sender">Sender object</param>
/// <param name="function">The operaiton is likly be in the retry logic if transient condition happens</param>
/// <returns>A TResult object or an exception</returns>
public abstract TResult Execute<TResult>(Func<TResult> function);
public abstract TResult Execute<TResult>(object sender, Func<TResult> function);

/// <summary>
/// Executes a function with a generic Task and TResult type.
/// </summary>
/// <typeparam name="TResult">Inner function return type</typeparam>
/// <param name="sender">Sender object</param>
/// <param name="function">The operaiton is likly be in the retry logic if transient condition happens</param>
/// <param name="cancellationToken">The cancellation instruction</param>
/// <returns>A task representing TResult or an exception</returns>
public abstract Task<TResult> ExecuteAsync<TResult>(Func<Task<TResult>> function, CancellationToken cancellationToken = default);
public abstract Task<TResult> ExecuteAsync<TResult>(object sender, Func<Task<TResult>> function, CancellationToken cancellationToken = default);

/// <summary>
/// Execute a function with a generic Task type.
/// </summary>
/// <param name="sender">Sender object</param>
/// <param name="function">The operaiton is likly be in the retry logic if transient condition happens</param>
/// <param name="cancellationToken">The cancellation instruction</param>
/// <returns>A Task or an exception</returns>
public abstract Task ExecuteAsync(Func<Task> function, CancellationToken cancellationToken = default);
public abstract Task ExecuteAsync(object sender, Func<Task> function, CancellationToken cancellationToken = default);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,40 +12,38 @@ namespace Microsoft.Data.SqlClient
/// <summary>
/// Apply a retry logic on an operation.
/// </summary>
public abstract class SqlRetryLogicProvider : SqlRetryLogicBaseProvider
internal class SqlRetryLogicProvider : SqlRetryLogicBaseProvider
{
// safety switch for the preview version
private const string EnableRetryLogicSwitch = "Switch.Microsoft.Data.SqlClient.EnableRetryLogic";
private bool EnableRetryLogic = false;
private readonly bool enableRetryLogic = false;

///
public SqlRetryLogicProvider()
public SqlRetryLogicProvider(SqlRetryLogicBase retryLogic)
{
AppContext.TryGetSwitch(EnableRetryLogicSwitch, out EnableRetryLogic);
}

private void OnRetrying(SqlRetryingEventArgs eventArgs)
{
Retrying?.Invoke(this, eventArgs);
AppContext.TryGetSwitch(EnableRetryLogicSwitch, out enableRetryLogic);
RetryLogic = retryLogic;
}

///
public override TResult Execute<TResult>(Func<TResult> function)
public override TResult Execute<TResult>(object sender, Func<TResult> function)
{
var exceptions = new List<Exception>();
RetryLogic.Reset();
retry:
try
{
return function.Invoke();
}
catch (Exception e)
{
if (EnableRetryLogic && RetryLogic.TransientPredicate(e))
if (enableRetryLogic && RetryLogic.RetryCondition(sender) && RetryLogic.TransientPredicate(e))
{
exceptions.Add(e);
if (RetryLogic.TryNextInterval(out TimeSpan intervalTime))
{
ApplyRetryEvent(RetryLogic.Current, intervalTime, exceptions);
// The retrying event raises on each retry.
ApplyRetryingEvent(sender, RetryLogic.Current, intervalTime, exceptions);

// TODO: log the retried execution and the throttled exception
Thread.Sleep(intervalTime);
Expand All @@ -64,22 +62,24 @@ public override TResult Execute<TResult>(Func<TResult> function)
}

///
public override async Task<TResult> ExecuteAsync<TResult>(Func<Task<TResult>> function, CancellationToken cancellationToken = default)
public override async Task<TResult> ExecuteAsync<TResult>(object sender, Func<Task<TResult>> function, CancellationToken cancellationToken = default)
{
var exceptions = new List<Exception>();
RetryLogic.Reset();
retry:
try
{
return await function.Invoke();
}
catch (Exception e)
{
if (EnableRetryLogic && RetryLogic.TransientPredicate(e))
if (enableRetryLogic && RetryLogic.RetryCondition(sender) && RetryLogic.TransientPredicate(e))
{
exceptions.Add(e);
if (RetryLogic.TryNextInterval(out TimeSpan intervalTime))
{
ApplyRetryEvent(RetryLogic.Current, intervalTime, exceptions);
// The retrying event raises on each retry.
ApplyRetryingEvent(sender, RetryLogic.Current, intervalTime, exceptions);

// TODO: log the retried execution and the throttled exception
await Task.Delay(intervalTime, cancellationToken);
Expand All @@ -98,22 +98,24 @@ public override async Task<TResult> ExecuteAsync<TResult>(Func<Task<TResult>> fu
}

///
public override async Task ExecuteAsync(Func<Task> function, CancellationToken cancellationToken = default)
public override async Task ExecuteAsync(object sender, Func<Task> function, CancellationToken cancellationToken = default)
{
var exceptions = new List<Exception>();
RetryLogic.Reset();
retry:
try
{
await function.Invoke();
}
catch (Exception e)
{
if (EnableRetryLogic && RetryLogic.TransientPredicate(e))
if (enableRetryLogic && RetryLogic.RetryCondition(sender) && RetryLogic.TransientPredicate(e))
{
exceptions.Add(e);
if (RetryLogic.TryNextInterval(out TimeSpan intervalTime))
{
ApplyRetryEvent(RetryLogic.Current, intervalTime, exceptions);
// The retrying event raises on each retry.
ApplyRetryingEvent(sender, RetryLogic.Current, intervalTime, exceptions);

// TODO: log the retried execution and the throttled exception
await Task.Delay(intervalTime, cancellationToken);
Expand All @@ -130,35 +132,39 @@ public override async Task ExecuteAsync(Func<Task> function, CancellationToken c
}
}
}

#region private methods

private Exception CreateException(IList<Exception> exceptions, bool manualCancellation = false)
{
// TODO: load the error message from resource file.
string message;
if (manualCancellation)
{
message = $"The retry manually has been canceled by user after {RetryLogic.Current - 1} attempt(s).";
message = $"The retry manually has been canceled by user after {RetryLogic.Current} attempt(s).";
}
else
{
message = $"The number of retries has been exceeded from the maximum {RetryLogic.Current} attempt(s).";
message = $"The number of retries has been exceeded from the maximum {RetryLogic.NumberOfTries} attempt(s).";
}

RetryLogic.Reset();
return new AggregateException(message, exceptions);
}

private void ApplyRetryEvent(int retryCount, TimeSpan intervalTime, List<Exception> exceptions)
private void OnRetrying(object sender, SqlRetryingEventArgs eventArgs) => Retrying?.Invoke(sender, eventArgs);

private void ApplyRetryingEvent(object sender, int retryCount, TimeSpan intervalTime, List<Exception> exceptions)
{
if (Retrying != null)
{
var retryEventArgs = new SqlRetryingEventArgs(retryCount - 1, intervalTime, exceptions);
OnRetrying(retryEventArgs);
OnRetrying(sender, retryEventArgs);
if (retryEventArgs.Cancel)
{
throw CreateException(exceptions, true);
}
}
}
#endregion
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public SqlRetryingEventArgs(int retryCount, TimeSpan delay, IList<Exception> exc

/// <summary>
/// Retry-attempt-number, after the fisrt exception occurrence.
/// It starts from zero.
/// </summary>
public int RetryCount { get; private set; }

Expand Down
Loading

0 comments on commit f0ada60

Please sign in to comment.