RestSharp - 异步请求-响应模式

6
给出以下情况:
1. 通过Post请求将新工作发送到API。该API返回JobID和HTTP响应代码202。 2. 然后使用该JobID请求状态终端点。如果响应正文中设置了“已完成”属性,则可以继续执行步骤3。 3. 使用JobID查询结果终端点并可进行处理。
我的问题是如何优雅、干净地解决这个问题。是否已经有现成的库实现了这个功能?我在RestSharp或其他HttpClient中找不到这样的功能。当前的解决方案如下:
async Task<string> PostNewJob()
{
  var restClient = new RestClient("https://baseUrl/");
  var restRequest = new RestRequest("jobs");        
  //add headers
           
  var response = await restClient.ExecutePostTaskAsync(restRequest);
  string jobId = JsonConvert.DeserializeObject<string>(response.Content);
  return jobId;
}

async Task WaitTillJobIsReady(string jobId)
{
  string jobStatus = string.Empty;
  var request= new RestRequest(jobId) { Method = Method.GET };  
  do
  {
    if (!String.IsNullOrEmpty(jobStatus))
        Thread.Sleep(5000); //wait for next status update

    var response = await restClient.ExecuteGetTaskAsync(request, CancellationToken.None);
    jobStatus = JsonConvert.DeserializeObject<string>(response.Content);
  } while (jobStatus != "finished");
}

async Task<List<dynamic>> GetJobResponse(string jobID)
{
  var restClient = new RestClient(@"Url/bulk/" + jobID);
  var restRequest = new RestRequest(){Method = Method.GET};
  var response =  await restClient.ExecuteGetTaskAsync(restRequest, CancellationToken.None);

  dynamic downloadResponse = JsonConvert.DeserializeObject(response.Content);
  var responseResult = new List<dynamic>() { downloadResponse?.ToList() };
  return responseResult;

}

async main()
{

var jobId = await PostNewJob();
WaitTillJobIsReady(jobID).Wait();
var responseResult = await GetJobResponse(jobID);

//handle result

}

正如@Paulo Morgado所说,我不应该在生产代码中使用Thread.Sleep / Task Delay。但是在我看来,我必须在方法WaitTillJobIsReady()中使用它吗?否则,在循环中使用Get请求会使API过载?

在这种情况下,最佳实践是什么?


在异步代码中不要使用Thread.Sleep(...),而应该使用await Task.Delay(...)。在生产代码中,两者都不应该使用。 - Paulo Morgado
使用 HttpClient 替代 RestSharp。 - Paulo Morgado
1
基本上你所做的已经很好了 - 这被称为轮询(不是长轮询,因为你没有保持一个流打开)。是的,你可能需要稍微重构一下,但是思路是正确的,至少我所知道的库都没有提供这个功能。你可以在这里看到类似的东西 - https://solores-software.net/Post/csharp-HttpClient-wrapper-for-asynchronous-REST-resources - Nikita Chayka
非常感谢您的回答! - user8606929
1
很惊讶没有人提到 Polly。它是一个非常有用的库,用于实现重试逻辑。它有各种用途,但其中最简单的之一是处理响应并指定是否重试以及等待多长时间再进行重试。话虽如此,就像 Nikita 说的那样,轮询也是可以的,而且在我看来通常是最简单的解决方案。Task.Delay 会将资源释放给线程池,而不像 Thread.Sleep,因此尽可能使用 Task.Delay。 - Timothy Jannace
显示剩余2条评论
1个回答

6

长轮询

处理这种类型的问题有多种方法,但正如其他人已经指出的那样,目前没有像RestSharp这样的库内置此功能。在我看来,克服这个问题的首选方法是修改API,支持像Nikita建议的长轮询。这就是:

服务器保持请求处于打开状态,直到有新数据可用。一旦有新数据可用,服务器会响应并发送新信息。当客户端接收到新信息时,它立即发送另一个请求,操作重复进行。这有效地模拟了服务器推送功能。

使用调度程序

很不幸,这并非总是可能的。另一个更优雅的解决方案是创建一个检查状态的服务,然后使用调度程序比如Quartz.NETHangFire在重复的时间间隔(如500毫秒至3秒)安排服务,直到成功为止。一旦获取到"Finished"属性,可以将任务标记为完成,停止继续轮询。这比你当前的解决方案更好,并提供更多控制和反馈。
使用计时器
除了使用Thread.Sleep之外,更好的选择是使用Timer。这将允许您在指定的时间间隔内连续调用委托,这似乎是您想要在这里做的。下面是一个计时器的示例用法,它将每2秒运行一次,直到达到10次运行为止。(来自Microsoft documentation
using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    private static Timer timer;

    static void Main(string[] args)
    {
        var timerState = new TimerState { Counter = 0 };

        timer = new Timer(
            callback: new TimerCallback(TimerTask),
            state: timerState,
            dueTime: 1000,
            period: 2000);

        while (timerState.Counter <= 10)
        {
            Task.Delay(1000).Wait();
        }

        timer.Dispose();
        Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}: done.");
    }

    private static void TimerTask(object timerState)
    {
        Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}: starting a new callback.");
        var state = timerState as TimerState;
        Interlocked.Increment(ref state.Counter);
    }

    class TimerState
    {
        public int Counter;
    }
}

为什么不要使用Thread.Sleep

你不想在需要定期执行的操作中使用Thread.Sleep的原因是因为Thread.Sleep实际上会放弃控制权,而最终重新获得控制权的时间并不由线程决定。它只是简单地表示希望放弃其剩余时间的控制权至少x毫秒,但实际上可能需要更长的时间才能获得控制权。

根据Microsoft文档

系统时钟以称为时钟分辨率的特定速率滴答。 实际超时时间可能不完全符合指定的超时时间,因为 指定的超时时间将被调整为与时钟滴答一致。 有关时钟分辨率和等待时间的更多信息,请参见Windows系统API中的Sleep函数。

Peter Ritchie实际上写了一篇关于为什么不应该使用Thread.Sleep的博客文章

结语

总体而言,我认为你目前的方法在处理这个问题方面有适当的想法,但是,你可能需要进行一些重构来利用上述方法之一来“未雨绸缪”。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接