0

我正在尝试编写策略。

我希望我的执行代码运行最长时间(示例中为 10 秒)。但我也想重试 x 次(样本中为 3 次)。并在失败之​​间暂停(示例中为 2 秒)。

我已经操纵我的存储过程来人为地延迟测试我的行为。

按照编码(如下代码),我的数据集在 30 秒后被填充(仅供参考:30 秒是存储过程中的硬编码值)。所以我的执行代码在 10 秒后没有退出......

理想情况下,我看到代码在前两次尝试 10 秒后退出,然后在第三次尝试中工作(因为存储过程不会人为延迟)。显然这不是真正的代码,但奇怪的存储过程给了我一种测试行为的方法。

我的存储过程:

USE [Northwind]
GO


/* CREATE */ ALTER PROCEDURE [dbo].[uspWaitAndReturn]
(
    @InvokeDelay bit
)

AS

SET NOCOUNT ON;

if ( @InvokeDelay > 0)
BEGIN
    WAITFOR DELAY '00:00:30';  
END

select top 1 * from dbo.Customers c order by newid()

GO

我的 C#/Polly/数据库代码:

    public DataSet GetGenericDataSet()
    {
        DataSet returnDs = null;

        int maxRetryAttempts = 3; /* retry attempts */
        TimeSpan pauseBetweenFailuresTimeSpan = TimeSpan.FromSeconds(2); /* pause in between failures */
        Policy timeoutAfter10SecondsPolicy = Policy.Timeout(TimeSpan.FromSeconds(10)); /* MAGIC SETTING here, my code inside the below .Execute block below would bail out after 10 seconds */
        Policy retryThreeTimesWith2SecondsInBetweenPolicy = Policy.Handle<Exception>().WaitAndRetry(maxRetryAttempts, i => pauseBetweenFailuresTimeSpan);
        Policy aggregatePolicy = timeoutAfter10SecondsPolicy.Wrap(retryThreeTimesWith2SecondsInBetweenPolicy);

        int attemptCounter = 0; /* used to track the attempt and conditionally set the @InvokeDelay value for the stored procedure */

        aggregatePolicy.Execute(() =>
        {
            try
            {
                attemptCounter++;

                /* Microsoft.Practices.EnterpriseLibrary.Data code */
                ////////DatabaseProviderFactory factory = new DatabaseProviderFactory();
                ////////Database db = factory.CreateDefault();
                ////////DbCommand dbc = db.GetStoredProcCommand("dbo.uspWaitAndReturn");
                ////////dbc.CommandTimeout = 120;
                ////////db.AddInParameter(dbc, "@InvokeDelay", DbType.Boolean, attemptCounter < maxRetryAttempts ? true : false); /* if i'm not on my last attempt, then pass in true to cause the artificial delay */
                ////////DataSet ds;
                ////////ds = db.ExecuteDataSet(dbc);
                ////////returnDs = ds;

                using (SqlConnection conn = new SqlConnection(@"MyConnectionStringValueHere"))
                {
                    SqlCommand sqlComm = new SqlCommand("[dbo].[uspWaitAndReturn]", conn);
                    sqlComm.Parameters.AddWithValue("@InvokeDelay", attemptCounter < maxRetryAttempts ? true : false);
                    sqlComm.CommandType = CommandType.StoredProcedure;

                    SqlDataAdapter da = new SqlDataAdapter();
                    da.SelectCommand = sqlComm;
                    DataSet ds = new DataSet();
                    da.Fill(ds);
                    returnDs = ds;
                }

            }
            catch (Exception ex)
            {
                string temp = ex.Message;
                throw;
            }
        });

        return returnDs;
    }

使用语句:

using System;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;
////    using Microsoft.Practices.EnterpriseLibrary.Data;
using Polly;

版本(packages.config)

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="CommonServiceLocator" version="1.0" targetFramework="net40" />
  <package id="EnterpriseLibrary.Common" version="6.0.1304.0" targetFramework="net45" />
  <package id="EnterpriseLibrary.Data" version="6.0.1304.0" targetFramework="net45" />


  <package id="Polly" version="5.6.1" targetFramework="net45" />


/>
</packages>

附加:

在@mountain traveller 的精彩回答之后,我有一个工作示例:

重点是:

添加了 TimeoutStrategy.Pessimistic

并添加了 DbCommand.Cancel() 调用(或 SqlCommand.Cancel() 如果您不使用企业库)来终止(以前的)命令,否则它们将继续运行(不好)。

