简体   繁体   中英

RestSharp - Asynchronous Request Reply Pattern

The following situation is given:

  1. A new job is sent to an API via Post Request. This API returns a JobID and the HTTP ResponseCode 202.

  2. This JobID is then used to request a status endpoint. If the end point has a "Finished" property set in the response body, you can continue with step 3.

  3. The results are queried via a result endpoint using the JobID and can be processed.

My question is how I can solve this elegantly and cleanly. Are there perhaps already ready-to-use libraries that implement exactly this functionality? I could not find such functionality for RestSharp or another HttpClient. The current solution looks like this:

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

}

As @Paulo Morgado said, I should not use Thread.Sleep / Task Delay in production code. But in my opinion I have to use it in the method WaitTillJobIsReady() ? Otherwise I would overwhelm the API with Get Requests in the loop?

What is the best practice for this type of problem?

Long Polling

There are multiple ways you can handle this type of problem, but as others have already pointed out no library such as RestSharp currently has this built in. In my opinion, the preferred way of overcoming this would be to modify the API to support some type of long-polling like Nikita suggested. This is where:

The server holds the request open until new data is available. Once available, the server responds and sends the new information. When the client receives the new information, it immediately sends another request, and the operation is repeated. This effectively emulates a server push feature.

Using a scheduler

Unfortunately this isn't always possible. Another more elegant solution would be to create a service that checks the status, and then using a scheduler such as Quartz.NET or HangFire to schedule the service at reoccurring intervals such as 500ms to 3s until it is successful. Once it gets back the "Finished" property you can then mark the task as complete to stop the process from continuing to poll. This would arguably be better than your current solution and offer much more control and feedback over whats going on.

Using Timers

Aside from using Thread.Sleep a better choice would be to use a Timer . This would allow you to continuously call a delegate at specified intervals, which seems to be what you are wanting to do here.

Below is an example usage of a timer that will run every 2 seconds until it hits 10 runs. (Taken from the 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;
    }
}

Why you don't want to use Thread.Sleep

The reason that you don't want to use Thread.Sleep for operations that you want on a reoccurring schedule is because Thread.Sleep actually relinquishes control and ultimately when it regains control is not up to the thread. It's simply saying it wants to relinquish control of it's remaining time for a least x milliseconds, but in reality it could take much longer for it to regain it.

Per the Microsoft documentation :

The system clock ticks at a specific rate called the clock resolution. The actual timeout might not be exactly the specified timeout, because the specified timeout will be adjusted to coincide with clock ticks. For more information on clock resolution and the waiting time, see the Sleep function from the Windows system APIs.

Peter Ritchie actually wrote an entire blog post on why you shouldn't use Thread.Sleep .

EndNote

Overall I would say your current approach has the appropriate idea on how this should be handled however, you may want to 'future proof' it by doing some refactoring to utilize on of the methods mentioned above.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM