REST

Long running processes and RESTful APIs.

I have to create a RESTful API that can accept a request for a resource that may take some time to find as it’s the result of a complex deterministic model. The ASP.NET Web API is hosted on an Azure Web Role and exposes a RESTful interface to the clients. The clients are not web browsers, in this case, but other applications.

On the initial GET or “fetch” of a particular resource the very long querystring that contains all of the data the model run requires is handled with a 202 Accepted. Then a 304 Not Modified is returned on each subsequent request for the same resource until finally we have the new resource in our server’s cache, placed there by a background process that monitors a queue that in turn is fed by the model engine. Finally we will return the new resource with a 200 OK. As the result already exists, in a way, and we are just having to take some time to fetch it for the client, then the cache is not being updated with a result but rather it is caching the resource from the application.

In REST the resource has an id. In this case the id is the URI or the ETag (the Entity Tag being the hash of the URI). This is not a “process”, like a POST or “insert” of a resource, nothing in the data is being changed, it’s just a very slow request.

Request Response Ping Ping

The initial request immediately returns a 202 Accepted. This is better than holding the connection open for up to the 4 minutes allowed by the Azure Load Balancer as that would be expensive and we would run the risk of overloading our application.

The 202 response carries with it an ETag (Entity Tag) which is the token that the client can use to make another request to the application. An ETag represents a resource and is intended to be used for caching. We are caching our resource and it’s current state is empty.

The client will then present the ETag in the If-None-Match header value. This is the specified way to check for any changes to a resource. If the state of the resource has not changed then the application will return a 304 Not Modified. If the resource has changed, which in practice means the application has completed its run and dumped the result in the cache, then the application will return the current state of the resource. The ETag is also returned, should the client wish to request the resource again, and a 200 OK to indicate the end of the request.

