How can I POST an HTTP request and wait for a callback without blocking the current thread?

3.5k views Asked by At

We have a .NET application that uses an HTTP based API where we POST a request to a third party HTTP endpoint (that is not under our control) and it calls us back at a later time on an HTTP endpoint that we give it; something like:

WebRequest request = WebRequest.Create(urlToMethod);
request.Method = @"POST";
request.Headers.Add(@"Callback", "http://ourserver?id="+id ); 

We make thousands upon thousands of these calls and so we'd like to be as effecient as possible (in terms of speed/memory/threads etc.)

As far as the callback code is concerned, we have a type that acts as a listener; this is how we start it up:

_httpListener = new HttpListener();
_httpListener.Prefixes.Add(ourServer);
_httpListener.Start();
_httpListener.BeginGetContext(callback, null);

When the server calls us back, it hits our callback method which looks something like this:

HttpListenerContext context = _httpListener.EndGetContext(result);

HttpListenerResponse httpListenerResponse = context.Response;
httpListenerResponse.StatusCode = 200;
httpListenerResponse.ContentLength64 = _acknowledgementBytes.Length;

var output = httpListenerResponse.OutputStream;
output.Write(_acknowledgementBytes, 0, _acknowledgementBytes.Length);

context.Response.Close();

var handler = ResponseReceived;

if (handler != null)
{
    handler(this, someData);
}

So we have a single instance of this listener (_which internally uses HttpListener) and for every response it gets, it informs all of the subscribers on the ResponseReceived event.

The subscribers (possibly hundreds of them) only care about data associated with their particular id. The subscribers look something like:

_matchingResponseReceived = new ManualResetEventSlim(false);
_listener.WhenResponseReceived += checkTheIdOfWhatWeGetAndSetTheEventIfItMatches;
postTheMessage();
_matchingResponseReceived.Wait(someTimeout);

It's that last line that's bugging me. We post the message but then block the whole thread waiting for the Listener to get a response and call our event handler. We'd like to use Tasks but doesn't seem like it'll give us much if we're blocking a whole thread waiting for the callback.

Is there a better (more TPL friendly) way of achieving this so that no threads are blocked and we get fire off more requests simultaneously?

2

There are 2 answers

1
svick On BEST ANSWER

async-await together with TaskCompletionSource pretty much were made for this.

The sender side creates a TaskCompletionSource, adds it to a dictionary (with key being the id of the request), makes the request and returns the TaskCompletionSource's Task.

The receiver then looks into the dictionary to find the right TaskCompletionSource, removes it from there and sets its result.

The caller of the sender method will await the returned Task, which will asynchronously wait for the receiver to process the callback call.

In code, it could look something like this:

// TODO: this probably needs to be thread-safe
// you can use ConcurrentDictionary for that
Dictionary<int, TaskCompletionSource<Result>> requestTcses;

public async Task<Result> MakeApiRequestAsync()
{
   int id = …;
   var tcs = new TaskCompletionSource<Result>();
   requestTcses.Add(id, tcs);
   await SendRequestAsync(id);
   return await tcs.Task;
}

…

var result = await MakeApiRequest();
var context = await _httpListener.GetContext();

// parse the response into id and result

var tcs = requestTcses[id];
requestTcses.Remove(id);
tcs.SetResult(result);
1
Shay___ On

This whole architecture seems to be more complicated than it should be (I might have not understood your program right). Why not post your request to the second server (BTW, you don't need string literal for "POST") and end the routine, then get the request from that server in a regular Web API method, parse the data to find the IDs, and execute thread for each ID?