Create resilient micro services with Node

The global idea is to build a core service that will enable an endpoint for micro service to self register to the core periodically.

For the example i will create a simple chatbot by using the wit api

This is what we want

First let’s create the core api, based on Express

const express = require('express');
const app = express();
const server = require('http').createServer(app);
const path = require('path');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const io = require('socket.io')(server);
const indexRoutes = require('./routes/index');
const config = require('./src/config');

const ServiceRegistry = require('./src/ServiceRegistry');
const serviceRegistry = new ServiceRegistry.ServiceRegistry();
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.static(path.join(__dirname, 'views')));
app.use(cookieParser());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use('/', indexRoutes);
app.set('_serviceRegistry', serviceRegistry);
server.listen(config.http.port, () => {
console.log(`Server is up at http:https://0.0.0.0:${config.http.port}`);require('./src/socket/Proxy')(io, serviceRegistry);
});
app.put('/register/service/:intent/:port', function(req, res,next) {
const serviceIntent = req.params.intent;
const servicePort = req.params.port;
const serviceLinkedKeys = req.body.linked;
const serviceIp = req.ip.replace('::ffff:', '');
    serviceRegistry.add(serviceIntent, serviceIp, servicePort, serviceLinkedKeys);
res.json({result: `${serviceIntent} at ${serviceIp}:${servicePort}`});
});

