We all are aware of the async nature of the javascript program execution through the Google V8 engine. It has a lot of advantages if we can use it properly otherwise the same async behavior can make a lot of trouble. One needs to understand callback, promise, and async-await features thoroughly before writing async code in node.js or javascript.
Recently, I was writing a script in node.js to build multiple docker images using dockerfile and push images to AWS elastic container registry. As building images take some time node.js was useful to run those processes asynchronously. The challenge I faced was to get a callback when all multiple asynchronous processes are done so that I can push those images to ECR.
Here I am adding my code snippet and how I refactored to solve the issue.
function buildImages(image) {
let tagName = 'docker-stack';
// auto-deployment/${image}/Dockerfile is the path to the image Dockerfile
let buildImageCommand = `docker build -f auto-deployment/${image}/Dockerfile -t ${image}:${tagName} .`;
// Spawn is used to stream the command output which buffers the
// output until the command is completed
const buildImage = spawn(buildImageCommand, [], {shell: true});
buildImage.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
buildImage.stderr.on('data', (data) => {
console.error(`stderr: ${data}`);
});
// Close event is called When the build images process is complete
buildImage.on('close', (code) => {
console.log(`${image} build completed`);
});
}
I have to build 3 images for our application asynchronously.
let images = ['lumen-api', 'web-app', 'mysql']
// Call buildImages(image) in loop
How can we get notified when all the images are built by running the same function for individual image? We need to use promise and callback effectively to achieve it.
To solve this kind of problem thare are many async libraries available and Async utility module is one of the popular modules to solve this problem. Here is the npm package.
I have to refactor my first code snippet like below by adding callback to the fucntion argument and return callback so that we can use async utility module.
//build-image-service.js file
module.exports = {
/**
* Build image using Dockerfile
*
* @param {string} image image name
* @param {function(object, bool)} callback: called aftre executing the
* command
*
* @return callback
*/
buildImages: function(image, callback) {
console.log(`Started building the ${image} image... \n`);
let tagName = 'docker-stack';
let buildImageCommand = `docker build -f auto-deployment/${image}/Dockerfile -t ${image}:${tagName} .`;
// Spawn is used to stream the command output unlike exec which buffers the
// output until the command is completed
const buildImage = spawn(buildImageCommand, [], {shell: true});
buildImage.stdout.on('data', (data) => {
console.log(`${image} image build: ${data}`);
});
buildImage.stderr.on('data', (data) => {
console.error(`${image} image build: ${data}`);
});
buildImage.on( 'error', ( err ) => {
console.log(`Error: "${ err }"`);
callback(err, false);
});
// Close event is the last event emitted for the spawn process
buildImage.on('close', (code) => {
console.log(`Build image of ${image} completed`);
callback(null, true);
});
}
};
How am I using async library to solve the issue?
var async = require("async");
var imageService = require('./build-image-service');
let images = ['lumen-api', 'web-app', 'mysql']
// Run build image process for all the above images
// asynchronously and get a callback when all processes are
// completed
async.forEachOf(images, (image, key, callback) => {
imageService.buildImages(image, function(err, result) {
if(!result) {
return callback(err);
}
callback();
});
}, err => {
// Once all the buildImage processes are complete which run asyncronously
// this block of code will be executed
// So I can push images to ECR now
});
Hopefully, it will give some idea to solve the issues related to asyncronous code. You can use many asnycn functions avialbel in async utility module. Here are quick examples:
async.map(['file1','file2','file3'], fs.stat, function(err, results) {
// results is now an array of stats for each file
});
async.filter(['file1','file2','file3'], function(filePath, callback) {
fs.access(filePath, function(err) {
callback(null, !err)
});
}, function(err, results) {
// results now equals an array of the existing files
});
async.parallel([
function(callback) { ... },
function(callback) { ... }
], function(err, results) {
// optional callback
});
async.series([
function(callback) { ... },
function(callback) { ... }
]);
Thank you!