Last active
January 19, 2016 15:02
-
-
Save Boggin/5f31ae869069cfcc2e59 to your computer and use it in GitHub Desktop.
A WebAPI Controller for an Azure WebRole that can manage a request that will take a long time to fulfill.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/' 'api/my' '?age=42&postcode=W1A4WW'"); | |
Console.WriteLine(usage.ToString()); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment