Polly:如何结合TimeoutPolicy和RetryPolicy请求功能

如何解决Polly:如何结合TimeoutPolicy和RetryPolicy请求功能

我正在尝试将TimeoutPolicyRetryPolicy结合在一起以进行Func中的API调用,但是我没有找到一种方法来实现这一目标。

如果我仅使用RetryPolicy,就可以正常工作。

我有一个GetRequest方法,该方法调用HttpClient并返回数据:

async Task<Data> GetRequest(string api,int id)
{
    var httpClient = new HttpClient();
    var response = await httpClient.GetAsync($"{api}{id}");

    var rawResponse = await response.Content.ReadAsStringAsync();
    return JsonConvert.DeserializeObject<Data>(rawResponse);
}

我也有Func可以将调用嵌入到此方法中: var func = new Func<Task<Data>>(() => GetRequest(api,i));

我这样称呼服务: Results.Add(await _networkService.RetryWithoutTimeout<Data>(func,3,OnRetry));

RetryWithoutTimeout方法如下:

async Task<T> RetryWithoutTimeout<T>(Func<Task<T>> func,int retryCount = 1,Func<Exception,int,Task> onRetry = null)
{
    var onRetryInner = new Func<Exception,Task>((e,i) =>
    {
        return Task.Factory.StartNew(() => {
#if DEBUG
            System.Diagnostics.Debug.WriteLine($"Retry #{i} due to exception '{(e.InnerException ?? e).Message}'");
#endif
        });
    });

    return await Policy.Handle<Exception>()
                        .RetryAsync(retryCount,onRetry ?? onRetryInner)
                        .ExecuteAsync<T>(func);
}

我已使用新的TimeoutPolicy方法将此代码更新为使用RetryWithTimeout

async Task<T> RetryWithTimeout<T>(Func<Task<T>> func,Task> onRetry = null,int timeoutDelay = 30)
{
    var onRetryInner = new Func<Exception,i) =>
    {
        return Task.Factory.StartNew(() => {
#if DEBUG
            System.Diagnostics.Debug.WriteLine($"Retry #{i} due to exception '{(e.InnerException ?? e).Message}'");
#endif
        });
    });

    var retryPolicy = Policy
        .Handle<Exception>()
        .RetryAsync(retryCount,onRetry ?? onRetryInner);

    var timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromSeconds(timeoutDelay));

    var policyWrap = timeoutPolicy.WrapAsync((IAsyncPolicy)retryPolicy);

    return await policyWrap.ExecuteAsync(
                    async ct => await Task.Run(func),CancellationToken.None
                    );
}

但是我看不到如何管理GetRequest()方法:我所有的测试都失败了...

编辑: 我已经基于@Peter Csala评论创建了一个示例。

因此,第一,我刚刚更新了重试次数,以检查retryPolicy是否正确应用:

private const int TimeoutInMilliseconds = 2500;
private const int MaxRetries = 3;
private static int _times;

static async Task Main(string[] args)
{
    try
    {
        await RetryWithTimeout(TestStrategy,MaxRetries);
    }
    catch (Exception ex)
    {
        WriteLine($"{nameof(Main)} - Exception - Failed due to: {ex.Message}");
    }
    Console.ReadKey();
}

private static async Task<string> TestStrategy(CancellationToken ct)
{
    WriteLine($"{nameof(TestStrategy)} has been called for the {_times++}th times.");
    await Task.Delay(TimeoutInMilliseconds * 2,ct);
    return "Finished";
}

internal static async Task<T> RetryWithTimeout<T>(Func<CancellationToken,Task<T>> func,int timeoutDelay = TimeoutInMilliseconds)
{
    WriteLine($"NetworkService - {nameof(RetryWithTimeout)}");
    var onRetryInner = new Func<Exception,i) =>
    {
        WriteLine($"NetworkService - {nameof(RetryWithTimeout)} #{i} due to exception '{(e.InnerException ?? e).Message}'");
        return Task.CompletedTask;
    });

    var retryPolicy = Policy
        .Handle<Exception>()
        .RetryAsync(retryCount,onRetry ?? onRetryInner);

    var timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromMilliseconds(timeoutDelay));

    var policyWrap = Policy.WrapAsync(retryPolicy,timeoutPolicy); //Important part #1

    return await policyWrap.ExecuteAsync(
                    async ct => await func(ct),//Important part #2
                    CancellationToken.None);
}

