Polling Architecture with Serverless and Node.js

Introduction

In this tutorial I’m going to talk about how to implement the Polling Architecture with Serverless and AWS services using Node.js as a programming language. Also I’m going to briefly explain why would you want to use it and in which cases it can be useful.

What is polling?

Polling is a technique to develop software used to fetch results asynchronously from a service, the steps can be described as:

  1. The client triggers the task with a request to the server.
  2. The server returns an acceptance result to the client and spins off a secondary process to work in the task.
  3. After finishing, the server stores the result using an identifier.
  4. Meanwhile the server is processing the information the client calls the service requesting for the result every certain time.
  5. The process is not finished until the server returns the result (fail or success) or the client stops sending the requests.
Polling Flow

This can be used when we have a process that can take several minutes or when we don’t need the result right await and we want to let the user continue working with the application meanwhile something is being processed.

Why polling?

When we start working with Serverless (AWS), One of the first things we should know is that there are certain limitations for some of the services that we are directly or indirectly using, this is often ignored when designing and planning the application.

One of the “biggest” issues with API Gateway at the moment is that it has only 30 seconds timeout cap for the integration layer (lambda-proxy, http-events, etc), this means that our process cannot take longer than 30 seconds and there is no way of increasing it right now.

But you would say “This is ok, any modern API should send a response even before the 5 seconds mark”, and you are correct and I totally agree with that statement, the problem here is that we’ll ofter connect to services that are slower that we can manage.

Also, we tend to use the lambda’s timeout if it was the same for both, lambda’s timeout can be easily increased but API Gateway will always terminate the request after 30 secs, so we need to be aware of that.

For more information about the limitations of API Gateway you can check this link.

So let’s begin…

For this tutorial I’m going to create two http services and one stand alone lambda function. This is the description for each one.

  • Trigger. This http service will start a secondary task (worker) where the information is going to be processed and return an acceptance response with the request id to the client.
  • Response. This http service is going to look for the result in Redis and is going to send it to the client, if the result is not found it is going to return a 404 content not found error.
  • Worker. This is going to be an stand alone lambda function that will execute the task and save the result to Redis.
AWS Polling Chart Flow

To continue with this tutorial this are the requisites:

  • AWS account (of course) and SDK credentials.
  • Elasticache “redis” instance up and running.
  • VPC, we are not going to expose the Redis instance (Best security practice).

I’m using the Serverless CLI tool to create a new app with this command:

serverless create --template aws-nodejs --path polling

Then I’m adding the functions to the configuration, plus the VPC parameters to connect to Redis and to be able to invoke lambda functions.

serverless.yml

After doing that we can start working on services logic, to begin with I’m creating a simple RedisManager prototype function so we’ll be able to open and close connections every time a new request comes up.

handler.js

Next, we can start with the trigger function.

module.exports.trigger = (event, context, callback) => {
const lambda = new AWS.Lambda(); // create AWS lambda instance
const identifier = uuid(); // set an identifier to save to redis
// Invoke the worker lambda function, with the parameters we got from the request
lambda.invoke({
FunctionName: 'polling-service-dev-worker',
//by using the invocation type as Event, we define that we don't want to wait for the response from the lambda function and we only need to trigger it.
    InvocationType: 'Event',
LogType: 'None',
Payload: JSON.stringify({ queryStringParameters: event.queryStringParameters || {}, identifier }),
}, (err) => (
// here we manage the possible error and send the identifier back to the client
    callback(null, err ? {
statusCode: 500,
body: JSON.stringify(err.code),
} : {
statusCode: 200,
body: JSON.stringify({ identifier }),
})
))
};

To trigger the worker we use the Invoke function used to interact with lambda functions, this functions can be invoked as an independent event or we can wait for the result to come back, but in this case we are going to trigger and independent event and return the identifier to the client.

Then we can proceed with the worker function, this function will get the queryParameters object coming from the event object of the trigger service and it is going to create an array of key-value, something like:

{"foo": "bar"} -> [{"key": "foo", "value": "bar"}]

Also what we are going to do is to wait 31 seconds until we set the result to Redis, this is going to simulate a slow process or call that we want to avoid getting timed out.

module.exports.worker = (event, context, callback) => {
const redisManager = RedisManager();
redisManager.open() // open redis connection
.then((redisClient) => (
setTimeout(() => { // wait for 31 seconds to set response on redis
const result = Object.keys(event.queryStringParameters).map(key => ({
'key': key,
value: event.queryStringParameters[key],
}))
// set result on redis, using identifier
redisClient.set(event.identifier, JSON.stringify(result), (err) => (
redisManager.close()
.then(() => (
callback(err, {
statusCode: 200,
})
))
))
}, 31000)
))
};

Last but not least, we’ll have to create the response service, this service is going to try to get the result from Redis, if it’s found the service will send it back to the client but if not the service is going to return a 404 with the message: “no content found.”

module.exports.response = (event, context, callback) => {
const redisManager = RedisManager();
redisManager.open() //open redis connection
.then((redisClient) => (
// get result from redis
redisClient.get(event.pathParameters.identifier, (err, result) => (
redisManager.close()
.then(() => (
err ? callback(null, {
statusCode: 500,
body: JSON.stringify(err.code),
})
// if it doesn't exists send 404 with message.
: !result ? callback(null, {
statusCode: 404,
body: JSON.stringify('content not available.'),
})
// if it does return it to the client
: callback(null, {
statusCode: 200,
body: JSON.stringify(result),
})
))
))
))
};

After deploying the application and getting the endpoints generated, we’ll be able to test the services using Postman.

First I’m going to make a request to the trigger endpoint.

trigger request

Then I’m going to take the identifier from the response and I’m going to hit the response endpoint until I get the result back from the service.

first request to response service
Second request after 30 secs with result

After 30+ seconds I’m able to get a the result from the response service.

Conclusions

One of the best applications for Node.js/Serverless is to build a middleware between SPA and legacy backend systems, but the problem comes up when the legacy systems are not well prepared fulfill the performance specs that a Cloud Base Application needs, so we can use this type of architecture to build efficient non-blocking services that can work perfectly for both the client and the legacy systems.

I hope you have enjoyed this tutorial and I’m going to prepare more like this related with AWS/Serverless and Node.js, if you have any questions please let me know or contact me at oscar-rreyes1@hotmail.com

.then((redisClient) => (
redisClient.get(event.pathParameters.identifier, (err, result) => (
redisManager.close()
.then(() => (
err ? callback(null, {
statusCode: 500,
body: JSON.stringify(err.code),
}) : !result ? callback(null, {
statusCode: 404,
body: JSON.stringify('content not available.'),
}) : callback(null, {
statusCode: 200,
body: JSON.stringify(result),
})
))
))
))
};