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 multiple retry issue on ExecuteScalarAsync + add missing ConfigureAwait calls #1220

Merged
merged 2 commits into from
Aug 20, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -2575,9 +2575,9 @@ protected override Task<DbDataReader> ExecuteDbDataReaderAsync(CommandBehavior b
throw result.Exception.InnerException;
}
return result.Result;
},
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.NotOnCanceled,
},
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.NotOnCanceled,
TaskScheduler.Default
);
}
Expand Down Expand Up @@ -2679,13 +2679,9 @@ private void SetCachedCommandExecuteReaderAsyncContext(ExecuteReaderAsyncCallCon
}

/// <include file='../../../../../../../doc/snippets/Microsoft.Data.SqlClient/SqlCommand.xml' path='docs/members[@name="SqlCommand"]/ExecuteScalarAsync[@name="CancellationToken"]/*'/>
public override Task<object> ExecuteScalarAsync(CancellationToken cancellationToken)
=> IsRetryEnabled ?
InternalExecuteScalarWithRetryAsync(cancellationToken) :
InternalExecuteScalarAsync(cancellationToken);

private Task<object> InternalExecuteScalarWithRetryAsync(CancellationToken cancellationToken)
=> RetryLogicProvider.ExecuteAsync(this, () => InternalExecuteScalarAsync(cancellationToken), cancellationToken);
public override Task<object> ExecuteScalarAsync(CancellationToken cancellationToken) =>
// Do not use retry logic here as internal call to ExecuteReaderAsync handles retry logic.
InternalExecuteScalarAsync(cancellationToken);

private Task<object> InternalExecuteScalarAsync(CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -3733,13 +3729,13 @@ private SqlDataReader GetParameterEncryptionDataReader(out Task returnTask, Task
// Read the results of describe parameter encryption.
command.ReadDescribeEncryptionParameterResults(describeParameterEncryptionDataReader, describeParameterEncryptionRpcOriginalRpcMap);

#if DEBUG
#if DEBUG
// Failpoint to force the thread to halt to simulate cancellation of SqlCommand.
if (_sleepAfterReadDescribeEncryptionParameterResults)
{
Thread.Sleep(10000);
}
#endif
#endif
}
catch (Exception e)
{
Expand Down Expand Up @@ -4897,7 +4893,7 @@ private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavi
private Task RunExecuteReaderTdsSetupContinuation(RunBehavior runBehavior, SqlDataReader ds, string optionSettings, Task writeTask)
{
Task task = AsyncHelper.CreateContinuationTaskWithState(
task: writeTask,
task: writeTask,
state: _activeConnection,
onSuccess: (object state) =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3106,14 +3106,10 @@ private Task<SqlDataReader> InternalExecuteReaderAsync(CommandBehavior behavior,
return returnedTask;
}

private Task<object> InternalExecuteScalarWithRetryAsync(CancellationToken cancellationToken)
=> RetryLogicProvider.ExecuteAsync(this, () => InternalExecuteScalarAsync(cancellationToken), cancellationToken);

/// <include file='../../../../../../../doc/snippets/Microsoft.Data.SqlClient/SqlCommand.xml' path='docs/members[@name="SqlCommand"]/ExecuteScalarAsync[@name="CancellationToken"]/*'/>
public override Task<object> ExecuteScalarAsync(CancellationToken cancellationToken)
=> IsRetryEnabled ?
InternalExecuteScalarWithRetryAsync(cancellationToken) :
InternalExecuteScalarAsync(cancellationToken);
public override Task<object> ExecuteScalarAsync(CancellationToken cancellationToken) =>
// Do not use retry logic here as internal call to ExecuteReaderAsync handles retry logic.
InternalExecuteScalarAsync(cancellationToken);

private Task<object> InternalExecuteScalarAsync(CancellationToken cancellationToken)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
</Compile>
<Compile Include="DataCommon\AADUtility.cs" />
<Compile Include="SQL\ConfigurableIpPreferenceTest\ConfigurableIpPreferenceTest.cs" />
<Compile Include="SQL\RetryLogic\RetryLogicCounterTest.cs" />
<Compile Include="SQL\RetryLogic\RetryLogicConfigHelper.cs" />
<Compile Include="SQL\RetryLogic\RetryLogicTestHelper.cs" />
<Compile Include="SQL\RetryLogic\SqlCommandReliabilityTest.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Threading;
using System.Threading.Tasks;
using Xunit;

namespace Microsoft.Data.SqlClient.ManualTesting.Tests
{
public class RetryLogicCounterTest
{
[ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))]
[InlineData("ExecuteScalarAsync", 3)]
[InlineData("ExecuteReaderAsync", 3)]
[InlineData("ExecuteXmlReaderAsync", 3)]
[InlineData("ExecuteNonQueryAsync", 3)]
public async void ValidateRetryCount_SqlCommand_Async(string methodName, int numOfTries)
{
RetryLogicTestHelper.SetRetrySwitch(true);

ErrorInfoRetryLogicProvider _errorInfoRetryProvider = new(
SqlConfigurableRetryFactory.CreateFixedRetryProvider(new SqlRetryLogicOption()
{ NumberOfTries = numOfTries, TransientErrors = new[] { 50000 } }));

using var connection = new SqlConnection(DataTestUtility.TCPConnectionString);
connection.Open();

using SqlCommand cmd = connection.CreateCommand();
cmd.RetryLogicProvider = _errorInfoRetryProvider;
cmd.CommandText = "THROW 50000,'Error',0";

_errorInfoRetryProvider.CallCounter = 0;
try
{
switch (methodName)
{
case "ExecuteScalarAsync":
await cmd.ExecuteScalarAsync();
break;
case "ExecuteReaderAsync":
{
using var _ = await cmd.ExecuteReaderAsync();
cheenamalhotra marked this conversation as resolved.
Show resolved Hide resolved
break;
}
case "ExecuteXmlReaderAsync":
{
using var _ = await cmd.ExecuteXmlReaderAsync();
cheenamalhotra marked this conversation as resolved.
Show resolved Hide resolved
break;
}
case "ExecuteNonQueryAsync":
await cmd.ExecuteNonQueryAsync();
break;
default:
break;
}
Assert.False(true, "Exception did not occur.");
}
catch
{
Assert.Equal(numOfTries, _errorInfoRetryProvider.CallCounter);
}
RetryLogicTestHelper.SetRetrySwitch(false);
cheenamalhotra marked this conversation as resolved.
Show resolved Hide resolved
}

[ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))]
[InlineData("ExecuteScalar", 3)]
[InlineData("ExecuteReader", 3)]
[InlineData("ExecuteXmlReader", 3)]
[InlineData("ExecuteNonQuery", 3)]
public void ValidateRetryCount_SqlCommand_Sync(string methodName, int numOfTries)
{
RetryLogicTestHelper.SetRetrySwitch(true);

ErrorInfoRetryLogicProvider _errorInfoRetryProvider = new(
SqlConfigurableRetryFactory.CreateFixedRetryProvider(new SqlRetryLogicOption()
{ NumberOfTries = numOfTries, TransientErrors = new[] { 50000 } }));

using var connection = new SqlConnection(DataTestUtility.TCPConnectionString);
connection.Open();

using SqlCommand cmd = connection.CreateCommand();
cmd.RetryLogicProvider = _errorInfoRetryProvider;
cmd.CommandText = "THROW 50000,'Error',0";

_errorInfoRetryProvider.CallCounter = 0;
try
{
switch (methodName)
{
case "ExecuteScalar":
cmd.ExecuteScalar();
break;
case "ExecuteReader":
{
using var _ = cmd.ExecuteReader();
cheenamalhotra marked this conversation as resolved.
Show resolved Hide resolved
break;
}
case "ExecuteXmlReader":
{
using var _ = cmd.ExecuteXmlReader();
cheenamalhotra marked this conversation as resolved.
Show resolved Hide resolved
break;
}
case "ExecuteNonQuery":
cmd.ExecuteNonQuery();
break;
default:
break;
}
Assert.False(true, "Exception did not occur.");
}
catch
{
Assert.Equal(numOfTries, _errorInfoRetryProvider.CallCounter);
}
RetryLogicTestHelper.SetRetrySwitch(false);
}

public class ErrorInfoRetryLogicProvider : SqlRetryLogicBaseProvider
{
public SqlRetryLogicBaseProvider InnerProvider { get; }

public ErrorInfoRetryLogicProvider(SqlRetryLogicBaseProvider innerProvider)
{
InnerProvider = innerProvider;
}

readonly AsyncLocal<int> _depth = new();
public int CallCounter = 0;

TResult LogCalls<TResult>(Func<TResult> function)
{
CallCounter++;
return function();
}

public override TResult Execute<TResult>(object sender, Func<TResult> function)
{
_depth.Value++;
try
{
return InnerProvider.Execute(sender, () => LogCalls(function));
}
finally
{
_depth.Value--;
}
}

public async Task<TResult> LogCallsAsync<TResult>(Func<Task<TResult>> function)
{
CallCounter++;
return await function();
}

public override async Task<TResult> ExecuteAsync<TResult>(object sender, Func<Task<TResult>> function,
CancellationToken cancellationToken = default)
{
_depth.Value++;
try
{
return await InnerProvider.ExecuteAsync(sender, () => LogCallsAsync(function), cancellationToken);
}
finally
{
_depth.Value--;
}
}

public async Task LogCallsAsync(Func<Task> function)
{
CallCounter++;
await function();
}

public override async Task ExecuteAsync(object sender, Func<Task> function,
CancellationToken cancellationToken = default)
{
_depth.Value++;
try
{
await InnerProvider.ExecuteAsync(sender, () => LogCallsAsync(function), cancellationToken);
}
finally
{
_depth.Value--;
}
}
}
}
}