关于日志,情况很好:

NetworkService - RetryWithTimeout
TestStrategy has been called for the 0th times.
NetworkService - RetryWithTimeout - Retry #1 due to exception 'A task was canceled.'
TestStrategy has been called for the 1th times.
NetworkService - RetryWithTimeout - Retry #2 due to exception 'A task was canceled.'
TestStrategy has been called for the 2th times.
NetworkService - RetryWithTimeout - Retry #3 due to exception 'A task was canceled.'
TestStrategy has been called for the 3th times.
Main - TimeoutRejectedException - Failed due to: The delegate executed asynchronously through TimeoutPolicy did not complete within the timeout.

然后,由于我需要全局超时,因此我更改了policyWrap

private static async Task<string> TestStrategy(CancellationToken ct)
{
    WriteLine($"{nameof(TestStrategy)} has been called for the {_times++}th times.");
    await Task.Delay(1500,ct);
    throw new Exception("simulate Exception");
}

var policyWrap = timeoutPolicy.WrapAsync(retryPolicy);

关于日志,这也是正确的:

TestStrategy has been called for the 0th times.
NetworkService - RetryWithTimeout #1 due to exception 'simulate Exception'
TestStrategy has been called for the 1th times.
NetworkService - RetryWithTimeout #2 due to exception 'A task was canceled.'
Main - TimeoutRejectedException - Failed due to: The delegate executed asynchronously through TimeoutPolicy did not complete within the timeout.

之后,我实现了一种调用API的方法,该方法带有一些Exceptions,以更贴近我的需求:

static async Task Main(string[] args)
{
    try
    {
        await RetryWithTimeout(GetClientAsync,MaxRetries);
    }
    catch (TimeoutRejectedException trEx)
    {
        WriteLine($"{nameof(Main)} - TimeoutRejectedException - Failed due to: {trEx.Message}");
    }
    catch (WebException wEx)
    {
        WriteLine($"{nameof(Main)} - WebException - Failed due to: {wEx.Message}");
    }
    catch (Exception ex)
    {
        WriteLine($"{nameof(Main)} - Exception - Failed due to: {ex.Message}");
    }
    Console.ReadKey();
}

private static async Task<CountriesResponse> GetClientAsync(CancellationToken ct)
{
    WriteLine($"{nameof(GetClientAsync)} has been called for the {_times++}th times.");
    HttpClient _client = new HttpClient();
    try
    {
        var response = await _client.GetAsync(apiUri,ct);
        // ! The server response is faked through a Proxy and returns 500 answer !
        if (!response.IsSuccessStatusCode)
        {
            WriteLine($"{nameof(GetClientAsync)} - !response.IsSuccessStatusCode");
            throw new WebException($"No success status code {response.StatusCode}");
        }
        var rawResponse = await response.Content.ReadAsStringAsync();
        WriteLine($"{nameof(GetClientAsync)} - Finished");
        return JsonConvert.DeserializeObject<CountriesResponse>(rawResponse);
    }
    catch (TimeoutRejectedException trEx)
    {
        WriteLine($"{nameof(GetClientAsync)} - TimeoutRejectedException : {trEx.Message}");
        throw trEx;
    }
    catch (WebException wEx)
    {
        WriteLine($"{nameof(GetClientAsync)} - WebException: {wEx.Message}");
        throw wEx;
    }
    catch (Exception ex)
    {
        WriteLine($"{nameof(GetClientAsync)} - other exception: {ex.Message}");
        throw ex;
    }
}

日志仍然正确:

