My quick lab experiment on the Event-Driven, Single Threaded and Asynchronous aspects of Node.js

During the prolonged lockdown of 2020 due to the lethal Covid-19 pandemic, I challenged myself to refresh and enhance my Javascript knowledge. In the process I met Node.js and I studied about React. Unfortunately I didn't create a meaningful website or webapp using my growing Javascript knowledge. This was that until I met 11ty an SSG which is based on Node.js. While only a basic knowledge of Javascript and Node.js is necessary to use 11ty, I found myself willing to learn the 'behind the scene' aspects of Node.js.

Today, I did a quick hands on experiment to learn a bit more about the Event-Driven, Single Threaded and Asynchronous aspects of Node.js. Here is the lab experiment.

Starter

mkdir nodejs-experiment
cd nodejs-experiment
npm init -y
npm install express
touch experiment1.js
nano experiment1.js

The contents of experiment1.js are as follows:

const express = require('express');
const app = express();

let requestCount=0;

app.get('/', (req, res) => {
++requestCount;

req.customSerialId=requestCount;
req.customStartDate=new Date();

console.log("Received a request with a customSerialId="+req.customSerialId);

req.customEndDate=new Date();

res.send(`{ "serialId": ${req.customSerialId}, "startDate": ${req.customStartDate}, "endDate": ${req.customEndDate} }`);
});


app.listen(3000, () => console.log('Server ready'));

I ran experiment1.js as follows:

node experiment1.js

I then opened two terminals (preferably as tabs) and issued the following command on both of them

curl http://localhost:3000/

Everything is okay. But so far no meaning lessons learnt about the Event-Driven, Single Threaded and Asynchronous aspects of Node.js. The only no-brainer lesson is that Node.js is capable of running multiple requests.

Blocking event

npm install moment
touch experiment2.js
nano experiment2.js

The contents of experiment2.js are as follows:

const express = require('express');
const moment = require('moment');
const app = express();

let requestCount = 0;

app.get('/', (req, res) => {
++requestCount;

const startMoment = moment();

req.customSerialId = requestCount;
req.customStartDate = new Date();
req.customCategory = requestCount % 2 === 1 ? "Delayed" : "Fast";

console.log("Received a request with a customSerialId=" + req.customSerialId);

if (requestCount % 2 === 1) {
while (moment().diff(startMoment, 'minutes') < 2) {
//For the odd numbered requests just delay with about 2 minutes doing nothing
}
}


req.customEndDate = new Date();

res.send(`{ "serialId": ${req.customSerialId}, "category": ${req.customCategory}, "startDate": ${req.customStartDate}, "endDate": ${req.customEndDate} }`);
});


app.listen(4000, () => console.log('Server ready'));

I ran experiment2.js as follows:

node experiment2.js

I then opened two terminals (preferably as tabs) and quickly issued the following command on both of them

curl http://localhost:4000/

Findings

  • Callbacks are executed in Call Stack by a single thread also known as the main thread.
  • Callbacks include Request Callbacks.
  • Callbacks are handled in the order they come.
  • Callbacks are queued in Event queue prior to execution.
  • Event queue is also known as Callback queue.
  • Main thread executes one callback at a time.
  • When Call Stack is empty and Event Queue has pending callbacks, Event Loop moves the first callback from Event Queue to Call Stack to be executed by main thread
  • The while loop in the request callback is an example of a CPU intensive operation which blocked the main thread thereby making Event Loop unable to move callbacks.

Experiment 2

Asynchronous/Non-blocking event

touch experiment3.js
nano experiment3.js

The contents of experiment3.js are as follows:


const express = require('express');
const app = express();

const ONE_SECOND = 1000;
const ONE_MINUTE = ONE_SECOND * 60;
let requestCount = 0;

app.get('/', (req, res) => {
++requestCount;

req.customSerialId = requestCount;
req.customStartDate = new Date();
req.customCategory = requestCount % 2 === 1 ? "Delayed" : "Fast";

console.log("Received a request with a customSerialId=" + req.customSerialId);

if (requestCount % 2 === 1) {
setTimeout(function () {
req.customEndDate = new Date();
res.send(`{ "serialId": ${req.customSerialId}, "category": ${req.customCategory}, "startDate": ${req.customStartDate}, "endDate": ${req.customEndDate} }`);
}, ONE_MINUTE * 2);

} else {
req.customEndDate = new Date();
res.send(`{ "serialId": ${req.customSerialId}, "category": ${req.customCategory}, "startDate": ${req.customStartDate}, "endDate": ${req.customEndDate} }`);
}
});


app.listen(5000, () => console.log('Server ready'));


I ran experiment3.js as follows:

node experiment3.js

I then opened two terminals (preferably as tabs) and quickly issued the following command on both of them

curl http://localhost:5000/

Findings

  • setTimeout is an example of a non-blocking or asynchronous operation
  • Asynchronous operation enables execution of the current callback to proceed without blocking
  • Execution of an asynchronous operation is performed by Thread Pool in background (not in main single thread)
  • After the execution of an asynchronous operation Thread Pool will send a corresponding callback to Event Queue aka Callback Queue.

Experiment 3