namespace WebRole.Controllers
{
public class MyController : ApiController
{
private readonly ICache cache;
private readonly IMyRequestQueue myRequestQueue;
public MyController(
ICache cache,
IModellerRequestQueue myRequestQueue)
{
this.cache = cache;
this.myRequestQueue = myRequestQueue;
}
public HttpResponseMessage Get([FromUri] MyRequest myRequest)
{
// the client's request is placed on the queue and they receive a
// 202 (Accepted) and an ETag (EntityTag). They poll with the ETag in
// their If-None-Match header. If the resource is not available
// yet they will receive a 304 (Not-Modified) and they must continue
// to poll. When the response is available then it will be written
// into the cache over the value of the original request. Now the ETag
// points to the result. On the next request the client will receive
// a 200 (OK) and their resource.
// try to get the request's ETag.
var clientETag = this.Request.Headers.IfNoneMatch.FirstOrDefault();
// if the If-None-Match header is supplied then
// this is a polling request for the resource.
if (clientETag != null)
{
return this.HandlePollingRequest(clientETag);
}
// if the If-None-Match header was not supplied then
// this is the first request for the resource.
// set an Accepted response message.
var response = new HttpResponseMessage(HttpStatusCode.Accepted);
// set a weak ETag.
var requestUri = this.Request.RequestUri.ToString();
var serverETag = ETag.Create(requestUri);
response.Headers.ETag = new EntityTagHeaderValue(serverETag, true);
// set a "retry after" suggestion.
response.Headers.RetryAfter = new RetryConditionHeaderValue(new TimeSpan(0, 0, 10));
this.cache.StringSet(serverETag, requestUri);
var myRequestDto = this.mapper.Map<PlanRequestDto>(myRequest);
myRequestDto.ETag = serverETag;
// put the work on the queue.
this.myRequestQueue.Client.SendAsync(
new BrokeredMessage(myRequestDto));
return response;
}
private HttpResponseMessage HandlePollingRequest(EntityTagHeaderValue clientETag)
{
HttpResponseMessage response;
var cachedValue = this.cache.StringGet(clientETag.Tag);
if (cachedValue == null)
{
// cache may have expired or
// the client may have an incorrect ETag.
return
this.Request.CreateResponse(
HttpStatusCode.PreconditionFailed,
Errors.NoETag);
}
// get the hash of the value in the cache.
var serverETag = ETag.Create(cachedValue);
// check the request ETag against the value in the cache
// to see if the resource has been updated.
if (clientETag.Tag.Equals(serverETag))
{
// if they match then the resource isn't available yet so
// return 304 (Not-Modified).
response = new HttpResponseMessage(HttpStatusCode.NotModified);
// add the ETag for the next polling request to use.
response.Headers.ETag = clientETag;
// set a retry after suggestion.
response.Headers.RetryAfter = new RetryConditionHeaderValue(new TimeSpan(0, 0, 10));
}
else
{
var myResponseDto = new JavaScriptSerializer().Deserialise<MyResponseDto>(cachedValue);
var myResponse = this.mapper.Map<MyResponse>(myResponseDto);
// the resource is available now so
// the updated resource should be returned.
response = this.Request.CreateResponse(HttpStatusCode.OK, myResponse);
// set the ETag for completeness.
response.Headers.ETag = clientETag;
}
return response;
}
}
public static class ETag
{
public static string Create(string cachedValue)
{
byte[] bytes = Encoding.ASCII.GetBytes(cachedValue);
byte[] hash = SHA256.Create().ComputeHash(bytes);
// ETag must be quoted string.
var builder = new StringBuilder("\"");
foreach (byte t in hash)
{
builder.Append(t.ToString("x2"));
}
builder.Append("\"");
string serverETag = builder.ToString();
return serverETag;
}
}
}
view raw MyController.cs hosted with ❤ by GitHub
namespace MyConsoleApplicationAsync
{
public class Program
{
public static void Main(string[] args)
{
if (args.Length < 2)
{
ShowUsage();
return;
}
MainAsync(args).Wait();
}
private static async Task MainAsync(string[] args)
{
var baseAddress = args[0];
var apiRoute = args[1];
var querystring = args[2];
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(baseAddress);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
// request with querystring.
var response = await client.GetAsync(apiRoute + querystring);
// request has been accepted (the first response) or
// response is not yet available in the server's cache.
while (response.StatusCode.Equals(HttpStatusCode.Accepted) ||
response.StatusCode.Equals(HttpStatusCode.NotModified))
{
// use the suggested timeout from the server.
var retryAfter = response.Headers.RetryAfter.Delta;
if (retryAfter.HasValue)
{
Thread.Sleep(retryAfter.Value);
}
// set the ETag (EntityTag) to check in the server's cache.
client.DefaultRequestHeaders.IfNoneMatch.Clear();
client.DefaultRequestHeaders.IfNoneMatch.Add(
response.Headers.ETag);
// check cache.
response = await client.GetAsync(new Uri(apiRoute));
}
// should be 200 (OK) but may be an error code.
Console.WriteLine(response.StatusCode);
Console.WriteLine(response.ReasonPhrase);
Console.WriteLine(response.Content.ReadAsStringAsync());
}
}
private static void ShowUsage()
{
var usage = new StringBuilder("Call the Web API.");
usage.AppendLine("Usage:");
usage.AppendLine("myApi baseAddress apiRoute querystring");
usage.AppendLine("Example:");
usage.AppendLine("myApi 'http://localhost:8080/&#39; 'api/my' '?age=42&postcode=W1A4WW'");
Console.WriteLine(usage.ToString());
}
}
}
view raw Program.cs hosted with ❤ by GitHub
namespace WorkerRole
{
public class WorkerRole : RoleEntryPoint
{
private readonly IMyResponseQueue responseQueue;
private readonly ICache cache;
public WorkerRole(ICache cache, IMyResponseQueue responseQueue)
{
this.cache = cache;
this.responseQueue = responseQueue;
}
public override void Run()
{
this.responseQueue.Client.OnMessageAsync(
async msg =>
{
this.ProcessMessage(msg);
});
}
private async Task ProcessMessage(BrokeredMessage message)
{
var myResponseDto = message.GetBody<MyResponseDto>();
var serialisedMyResponseDto = new JavaScriptSerializer().Serialise(myResponseDto);
this.cache.StringSet(myResponseDto.ETag, serialisedMyResponseDto);
message.CompleteAsync();
}
}
}
view raw Worker.cs hosted with ❤ by GitHub

Note that the above is only example code that has been stripped of some conditional checks and guard clauses for readability.