Long running tasks

Eventually most applications develop some mechanism for launching and tracking the progress of a task running asynchronously.

In Enterprise Tester this is normally seen ss a progress dialog:

In the application this was handled in more then one way, by different parts of the application, but through the API we saw an opportunity to unite these different methods.

API as plaster (spackle for Americans)

Applications over time grow and morph in often unforeseen ways, heading in directions you never originally imagined (an incidentally this is part of the reason why our jobs as developers is so much fun).

The result of this is that you can often end up with multiple features over time, that at first seem very different, but at some point a perception-shift occurs and you realize in fact they are variations on the same feature.

At this point there's a strong desire to try and rectify the issue - but your faced with some problems:

  • It's going to involve lots of work to align everything together.
  • Unless you plan to build further on this feature, it's difficult to justify any increase in value to the business.
  • If you are somewhat pragmatic, you may struggle to justify it internally as well.

But as an alternative to addressing the problem from the bottom up, when adding an API to your product, you also have the option of addressing it an API level - and having the API take care of then delegating to find the appropriate implementation.

This is where the API then behaves as plaster, smoothing over the cracks and small imperfections in your implementation as it is exposed to the world of potential 3rd party developers.

But enough of the hypothetical - let's take a look at what we did for background tasks.

First we introduced a new layer of abstraction:

public interface IJobHandler : IModule
{
string Key { get; }
string Description { get; }
string CreateJob(IDictionary parameters);
ProgressReportDTO GetProgressReport(string jobId);
bool CanHandle(string jobId);
}

This allowed a thin adapter to be created over the top of each background task implementation.

Next - in each implementation of this interface we created a composite key (under the hood most of the task implementations used a GUID Identifier for tracking the progress of the job) which could be used to differentiate the ID of the job from other handlers:

public bool CanHandle(string jobId)
{
if (!jobId.StartsWith(_keyPrefix))
{
return false;
}

if (ExtractId(jobId) == null)
{
return false;
}

return true;
}

The key prefix also has the bonus of allowing our background tasks to be identified by something a little more meaningful then a GUID i.e. "reindex_task_B55C4A97-9731-4907-AF8F-13BB10A01C3A" - a small change, but a pleasant one.

Last of all, each job implementation wraps the progress results from the underlying implementation , mapping them into the a common DTO.

IDictionary AdditionalProperties { get; }
IList Links { get; }

Like we do with other models returned from the API, we leverage dictionaries and JSON rewriting to handle adding additional information to the progress results.

Controller

Just for the sake of completeness, here is the controller action we now use for creating a task:

public HttpResponseMessage Post(CreateBackgroundTask dto)
{
IJobHandler handler = _registry.GetByKey(dto.Type);

string jobId = handler.CreateJob(dto.Parameters);

ProgressReportDTO reportDto = handler.GetProgressReport(jobId);

ViewBackgroundTaskModel wrapped = _viewModelMapper.Map(reportDto, Expands);

HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.Created, wrapped);

response.Headers.Location = new Uri(_urlTransformer.ToAbsolute(string.Format("~/api/backgroundtask/{0}", jobId)));

return response;
}

When creating a new background task, we might get a result like this for example:

{
"Complete": false,
"TotalElements": 0,
"ProcessedElements": 0,
"StartedAt": "2012-08-06T11:28:45Z",
"ProgressInPercent": 0.0,
"Id": "ticketlinking_cba3035a-bf63-4006-89b1-b291aaac0460",
"Message": null,
"Self": "http://localhost/api/backgroundtask/ticketlinking_cba3035a-bf63-4006-89b1-b291aaac0460"
}

We can make additional GET requests to the Self URI to get progress updates, upon completion the response contains additional information (including in this case a link to a new resource that was created as part of the execution of this background task).

{
"Complete": true,
"StartedAt": "2012-08-06T11:39:45Z",
"FinishedAt": "2012-08-06T11:39:53Z",
"ProgressInPercent": 1.0,
"Id": "ticketlinking_9b01796c-a9ae-40cb-a6ad-a802346c0c33",
"Message": "Completed",
"IncidentId": "029b2c43-38be-4c94-b547-a0a50185fb9e",
"Self": "http://localhost/api/backgroundtask/ticketlinking_9b01796c-a9ae-40cb-a6ad-a802346c0c33",
"Links": [
{
"Href": "http://localhost/api/incident/029b2c43-38be-4c94-b547-a0a50185fb9e",
"Rel": "Incident"
}
]
}

What about SignalR

Currently getting progress for a background task is done by polling the resource URL - we did investigate leveraging SignalR to make this work in a more real-time fashion, but struck a few issues:

  • Internally the underlying sources of the progress information didn't support progress change events - so we would still be having to poll internally.
  • Many of our clients would still end up polling because it's simpler to implement
  • The SignalR + WebAPI story wasn't very well developed - we did review the SignalR.AspNetWebApi project on github, but it wasn't being updated at the same pace as the ASP.Net Web API preview releases were hitting github.

We also investigated some other ideas - including PushStreamContent Which is now really easy to implement in the RTM build of WebAPI or trying to leverage WebBackgrounder (but that didn't really fit our needs).

Next

Next in part 7 we are going to take a look at the approach we took to testing our API (including end-to-end testing and Approval Tests).

Written on August 20, 2012