我还必须“反转”我的“Policy aggregatePolicy”。

    public DataSet GetGenericDataSet()
    {
        DataSet returnDs = null;

        DbCommand dbc = null; /* increase scope so it can be cancelled */

        int maxRetryAttempts = 3; /* retry attempts */
        TimeSpan pauseBetweenFailuresTimeSpan = TimeSpan.FromSeconds(2); /* pause in between failures */
        Policy timeoutAfter10SecondsPolicy = Policy.Timeout(
            TimeSpan.FromSeconds(10), 
            TimeoutStrategy.Pessimistic,
            (context, timespan, task) =>
            {
                string x = timespan.Seconds.ToString();
                if (null != dbc)
                {
                    dbc.Cancel();
                    dbc = null;
                }
            });

        Policy retryThreeTimesWith2SecondsInBetweenPolicy = Policy.Handle<Exception>().WaitAndRetry(maxRetryAttempts, i => pauseBetweenFailuresTimeSpan);
        ////Policy aggregatePolicy = timeoutAfter10SecondsPolicy.Wrap(retryThreeTimesWith2SecondsInBetweenPolicy);
        Policy aggregatePolicy = retryThreeTimesWith2SecondsInBetweenPolicy.Wrap(timeoutAfter10SecondsPolicy);

        int attemptCounter = 0; /* used to track the attempt and conditionally set the @InvokeDelay value for the stored procedure */

        aggregatePolicy.Execute(() =>
        {
            try
            {
                attemptCounter++;

                /* Microsoft.Practices.EnterpriseLibrary.Data code */
                DatabaseProviderFactory factory = new DatabaseProviderFactory();
                Database db = factory.CreateDefault();
                dbc = db.GetStoredProcCommand("dbo.uspWaitAndReturn");
                dbc.CommandTimeout = 120;
                db.AddInParameter(dbc, "@InvokeDelay", DbType.Boolean, attemptCounter < maxRetryAttempts ? true : false); /* if i'm not on my last attempt, then pass in true to cause the artificial delay */
                DataSet ds;
                ds = db.ExecuteDataSet(dbc);
                returnDs = ds;

                ////////using (SqlConnection conn = new SqlConnection(@"YOUR_VALUE_HERE"))
                ////////{
                ////////    SqlCommand sqlComm = new SqlCommand("[dbo].[uspWaitAndReturn]", conn);
                ////////    sqlComm.Parameters.AddWithValue("@InvokeDelay", attemptCounter < maxRetryAttempts ? true : false);
                ////////    sqlComm.CommandType = CommandType.StoredProcedure;

                ////////    SqlDataAdapter da = new SqlDataAdapter();
                ////////    da.SelectCommand = sqlComm;
                ////////    DataSet ds = new DataSet();
                ////////    da.Fill(ds);
                ////////    returnDs = ds;
                ////////}
            }
            catch (SqlException sqlex)
            {
                switch (sqlex.ErrorCode)
                {
                    case -2146232060:
                        /* I couldn't find a more concrete way to find this specific exception, -2146232060 seems to represent alot of things */
                        if (!sqlex.Message.Contains("cancelled"))
                        {
                            throw;
                        }

                        break;
                    default:
                        throw;
                }
            }
        });

        return returnDs;
    }

Sql Profiler 结果

4

1 回答 1

1

PollyTimeoutPolicy 有两种模式

您正在执行的委托不尊重任何CancellationToken. 因此(对于发布的原始代码),您需要配置要使用的策略TimeoutStrategy.Pessimistic

Policy timeoutAfter10SecondsPolicy = Policy.Timeout(TimeSpan.FromSeconds(10), TimeoutStrategy.Pessimistic);

(在发布的原始代码中,Policy timeoutAfter10SecondsPolicy = Policy.Timeout(TimeSpan.FromSeconds(10));采用TimeoutStrategy.Optimistic,因为这是默认设置。)


以上是为了解释为什么您没有看到所TimeoutPolicy提供的代码中的工作。 也就是说:请注意Polly wiki 中关于悲观超时意味着什么的讨论:它允许调用线程离开等待执行的委托,但不会取消线程/Task运行该委托。所以使用中的 SQL 资源不会在超时时被释放。

为确保 SQL 资源在超时时释放,您需要扩展代码以在超时时取消 SQL 操作。您可以使用SqlCommand.Cancel() 方法,例如此 StackOverflow 答案中所示。PollyTimeoutPolicy 可以配置一个在超时发生时调用的onTimeout委托:即配置此委托以调用适当的SqlCommand.Cancel().

或者,可以将整个代码移至异步方法,并使用SqlCommand.ExecuteReaderAsync(CancellationToken)之类的方法,再加上 Polly驱动的乐观超时CancellationToken。但这是一个更广泛的讨论。

于 2017-11-29T21:32:08.643 回答