Queuing Requests in Node.js

5 seconds before the OOPs

Barisere Jonathan
Sprinthub Engineering
8 min readSep 28, 2018

--

A queue buffering messages between producers and consumers
Image source: The Apache ActiveMQ Project

Update

I wrote this article while I was an intern in 2018. Since then I have learned better ways of working with queues. The technique presented here is bad for several reasons, among which is the fact that it uses your function names in the queued data. This makes the entire system brittle to function renames.

I suggest you find better resources for this instead. If you want to use this technique, use a job type instead of adding the worker function name to the job. That way you can version your job types and have a flexible system.

If you are still interested in this article, read it below.

Have you ever deleted a file accidentally and wished you could go back in time only three seconds? How about clicking the send button, only to realise that you misspelt a conspicuous word (‘regards’ as ‘retards’, auto complete?). You stare helplessly at the screen, frustrated that you cannot find a way to undo the action.

While using a software system, we sometimes trigger actions unintentionally. Yes, that was a typo, but little mistakes can cost a lot. Ideally, the system should provide some safeguards against such mistakes. Software developers can help by anticipating such possible mistakes and building safeguards into the software system.

One such safeguard is the provision of intermediate confirmation steps that require the user to review the action(s) to be taken and the resource(s) that will be affected. File managers often provide such intermediate confirmation steps. KDE’s Dolphin file manager presents this pop-up window whenever one tries to delete a file.

KDE’s Dolphin File Manager Delete Confirmation Dialog

Another safeguard is providing an option to undo an action that has just been taken. For example, most file managers do not delete a file when a user clicks the ‘delete’ option. Instead, the file is moved to a different location (often called ‘Trash’ or ‘Recycle Bin’) from which it can be either recovered if needed or permanently deleted. Google Photos provides this option when one tries to delete photos.

You can dump it all here

The third form of safeguard is to delay the execution of the action for a period of time, and then carry out the action once the time elapses. This form is often paired with an ‘undo’ option that allows the requested action to be cancelled within a duration. The duration can be of any length, from a few seconds to many days (for example, Facebook account deletion take as long as 30 days, during which signing into the account cancels the deletion).

In this article, we will examine some ways of delaying request execution in a web application. We will assume a simple client that sends requests for CRUD operations to a server, and of course a server that processes the requests. The server program will be written in JavaScript (on Node.js), although the concepts can apply to any language and platform.

The scenario we imagine is as follows. The user interface for our web application displays information about a resource and provides a ‘delete’ button next to it. When the user clicks the ‘delete’ button, the user interface is updated to show an ‘undo’ button which the user can click, within a few seconds, to cancel the request.

Before clicking the ‘delete’ button
After clicking the ‘delete’ button

We will explore two options for delaying the request execution. One option is to handle the delay in the same process that executes the request, and the other depends on an external service.

Delaying the Request Using setTimeout

JavaScript has an inbuilt function setTimeoutthat accepts a function to execute and a timeout in milliseconds after which the function will be sent for execution. This function can be used on either the server or the client, to achieve delayed requests.

Setting the Timeout on the client

This would be accomplished by delaying the sending of the HTTP request to the server for some time, then sending the request after that time has elapsed. This can be done as follows.

First, we make a WeakMap to save references to any delete button that is clicked. Then we define a function that schedules the request after a delay.

The handler for the delete click events adds a timeout ID to the deleteRequests map, with the source event as the key.

Click event handler for the delete button

If the undo button is clicked before the delete request has been sent, we cancel the request that was sent by calling clearTimeout with the timeout ID that was saved when the delete request was scheduled. The function below does just that.

This approach has the upside of keeping the source of the action request (delete resource) and the undo request closer, making it easier to cancel the action within a short time of the cancel request.

One downside to this approach would be a bad user experience in cases where the network fails before the request was sent. The users would expect the request to succeed because they did not request an undo, but would be surprised that the request did not work (it was never sent).

Setting the Timeout on the Server

Instead of delaying the request on the client, the action can be delayed on the server using setTimeout. This approach provides the convenience of not depending on an external service (which can fail), as well as being simpler to implement.

However, this approach will not work for programs that run in more than one process (whether on a single machine or on different machines). For example, if the server processes run in three Docker services managed by docker-compose or Docker swarm, requests can end up on any server process, and an undo request can be sent to a server process that does not have the context required to fulfil the request.

The process is similar to the previous approach on the client. First, we make a map for tracking delete requests, a function that schedules the deletion of the specified resource, and a function to cancel the scheduled task.

On each delete request for the resource, we schedule the deletion and save the timeout ID, which will be deleted either when the delete request is cancelled, or when the resource is deleted.

Using an External Task Queue to Delay Request Execution

We noticed a problem with the last approach: it cannot work for a server that runs on multiple processes. Let us fix that by using an external service to provide a task queue for the delete requests. The external service will serve as a synchronisation mechanism for the server processes.

There are different possible approaches to this. One option is to use a task queue provided by a cloud provider. On Google Cloud Platform (GCP), services running on App Engine can create task queues, and other services running either on App Engine or any other compute platform can consume tasks off those queues. For this example, we will use Redis to provide storage for the request data, and use the Bull to provide a queue based on Redis.

In a file named undo-queue.js, we will create a module that handles the creation of the queue, addition of tasks to the queue, running the tasks, as well as removing tasks from the queue in order to cancel them. First we import the necessary modules, initialize a map for storing references to worker functions (functions that will process jobs off the queue), and construct a new Queue instance. The Bull library is well documented, so look up the options we pass to the Queue constructor to know what they do.

Constructing the undo queue.

With our queue in place, we need a function for adding jobs to the queue, a function to process jobs off the queue, and a function to cancel jobs if we do not want them processed.

And with that, we have a module to manage our task queues. To use this module, we import it and register a worker by calling makeWorker with the worker function. The worker function should be given a unique name in order to identify it when it needs to be called to process a job.

Request Handlers for ‘delete’ and ‘cancel’ requests.

And that completes our strategy for delaying request execution using an external task queue.

Advantages of Using an External Task Queue

  • This approach also has the advantage of being reusable. With the same Redis server, we can create various queues with different configurations for different kinds of jobs.
  • The jobs in the queue will be retried if they fail, according to the configuration we passed in. This can be useful if the operation carried out by the worker function is idempotent (example, a delete operation).

Downsides of Using an External Task Queue

  • If the Redis server is unavailable, the application will not be able to schedule and process requested actions.
  • We take on a few more dependencies, and, if done wrong, we can increase the complexity of the application.

Summary

In this article, we examined the need for providing safeguards for unintended actions in our applications. We considered two strategies for providing such safeguards: first, by using JavaScript’s setTimeout function either in the browser or on the server; second, by separating the mechanism for tracking and delaying such unintended actions from the application server (we achieved this using a task queue implemented with the Bull library and Redis). We found that both strategies have upsides and downsides, and which one is suitable depends on the needs of the application.

You read up to this point, great. I assume you have some impressions to share. Please leave a comment below. Let us know what you think about the problem discussed, or about how you would approach the solution yourself. Thank you for stopping by. ☺

--

--

Barisere Jonathan
Sprinthub Engineering

Sometimes the thoughts come to me, sometimes I seek them. I share some thoughts here.