The important line here is the app.put(‘/register/service/:intent/:port’ …..

This is the register endpoint for the future micro service, on this endpoint they will be able to self register. We’ll see later what is the attended body for this request.

Then the service registry

class ServiceRegistry {
    constructor() {
this._services = [];
this._timeout = 30;
}
    // get all the registered services keys
allKeys() {
return Object.keys(this._services);
}
    // add a new micro service
add(intent, ip, port, linkedKeys) {
const key = intent+ip+port;
        if(!this._services[key]) {
this._services[key] = {};
this._services[key].timestamp = Math.floor(new Date() / 1000);
this._services[key].ip = ip;
this._services[key].port = port;
this._services[key].uri = `http://${ip}:${port}/service`;
this._services[key].intent = intent;
this._services[key].linked = linkedKeys;
            console.log(`Added service for intent ${intent} on ${ip}:${port}`);
this._cleanup();
return;
}
        this._services[key].timestamp = Math.floor(new Date() / 1000);
console.log(`Updated service for intent ${intent} on ${ip}:${port}`);
this._cleanup();
}
    //remove a micro service
remove(intent, ip, port) {
const key = intent + ip + port;
delete this._services[key];
}
    // return the wanted micro service
get(intent) {
console.log('searching service for intent : '+intent);
this._cleanup();
for(let key in this._services) {
if(this._services[key].intent == intent) return this._services[key];
}
console.log(`${intent} is not registered as service.`);
return null;
}
    // remove outdated micro services
_cleanup() {
const now = Math.floor(new Date() / 1000);
        for(let key in this._services) {
if(this._services[key].timestamp + this._timeout < now) {
console.log(`Removed service for intent ${this._services[key].intent}`);
delete this._services[key];
}
}
}
}
module.exports = {
ServiceRegistry,
services: {
NEWS: 'news'
}
};

A class that permit to make CRUD operations on micro services instances.

Each time a micro service is called, the cleanup is call, the goal is to always have living micro service in the registry.

Ok then the factory client

const Map = require('immutable').Map;
const messages = require('../../Message');
const resolveService = (serviceRegistry, data) => {
let services = Map();
Object.keys(data).map(intent => {
const service = serviceRegistry.get(intent);
if (service) {
services = services.set(intent, service);
}
});
return services;
};
const factoryService = classname => {
switch (classname) {
case 'greeting':
return require('./GreetingClient');
case 'chitchat':
return require('./ChitChatClient');
}
};
class Factory {
/**
*
* @param {ServiceRegistry} serviceRegistry
* @param {object} aiAnalysis
* @param {Server} io
*/
static resolve(serviceRegistry, aiAnalysis, io) {
//an example of a wit request
const a = {

"msg_id": "00bXNL6HCLYxToVWH",
"_text": "donne moi des news sur js",
"entities": {
"news": [{"confidence": 1, "value": "news", "type": "value"}],
"types": [{"confidence": 1, "value": "javascript", "type": "value"}]
}
};
const actionList = resolveService(serviceRegistry, aiAnalysis.entities);
if (actionList.size === 0) {
return new Promise((resolve, reject) => resolve(new Error("I don't understand, soon i will be smarter :)")));
}
return actionList.map(service => {
let query = {};
if (service.linked) {
service.linked.map(key => {
query[key] = aiAnalysis.entities[key][0].value
});
}
const clientService = factoryService(service.intent);
const waitingMessage = clientService.waitingMessage();
if (waitingMessage) {
io.emit(messages.ANSWER_MESSAGE, waitingMessage);
}
return clientService.askForData(`${service.uri}`, query).then(data => data);
});
}
}
module.exports = Factory;

The factory client will send a request to wit for knowing what kind of intent and actions the user ask to the bot. And then will try to call the micro service through the serviceRegistry.

Ok now let’s build the view

<html lang="en">
<head><link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css"integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous"><link rel="stylesheet" href="css/style.css"><script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"crossorigin="anonymous"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js"integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4"crossorigin="anonymous"></script><script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/js/bootstrap.min.js"integrity="sha384-h0AbiXch4ZDo7tp9hKZ4TsHbi047NrKGLO3SEJAg45jXxnGIfYzk4Si90RDIqNm1"crossorigin="anonymous"></script>
</head>
<body>
<div class="container-fluid"><div class="card-group"><div class="col-6"><div class="card"><div class="card-body"><div id="dialog"></div><form id="dialogForm"><label for="message" class="col-12"><input type="text" id="message" class="form-control"/></label><button type="submit" class="btn" id="sender">Talk</button></form></div></div></div></div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.1/socket.io.js"></script>
<script src="js/script.js"></script>
</body>
</html>

And the script.js file

const socket = io();
let username='user';
const botName='bender';
const stickToEnd = function () {
document.getElementById('dialog').scrollTop = document.getElementById('dialog').scrollHeight
};
jQuery(document).ready($ => {
socket.on('holdOn', () => {
$('#dialog').append(`<div id="botIsThinking" class="col-8 bot">...</div>`);
stickToEnd();
});
socket.on('holdOff', () => {
$('#botIsThinking').remove();
stickToEnd();
    });
$('#dialogForm').on('submit', (e) => {
e.preventDefault();
let $message = $('#message');
const message = $message.val();
if (message) {
sendToBot(message);
appendToChatDialog(message, 'human', username);
$message.val('');
}
stickToEnd();
});
    const sendToBot = (message) => {
socket.emit('chat message', message);
};
    const appendToChatDialog = (msg, from, name, isError) => {
if (msg.constructor === Array) {
msg = mapToResponse(msg)
}
$('#dialog').append(`<div class="col-12 ${from}">${name}</div>`);
$('#dialog').append(`<div class="col-8 ${from}-border ${isError ? `alert-danger` : ''}">${msg}</div>`);
stickToEnd();
};
    const mapToResponse = (responses) => {
return responses.map(response => {
let title, link, author, picture;
if (response.title) {title = response.title;}
if (response.link) {link = response.link;}
if (response.author_name) {author = response.author_name;}
if (response.author_icon) {picture = response.author_icon;}
            return '<div><p><a href="'+link+'">'+title+'</a></p><p>'+author+'</p></div>'
})
};
    socket.on('answer message', message => {
appendToChatDialog(message, 'bot', botName);
});
    socket.on('answer error', message => {
appendToChatDialog(message, 'bot', botName, true);
});
});

Ugh! i know this is ugly, but this is not the point, the front rendering is at least just for the example.

Ok for now our api core is functionnal.

It’s time to build the micro service

An example of greeting micro service

const express = require('express');
const app = express();
const server = require('http').createServer(app);
const request = require('request');
const config = require('./src/config');
server.listen();
server.on('listening', () => {
console.log(`${server.address().port} in ${app.get('env')} mode.`);
    const registerToMotherlord = () => {
const uri = `${config.swarm.motherlord}/register/service/greeting/${server.address().port}`;
        const options = {
method: 'PUT',
uri: uri
};
console.log(`attempt to register to motherlord : ${uri}`);
request(options, (err, res) => {
if(err) {
console.log(err);
console.log("Error connecting to motherlord");
}
});
    };
registerToMotherlord();
setInterval(registerToMotherlord, 10*1000);
});
app.post('/service', (req, res, next) => {
res.send("It is nice to talk with a person who knows good manners. Howdylididou!");
});

Here the registerToMotherLord is the method that will register the service to the service registry of the core api.

The body of the PUT method just need {linked: [‘greeting’]}, greeting is the name you want to register for this micro service.

const config = {
swarm: {
motherlord: 'http://0.0.0.0:8000'
}
};
module.exports = config;

And the simple config file for defining access to the core api.

As you see there is no big magic, the only thing is the setInterval called in micro service, should be less than the timeout defined in serviceRegistry, if not you could have dead micro service for litlle laps of time.

In this example, the security consideration are not taken in account

Of course, review and constructive criticisms are welcome :)

Here is the link to github for the core api

Here is the link for another micro service that return a list of tech news, based on the same architecture

Here, the link to a generator of micro service

And finally my github account

Thanks for reading