Article
Eduard Lebedyuk · Dec 7, 2017 3m read

Asynchronous REST

In this article I'd like to discuss asynchronous REST and approaches to implementing it.

Why do we need asynchronous REST? Simply put - answering the request takes too much time. While most requests usually can be satisfied immediately, some can't. The reasons are varied:

  • You need to perform time-consuming calculations
  • Performing action actually takes time (for example container creation)
  • etc.

The solution to these problems is asynchronous REST. Asynchronous REST works by separating request and real response. Here's an example, let's consider the following simple async REST broker:


/// Basic async REST example.
Class Utils.AsyncREST Extends %CSP.REST
{

Parameter CONTENTTYPE = "application/json";

Parameter CHARSET = "UTF-8";

Parameter UseSession As BOOLEAN = 1;

Parameter HandleCorsRequest = 1;

Parameter GLVN = "^AsyncREST";

XData UrlMap [ XMLNamespace = "http://www.intersystems.com/urlmap" ]
{
<Routes>
  <Route Url="/StartTask" Method="GET" Call="StartTask"/>
  <Route Url="/GetTask/:TaskId" Method="GET" Call="GetTask"/>
</Routes>
}

/// Get info about task
ClassMethod GetTask(TaskId As %Integer) As %Status
{
    Set PercentDone = ..TaskStatus(TaskId)
    
    If PercentDone = 100 {
        Set Result = ..GetCompletedTask(TaskId)
    } Else {
        Set Result = {"TaskId" : (+TaskId), "PercentDone":(PercentDone), "Alive":($Data(^$JOB(TaskId)))}
    }
    
    Write Result.%ToJSON()
    Quit $$$OK
}

/// Start new task and return result immediately
ClassMethod StartTask() As %Status
{
    Job ..Task()
    Write {"TaskId" : ($zchild)}.%ToJSON()
    Quit $$$OK
}

/// Actually do task
ClassMethod Task()
{
    Set Wait = 0
    Set Increase = 1
    While Wait<=10 {
        Hang Increase
        Do ..LogTaskStatus( Wait * 10)
        Set Wait = Wait + Increase
            
    }
    
    Do ..LogTaskData("Some result")
}

/// Set current task progression
ClassMethod LogTaskStatus(PercentDone As %Integer(MINVAL=0,MAXVAL=100))
{
    Set:PercentDone>100 PercentDone = 100
    Set:PercentDone<0 PercentDone = 0
    
    Set @..#GLVN@($job) = PercentDone
    If PercentDone = 0 {
        Set @..#GLVN@($job, "Start") = $zdt($h, 3, 1, 3)
    } ElseIf PercentDone = 100 {
        Set @..#GLVN@($job, "End") = $zdt($h, 3, 1, 3)
    }
}

/// Specify data = task output
ClassMethod LogTaskData(Data)
{
    Set @..#GLVN@($job, "Data") = Data
}

/// Get current task progression
ClassMethod TaskStatus(TaskId As %Integer) [ CodeMode = expression ]
{
@..#GLVN@(TaskId)
}

/// Get info about completed task
ClassMethod GetCompletedTask(TaskId As %Integer) As %DynamicObject [ CodeMode = expression ]
{
{
    "TaskId": (+TaskId),
    "PercentDone": 100,
    "Start": (@..#GLVN@(TaskId, "Start")),
    "End": (@..#GLVN@(TaskId, "End")),
    "Data": (@..#GLVN@(TaskId, "Data")),
    "IsJobAlive": ($Data(^$JOB(TaskId)))
}
}

/// Remove complete task
ClassMethod ClearTask(TaskId As %Integer)
{
    Kill @..#GLVN@(TaskId)
}

}

Here's how it works (assume our broker is set as a broker for /asyncREST web application). You can get the code here)

First we start the task  http://localhost:57772/asyncREST/StartTask
We get response immediately with task identifier:

{"TaskId":12608}

Next we check if task is complete http://localhost:57772/asyncREST/GetTask/12608

{
  "TaskId": 12608,
  "PercentDone": 20,
  "Alive": 1
}

When task is done we get data at http://localhost:57772/asyncREST/GetTask/12608

{
  "TaskId": 12608,
  "PercentDone": 100,
  "Start": "2017-12-07 23:18:55.000",
  "End": "2017-12-07 23:19:05.000",
  "Data": "Some result",
  "IsJobAlive": 0
}

What do you think of this approach?

3
0 1,125
Discussion (9)2
Log in or sign up to continue

Looks like just some simple implementation for Long Pulling requests.

But how about getting not just a progress but how to get some data before task finished. 

And also, I see beg security Issue, when I can get information from any other process. You use TaskId directly from the request like you trust everybody. And also easy to get SUBSCRIPT error.

BTW: do you suppose to acept someone's answer? Looks like it is mostly just discissing topic.

But how about getting not just a progress but how to get some data before task finished. 

Append to data global as required and get data on each check, not only for complete requests?

And also, I see beg security Issue, when I can get information from any other process. You use TaskId directly from the request like you trust everybody. And also easy to get SUBSCRIPT error.

I completely agree that it's unsecure and with no $get(). But it's a minimal implementation, so I skipped a lot of details.

BTW: do you suppose to acept someone's answer? Looks like it is mostly just discissing topic.

I think it's more of a discussion topic. That's why it's an article and not a question.

That's why it's an article and not a question.

So, for now, yes. I changed to article

So, for now, yes. I changed to article

It was a question? My bad, I thought I created an article.

Aside from the security concerns already raised (you should never direct accept user input), I feel the whole approach violates the statelessness of a REST architecture?
Thoughts?

 I feel the whole approach violates the statelessness of a REST architecture?

The request itself is stateless. For example session tracking is okay within REST despite the fact that session exists between requests. Here's how I understand violation of REST statelessness: let's say you have a newsfeed API /news and you return news in chunks of 20 elements. Users are authenticated.

REST way:

  • GET /news/1 - returns first 20 elements
  • GET /news/2 - returns elements 21-40
  • etc.

Not a REST way:

  • GET /news/next - returns first 20 elements
  • GET /news/next - returns elements 21-40
  • etc.

Now, the second approach violates REST principles because request itself - /news/next does not contain enough information to process it. Server should track how much pages each client received, and which page to return next.

To sum up: statelessness in REST means that request itself is enough to determine what do we need to do  with it and request does not require pulling any additional information about previous requests from the same client.

Any restrictions on using a messaging/queuing system? 

Please consider expanding your question. I'm not sure what do you mean.