NetworkService - RetryWithTimeout
GetClientAsync has been called for the 0th times.
GetClientAsync - !response.IsSuccessStatusCode
GetClientAsync - WebException: No success status code InternalServerError
NetworkService - RetryWithTimeout #1 due to exception 'No success status code InternalServerError'
GetClientAsync has been called for the 1th times.
GetClientAsync - !response.IsSuccessStatusCode
GetClientAsync - WebException: No success status code InternalServerError
NetworkService - RetryWithTimeout #2 due to exception 'No success status code InternalServerError'
GetClientAsync has been called for the 2th times.
GetClientAsync - !response.IsSuccessStatusCode
GetClientAsync - WebException: No success status code InternalServerError
NetworkService - RetryWithTimeout #3 due to exception 'No success status code InternalServerError'
GetClientAsync has been called for the 3th times.
GetClientAsync - other exception: The operation was canceled.
Main - TimeoutRejectedException - Failed due to: The delegate executed asynchronously through TimeoutPolicy did not complete within the timeout.

最后,我希望能够调用一个“通用”方法,该方法可以在每个API调用中重复使用。此方法将如下所示:

static async Task<T> ProcessGetRequest<T>(Uri uri,CancellationToken ct)
{
    WriteLine("ApiService - ProcessGetRequest()");

    HttpClient _client = new HttpClient();

    var response = await _client.GetAsync(uri);
    if (!response.IsSuccessStatusCode)
    {
        WriteLine("ApiService - ProcessGetRequest() - !response.IsSuccessStatusCode");
        throw new WebException($"No success status code {response.StatusCode}");
    }
    var rawResponse = await response.Content.ReadAsStringAsync();

    return JsonConvert.DeserializeObject<T>(rawResponse);
}

但是,为此,我必须同时通过CancellationToken传递Uri和Api RetryWithTimeout,但我不知道如何管理它。

我试图通过以下方式更改RetryWithTimeout的签名:

internal static async Task<T> RetryWithTimeout<T>(Func<Uri,CancellationToken,int timeoutDelay = TimeoutInMilliseconds)

但是我找不到如何管理Func ...

您有想法或解释吗?

解决方法

您需要将CancellationToken传递到要取消的(由于超时)功能。

因此,假设您具有以下简化方法:

private const int TimeoutInMilliseconds = 1000;
private static int _times;
private static async Task<string> TestStrategy(CancellationToken ct)
{
    Console.WriteLine($"{nameof(TestStrategy)} has been called for the {_times++}th times.");
    await Task.Delay(TimeoutInMilliseconds * 2,ct);
    return "Finished";
}

因此,您的RetryWithTimeout可以像这样调整/修改:

static async Task<T> RetryWithTimeout<T>(Func<CancellationToken,Task<T>> func,int retryCount = 1,Func<Exception,int,Task> onRetry = null,int timeoutDelay = TimeoutInMilliseconds)
{
    var onRetryInner = new Func<Exception,Task>((e,i) =>
    {
        Console.WriteLine($"Retry #{i} due to exception '{(e.InnerException ?? e).Message}'");
        return Task.CompletedTask;
    });

    var retryPolicy = Policy
        .Handle<Exception>()
        .RetryAsync(retryCount,onRetry ?? onRetryInner);

    var timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromMilliseconds(timeoutDelay));

    var policyWrap = Policy.WrapAsync(retryPolicy,timeoutPolicy); //Important part #1

    return await policyWrap.ExecuteAsync(
                    async ct => await func(ct),//Important part #2
                    CancellationToken.None);
}

重要的部分#1-重试是外部的,超时是内部的策略
重要部分#2-由于超时,CancellationToken传递给了要取消的功能

以下用法

static async Task Main(string[] args)
{
    try
    {
        await RetryWithTimeout(TestStrategy);
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Failed due to: {ex.Message}");
    }

    Console.ReadKey();
}

将产生以下输出:

TestStrategy has been called for the 0th times.
Retry #1 due to exception 'A task was canceled.'
TestStrategy has been called for the 1th times.
Failed due to: The delegate executed asynchronously through TimeoutPolicy did not complete within the timeout.

请记住,重试开始之前有0次尝试。

,

我终于找到了一个可行的解决方案,该解决方案已由@Peter Csala完成。

