Skip to content

Commit

Permalink
Added support for retrying SQL transient errors. (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
jackliums authored May 7, 2020
1 parent 824da1a commit a4fd3b0
Show file tree
Hide file tree
Showing 25 changed files with 1,092 additions and 18 deletions.
63 changes: 63 additions & 0 deletions THIRDPARTYNOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,69 @@ This file is based on or incorporates material from the projects listed below (T
>
> [ http://www.opensource.org/licenses/bsd-license.php ]>
## Polly 7.2.0
* Component Source: https://github.com/App-vNext/Polly
* Component Copyright and License:
> New BSD License
> =
> Copyright (c) 2015-2018, App vNext
> All rights reserved.
>
> Redistribution and use in source and binary forms, with or without
> modification, are permitted provided that the following conditions are met:
> * Redistributions of source code must retain the above copyright
> notice, this list of conditions and the following disclaimer.
> * Redistributions in binary form must reproduce the above copyright
> notice, this list of conditions and the following disclaimer in the
> documentation and/or other materials provided with the distribution.
> * Neither the name of App vNext nor the
> names of its contributors may be used to endorse or promote products
> derived from this software without specific prior written permission.
>
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
> ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
> WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
> DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
> DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
> (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
> ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
> (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
> SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
## Polly.Contrib.WaitAndRetry
* Component Source: https://github.com/Polly-Contrib/Polly.Contrib.WaitAndRetry
* Component Copyright and License:
> BSD 3-Clause License
>
> Copyright (c) 2019, Polly.Contrib
> All rights reserved.
>
> Redistribution and use in source and binary forms, with or without
> modification, are permitted provided that the following conditions are met:
>
> * Redistributions of source code must retain the above copyright notice, this
> list of conditions and the following disclaimer.
>
> * Redistributions in binary form must reproduce the above copyright notice,
> this list of conditions and the following disclaimer in the documentation
> and/or other materials provided with the distribution.
>
> * Neither the name of the copyright holder nor the names of its
> contributors may be used to endorse or promote products derived from
> this software without specific prior written permission.
>
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
> AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
> IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
> DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
> FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
> DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
> SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
> OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
> OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
## StyleCop.Analyzers 1.1.118
* Component Source: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/
* Component Copyright and License:
Expand Down
10 changes: 5 additions & 5 deletions scripts/Publish-LocalChanges.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Push-Location $RepoRootPath
try {
$branchName = &git rev-parse --abbrev-ref HEAD
$branchNameParts = $branchName.Split("/")
$versionSuffix = "$($branchNameParts[$branchNameParts.Length - 1])-$(Get-Date -Format yyyyMMdd-HHmmss)-preview"
$version = "1.0.0-$($branchNameParts[$branchNameParts.Length - 1])-$(Get-Date -Format yyyyMMdd-HHmmss)-preview"

# Find all projects in the components.
$projects = Get-ChildItem -Recurse -Include *.csproj -Exclude *Tests.csproj $RepoRootPath
Expand All @@ -100,10 +100,10 @@ try {

$projects | ForEach-Object {
if ($SkipBuild) {
&dotnet pack -o $tempPath --no-build --version-suffix $versionSuffix --include-symbols $_.FullName
&dotnet pack -o $tempPath --no-build --include-symbols $_.FullName /p:PackageVersion=$version
}
else {
&dotnet pack -o $tempPath --version-suffix $versionSuffix --include-symbols $_.FullName
&dotnet pack -o $tempPath --include-symbols $_.FullName /p:PackageVersion=$version
}
}

Expand Down Expand Up @@ -142,8 +142,8 @@ try {
$project = [xml](Get-Content $_)

$project.SelectNodes("Project/ItemGroup/PackageReference") |
Where-Object { $_.Include -match "Microsoft.Health.*" -and $_.Include -ne "Microsoft.Health.Extensions.BuildTimeCodeGenerator" } |
ForEach-Object { $_.Version = "1.0.0-$versionSuffix" }
Where-Object { $_.Include -match "Microsoft.Health.*" } |
ForEach-Object { $_.Version = $version }

$writer = New-Object System.IO.StreamWriter($_, $false, $utf8WithBom)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System.Data.SqlClient;
using Microsoft.Health.SqlServer.Extensions;
using Xunit;

namespace Microsoft.Health.SqlServer.UnitTests.Extensions
{
public class SqlExceptionExtensionsTests
{
[Theory]
[InlineData(10928)]
[InlineData(10929)]
[InlineData(10053)]
[InlineData(10054)]
[InlineData(10060)]
[InlineData(18401)]
[InlineData(40197)]
[InlineData(40540)]
[InlineData(40613)]
[InlineData(40143)]
[InlineData(233)]
[InlineData(64)]
public void GivenATransientException_WhenCheckedIfExceptionIsTransient_ThenTrueShouldBeReturned(int number)
{
SqlException sqlException = SqlExceptionFactory.Create(number);

Assert.True(sqlException.IsTransient());
}

[Fact]
public void GivenANonTransientException_WhenCheckedIfExceptionIsTransient_ThenFalseShouldBeReturned()
{
SqlException sqlException = SqlExceptionFactory.Create(10001);

Assert.False(sqlException.IsTransient());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System;
using System.Data;
using System.Data.SqlClient;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Health.SqlServer.Extensions;
using Microsoft.Health.SqlServer.Features.Client;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Polly;
using Xunit;

namespace Microsoft.Health.SqlServer.UnitTests.Features.Client
{
public class RetrySqlCommandWrapperTests
{
private static readonly CancellationToken DefaultCancellationToken = new CancellationTokenSource().Token;

private readonly SqlCommandWrapper _sqlCommandWrapper = Substitute.For<SqlCommandWrapper>(new SqlCommand());
private readonly IAsyncPolicy _asyncPolicy = Policy
.Handle<SqlException>(sqlException => sqlException.IsTransient())
.WaitAndRetryAsync(new TimeSpan[] { TimeSpan.Zero, TimeSpan.Zero, TimeSpan.Zero });

private readonly RetrySqlCommandWrapper _retrySqlCommandWrapper;

public RetrySqlCommandWrapperTests()
{
_retrySqlCommandWrapper = new RetrySqlCommandWrapper(_sqlCommandWrapper, _asyncPolicy);
}

[Fact]
public async Task GivenATransientException_WhenNonQueryIsExecuted_ThenItShouldRetry()
{
_sqlCommandWrapper.ExecuteNonQueryAsync(DefaultCancellationToken).Throws(CreateTransientException());

await ExecuteAndValidateExecuteNonQueryAsync(4);
}

[Fact]
public async Task GivenANonTransientException_WhenNonQueryIsExecuted_ThenItShouldNotRetry()
{
_sqlCommandWrapper.ExecuteNonQueryAsync(DefaultCancellationToken).Returns<int>(
_ => throw CreateTransientException(),
_ => throw CreateNonTransientException());

await ExecuteAndValidateExecuteNonQueryAsync(2);
}

private async Task ExecuteAndValidateExecuteNonQueryAsync(int expectedNumberOfCalls)
{
await Assert.ThrowsAsync<SqlException>(() => _retrySqlCommandWrapper.ExecuteNonQueryAsync(DefaultCancellationToken));

await _sqlCommandWrapper.Received(expectedNumberOfCalls).ExecuteNonQueryAsync(DefaultCancellationToken);
}

[Fact]
public async Task GivenATransientException_WhenReaderIsExecuted_ThenItShouldRetry()
{
_sqlCommandWrapper.ExecuteReaderAsync(DefaultCancellationToken).Throws(CreateTransientException());

await ExecuteAndValidateExecuteReaderAsync(4);
}

[Fact]
public async Task GivenANonTransientException_WhenReaderIsExecuted_ThenItShouldNotRetry()
{
_sqlCommandWrapper.ExecuteReaderAsync(DefaultCancellationToken).Returns<SqlDataReader>(
_ => throw CreateNonTransientException());

await ExecuteAndValidateExecuteReaderAsync(1);
}

private async Task ExecuteAndValidateExecuteReaderAsync(int expectedNumberOfCalls)
{
await Assert.ThrowsAsync<SqlException>(() => _retrySqlCommandWrapper.ExecuteReaderAsync(DefaultCancellationToken));

await _sqlCommandWrapper.Received(expectedNumberOfCalls).ExecuteReaderAsync(DefaultCancellationToken);
}

[Fact]
public async Task GivenATransientException_WhenReaderWithBehaviorIsExecuted_ThenItShouldRetry()
{
CommandBehavior behavior = CommandBehavior.SingleRow;

_sqlCommandWrapper.ExecuteReaderAsync(behavior, DefaultCancellationToken).Throws(CreateTransientException());

await ExecuteAndValidateExecuteReaderAsync(behavior, 4);
}

[Fact]
public async Task GivenANonTransientException_WhenReaderWithBehaviorIsExecuted_ThenItShouldNotRetry()
{
CommandBehavior behavior = CommandBehavior.SchemaOnly;

_sqlCommandWrapper.ExecuteReaderAsync(behavior, DefaultCancellationToken).Returns<SqlDataReader>(
_ => throw CreateNonTransientException());

await ExecuteAndValidateExecuteReaderAsync(behavior, 1);
}

private async Task ExecuteAndValidateExecuteReaderAsync(CommandBehavior behavior, int expectedNumberOfCalls)
{
await Assert.ThrowsAsync<SqlException>(() => _retrySqlCommandWrapper.ExecuteReaderAsync(behavior, DefaultCancellationToken));

await _sqlCommandWrapper.Received(expectedNumberOfCalls).ExecuteReaderAsync(behavior, DefaultCancellationToken);
}

[Fact]
public async Task GivenATransientException_WhenScalarIsExecuted_ThenItShouldRetry()
{
_sqlCommandWrapper.ExecuteScalarAsync(DefaultCancellationToken).Throws(CreateTransientException());

await ExecuteAndValidateExecuteScalarAsync(4);
}

[Fact]
public async Task GivenANonTransientException_WhenScalarIsExecuted_ThenItShouldNotRetry()
{
_sqlCommandWrapper.ExecuteScalarAsync(DefaultCancellationToken).Returns(
_ => throw CreateTransientException(),
_ => throw CreateTransientException(),
_ => throw CreateNonTransientException());

await ExecuteAndValidateExecuteScalarAsync(3);
}

private async Task ExecuteAndValidateExecuteScalarAsync(int expectedNumberOfCalls)
{
await Assert.ThrowsAsync<SqlException>(() => _retrySqlCommandWrapper.ExecuteScalarAsync(DefaultCancellationToken));

await _sqlCommandWrapper.Received(expectedNumberOfCalls).ExecuteScalarAsync(DefaultCancellationToken);
}

private static SqlException CreateTransientException()
=> SqlExceptionFactory.Create(10928);

private static SqlException CreateNonTransientException()
=> SqlExceptionFactory.Create(50404);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Threading.Tasks;
using Microsoft.Health.SqlServer.Configs;
using Microsoft.Health.SqlServer.Features.Client;
using NSubstitute;
using Polly;
using Xunit;

namespace Microsoft.Health.SqlServer.UnitTests.Features.Client
{
public class SqlServerTransientFaultRetryPolicyFactoryTests
{
private readonly SqlServerDataStoreConfiguration _sqlServerDataStoreConfiguration = new SqlServerDataStoreConfiguration();
private readonly IPollyRetryLoggerFactory _pollyRetryLoggerFactory = Substitute.For<IPollyRetryLoggerFactory>();

private readonly SqlServerTransientFaultRetryPolicyFactory _sqlServerTransientFaultRetryPolicyFactory;

private readonly IAsyncPolicy _asyncPolicy;
private readonly List<TimeSpan> _capturedRetries = new List<TimeSpan>();

public SqlServerTransientFaultRetryPolicyFactoryTests()
{
_sqlServerDataStoreConfiguration.TransientFaultRetryPolicy = new SqlServerTransientFaultRetryPolicyConfiguration()
{
InitialDelay = TimeSpan.FromMilliseconds(200),
RetryCount = 3,
Factor = 3,
FastFirst = true,
};

Action<Exception, TimeSpan, int, Context> onRetryCapture = (exception, sleepDuration, retryCount, _) =>
{
_capturedRetries.Add(sleepDuration);
};

_pollyRetryLoggerFactory.Create().Returns(onRetryCapture);

_sqlServerTransientFaultRetryPolicyFactory = new SqlServerTransientFaultRetryPolicyFactory(
_sqlServerDataStoreConfiguration,
_pollyRetryLoggerFactory);

_asyncPolicy = _sqlServerTransientFaultRetryPolicyFactory.Create();
}

[Fact]
public async Task GivenATransientException_WhenRetryPolicyIsUsed_ThenItShouldRetry()
{
await Assert.ThrowsAsync<SqlException>(() =>
_asyncPolicy.ExecuteAsync(() => Task.Run(() => throw SqlExceptionFactory.CreateTransientException())));

ValidateCapturedRetries();
}

[Fact]
public async Task GivenANonTransientException_WhenRetryPolicyIsUsed_ThenItShouldNotRetry()
{
await Assert.ThrowsAsync<SqlException>(() =>
_asyncPolicy.ExecuteAsync(() => Task.Run(() => throw SqlExceptionFactory.CreateNonTransientException())));

Assert.Empty(_capturedRetries);
}

[Fact]
public async Task GivenATimeoutException_WhenRetryPolicyIsUsed_ThenItShouldRetry()
{
await Assert.ThrowsAsync<TimeoutException>(() =>
_asyncPolicy.ExecuteAsync(() => Task.Run(() => throw new TimeoutException())));

ValidateCapturedRetries();
}

[Fact]
public async Task GivenOtherException_WhenRetryPolicyIsUsed_ThenItShouldNotRetry()
{
await Assert.ThrowsAsync<Exception>(() =>
_asyncPolicy.ExecuteAsync(() => Task.Run(() => throw new Exception())));

Assert.Empty(_capturedRetries);
}

private void ValidateCapturedRetries()
{
Assert.Collection(
_capturedRetries,
item => Assert.Equal(TimeSpan.Zero, item),
item => Assert.Equal(TimeSpan.FromMilliseconds(200), item),
item => Assert.Equal(TimeSpan.FromMilliseconds(600), item));
}
}
}
Loading

0 comments on commit a4fd3b0

Please sign in to comment.