[
    {
        "NODE": "rabbit@server567","EXCLUSIVE": false,"NAME": "test-01","SYNCHRONISED_SLAVE_NODES": [],"SLAVE_NODES": [],"AUTO_DELETE": false,"VHOST": "/","ARGUMENTS": {},"TYPE": "classic","DURABLE": false
    },{
        "NODE": "rabbit@server567","NAME": "test-02","NAME": "test-03",{
        "MESSAGES_UNACKNOWLEDGED_RAM": 0,"RECOVERABLE_SLAVES": null,"CONSUMERS": 0,"REDUCTIONS": 9700519,"MESSAGE_BYTES_PAGED_OUT": 0,"MESSAGE_BYTES_UNACKNOWLEDGED": 0,"REDUCTIONS_DETAILS": {
            "RATE": 0.0
        },"MESSAGE_BYTES": 0,"MESSAGES_UNACKNOWLEDGED": 0,"CONSUMER_UTILISATION": null,"GARBAGE_COLLECTION": {
            "MAX_HEAP_SIZE": 0,"MIN_HEAP_SIZE": 233,"FULLSWEEP_AFTER": 65535,"MINOR_GCS": 15635,"MIN_BIN_VHEAP_SIZE": 46422
        },"MESSAGES_DETAILS": {
            "RATE": 0.0
        },"SLAVE_NODES": [
            "rabbit@server567"
        ],"MESSAGE_BYTES_PERSISTENT": 0,"POLICY": "ha-all","MESSAGES_PAGED_OUT": 0,"NODE": "rabbit@server566","HEAD_MESSAGE_TIMESTAMP": null,"DURABLE": false,"MESSAGES_READY_RAM": 0,"STATE": "running","EFFECTIVE_POLICY_DEFINITION": {
            "HA-MODE": "all"
        },"MESSAGES_READY": 0,"MESSAGES_RAM": 0,"MESSAGE_BYTES_READY": 0,"SINGLE_ACTIVE_CONSUMER_TAG": null,"NAME": "test-04","MESSAGES_PERSISTENT": 0,"BACKING_QUEUE_STATUS": {
            "MIRROR_SENDERS": 0,"Q1": 0,"Q3": 0,"Q2": 0,"Q4": 0,"AVG_ACK_EGRESS_RATE": 0.0,"MIRROR_SEEN": 0,"LEN": 0,"TARGET_RAM_COUNT": "infinity","MODE": "default","NEXT_SEQ_ID": 0,"DELTA": [
                "delta","undefined","undefined"
            ],"AVG_ACK_INGRESS_RATE": 0.0,"AVG_EGRESS_RATE": 0.0,"AVG_INGRESS_RATE": 0.0
        },"MESSAGES": 0,"IDLE_SINCE": "2020-10-16 13:50:50","OPERATOR_POLICY": null,"SYNCHRONISED_SLAVE_NODES": [
            "rabbit@server567"
        ],"MEMORY": 10556,"EXCLUSIVE_CONSUMER_TAG": null,"MESSAGES_READY_DETAILS": {
            "RATE": 0.0
        },"MESSAGES_UNACKNOWLEDGED_DETAILS": {
            "RATE": 0.0
        },"MESSAGE_BYTES_RAM": 0
    }
]

我得到这些结果:

private const int TimeoutInMilliseconds = 2500;
private const int MaxRetries = 3;

private static Uri apiUri = new Uri("https://api/param");
private static int _times;

public static Country[] Countries
{
    get;
    set;
}

static async Task Main(string[] args)
{
    try
    {
        await LoadCountriesWithRetry(false);
    }
    catch (Exception ex)
    {
        WriteLine($"{nameof(Main)} - Exception - Failed due to: {ex.Message}");
    }
    Console.ReadKey();
}

static async Task LoadCountriesWithRetry(bool shouldWaitAndRetry)
{
    WriteLine($"{nameof(LoadCountriesWithRetry)}");
    try
    {
        Countries = await GetCountriesWithRetry();
    }
    catch (TimeoutRejectedException trE)
    {
        WriteLine($"{nameof(LoadCountriesWithRetry)} - TimeoutRejectedException : {trE.Message}");
    }   
    catch (WebException wE)
    {
        WriteLine($"{nameof(LoadCountriesWithRetry)} - WebException : {wE.Message}");
    }
    catch (Exception e)
    {
        WriteLine($"{nameof(LoadCountriesWithRetry)} - Exception : {e.Message}");
    }
}

public static async Task<Country[]> GetCountriesWithRetry()
{
    WriteLine($"{nameof(GetCountriesWithRetry)}");
    var response = await GetAndRetry<CountriesResponse>(uri,MaxRetries);
    return response?.Countries;
}

static Func<CancellationToken,Task<T>> IssueRequest<T>(Uri uri) => ct => ProcessGetRequest<T>(ct,uri);

public static async Task<T> GetAndRetry<T>(Uri uri,int retryCount,Task> onRetry = null)
    where T : class
{
    WriteLine($"{nameof(GetAndRetry)}");
    return await RetryWithTimeout<T>(IssueRequest<T>(uri),retryCount);
}

static async Task<T> ProcessGetRequest<T>(CancellationToken ct,Uri uri)
{
    WriteLine($"{nameof(ProcessGetRequest)}");
    HttpClient _client = new HttpClient();
    var response = await _client.GetAsync(uri,ct);
    if (!response.IsSuccessStatusCode)
    {
        WriteLine($"{nameof(ProcessGetRequest)} - !response.IsSuccessStatusCode");
        throw new WebException($"No success status code {response.StatusCode}");
    }
    var rawResponse = await response.Content.ReadAsStringAsync();
    WriteLine($"{nameof(ProcessGetRequest)} - Success");
    return JsonConvert.DeserializeObject<T>(rawResponse);
}

internal static async Task<T> RetryWithTimeout<T>(Func<CancellationToken,Uri uri,int timeoutDelay = TimeoutInMilliseconds)
{
    WriteLine($"{nameof(RetryWithTimeout)}");
    var onRetryInner = new Func<Exception,i) =>
    {
        WriteLine($"{nameof(RetryWithTimeout)} - onRetryInner #{i} due to exception '{(e.InnerException ?? e).Message}'");
        return Task.CompletedTask;
    });

    var retryPolicy = Policy
        .Handle<Exception>()
        .RetryAsync(retryCount,onRetry ?? onRetryInner);

    var timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromMilliseconds(timeoutDelay));

    var policyWrap = timeoutPolicy.WrapAsync(retryPolicy);

    return await policyWrap.ExecuteAsync(
                    async (ct) => await func(ct),CancellationToken.None);
}

=>重试API调用,直到出现超时

谢谢@Peter Csala!

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


依赖报错 idea导入项目后依赖报错,解决方案:https://blog.csdn.net/weixin_42420249/article/details/81191861 依赖版本报错:更换其他版本 无法下载依赖可参考:https://blog.csdn.net/weixin_42628809/a
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下 2021-12-03 13:33:33.927 ERROR 7228 [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPL
错误1:gradle项目控制台输出为乱码 # 解决方案:https://blog.csdn.net/weixin_43501566/article/details/112482302 # 在gradle-wrapper.properties 添加以下内容 org.gradle.jvmargs=-Df
错误还原:在查询的过程中,传入的workType为0时,该条件不起作用 &lt;select id=&quot;xxx&quot;&gt; SELECT di.id, di.name, di.work_type, di.updated... &lt;where&gt; &lt;if test=&qu
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct redisServer’没有名为‘server_cpulist’的成员 redisSetCpuAffinity(server.server_cpulist); ^ server.c: 在函数‘hasActiveC
解决方案1 1、改项目中.idea/workspace.xml配置文件,增加dynamic.classpath参数 2、搜索PropertiesComponent,添加如下 &lt;property name=&quot;dynamic.classpath&quot; value=&quot;tru
删除根组件app.vue中的默认代码后报错:Module Error (from ./node_modules/eslint-loader/index.js): 解决方案:关闭ESlint代码检测,在项目根目录创建vue.config.js,在文件中添加 module.exports = { lin
查看spark默认的python版本 [root@master day27]# pyspark /home/software/spark-2.3.4-bin-hadoop2.7/conf/spark-env.sh: line 2: /usr/local/hadoop/bin/hadoop: No s
使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-