Introducing svc-js — a framework for Grenache

Robert Kowalski
Bitfinex
Published in
7 min readDec 29, 2017

As a distributed team at Bitfinex, we love distributed systems. A core technology for our backend services is Kademlia — a distributed hash table (DHT) which acts as the backbone of the almost infinitely scalable Bittorrent network, currently averaging between 150 and 170 million monthly users.

In this article we take a look at the framework that we use to build microservices at Bitfinex. Built on Grenache, this framework enables us to add new features to Bitfinex in an efficient and reliable manner through bootstrapping a new service for DNS lookups.

In a recent article, titled ‘Microservices with Grenache’ we looked at how to build a small RPC client/server system using the core Grenache libraries. The following framework, svc-js, uses the same libraries whilst also leveraging the principles addressed in the previous blog post. The DHT takes care of service discovery and load balancing between services.

svc-js CLI

The easiest way to create a new service is by using our CLI. The CLI takes a local base repository and sets up an initial configuration with a sample API endpoint.

In the following tutorial, we first get all the required dependencies, before taking a look at how the service works in general. As a last step, we use the framework to implement our own DNS lookup microservice.

Hands on

As a first step, we install the CLI:

npm install -g https://github.com/bitfinexcom/svc-js-cli

In case we haven’t installed Grenache Grape yet, we install that too:

npm install -g grenache-grape

Our DNS service will be a utility service, so we clone bfx-util-js. This will act as the base repository the CLI will use to set up the service:

git clone https://github.com/bitfinexcom/bfx-util-js

The command svc-js-cli init grenache-api-base considers three arguments: The name of the new service, the port it will listen on and the location of the base repository that we use as bootstrap.

We will call our small service dns-service. It listens on port 1337 and uses the cloned bfx-util-js repository we run:

svc-js-cli init grenache-api-base dns-service 1337 ./bfx-util-js

If everything works out, the CLI will print a success message which contains all steps that are needed to continue:

In this case, it will print:

All set up!To test the new service, start two grapes:grape --dp 20001 --aph 30001 --bn '127.0.0.1:20002'
grape --dp 20002 --aph 40001 --bn '127.0.0.1:20001'
Run your worker:cd /Users/robert/bitfinex/dns-service
node worker.js --env=development --wtype=wrk-dns-service-api --apiPort 1337
and run the example.js in /Users/robert/bitfinex/dns-service:cd /Users/robert/bitfinex/dns-service
node example.js

The first two commands will start two Grape servers which connect to one another via port 20001 and 20002. Together they build the DHT. They will also listen on port 30001 and 40001 for HTTP requests.

Via the HTTP ports, our services and their clients will interact with the DHT:

grape --dp 20001 --aph 30001 --bn '127.0.0.1:20002'
grape --dp 20002 --aph 40001 --bn '127.0.0.1:20001'

The command node worker.js --env=development --wtype=wrk-dns-service-api --apiPort 1337 will spin up a new worker. The worker automatically connects to the Grapes. Make sure that you have switched to the dns-service directory before executing the command. The worker itself will listen on port 1337. The wtype argument maps to a file the CLI created for us. With --wtype=wrk-dns-service-api we say that worker.js should initiate the class in workers/api.service.lookup.wrk.js for us. We will take a look at this file in a few moments.

The last command suggested in the CLI output, node example.js explains how we can run a demo client. First the client will connect to the Grapes. From the Grapes, the client will receive the service it can connect to. With that information the client-library opens a direct connection to the service.

We will now walk through example.js.

As mentioned above, the client opens a connection to the Grapes/DHT.

const link = new Link({
grape: 'http://127.0.0.1:30001'
})
link.start()
const peer = new Peer(link, {})
peer.init()

After the connection is established, it fires a request to the Grapes, done through asking the Grapes for a service called dns:service. Behind the scenes, the Grapes send back the IP of one of the services called dns:service.

The client then connects directly to the worker and sends the query to the worker. In the case of example.js we send name: Paolo to the endpoint getHelloWorld:

const query = {
action: 'getHelloWorld',
args: [ { name: 'Paolo' } ]
}
peer.request('dns:service', query, { timeout: 10000 }, (err, data) => {
if (err) {
console.error(err)
process.exit(1)
}
console.log('query response:')
console.log(data)
console.log('---')
})

When we run the script, the server responds with: Hello Paolo

In the next section we will explore the anatomy of worker in detail, taking a deeper look at how it is configured and how the API endpoints work before we add our own functionality.

A closer look at our worker

In the previous section we found the right service by using the key dns:service: peer.request('dns:service'). For our worker, the CLI set this up for us in config/service.lookup.json:

{
"grcServices": [
"dns:service"
]
}

When we start the service with node worker.js --env=development --wtype=wrk-dns-service-api --apiPort 1337 it will take the --wtype=wrk-dns-service-api argument and map it to the filename workers/api.service.lookup.wrk.js. This initiates the exported class WrkdnsServiceApi for us, which starts the service.

We will now take a look at workers/api.service.lookup.wrk.js andservice.dns.js in the workers/loc.api directory.

The constructor of the class WrkdnsServiceApi in workers/api.service.lookup.wrk.js loads our config file config/service.lookup.json:

this.loadConf('service.lookup', 'lookup')

It also executes the inherited methods init and start. They connect the service to the Grapes and spin up our Grenache API at workers/loc.api/service.dns.js. In case extra work is needed on startup, we can choose to extend them.

Earlier in this post (when we ran example.js)we passed { name: 'Paolo' } via the args array. We also defined the action we want to address with action: 'getHelloWorld'. Here is our query from example.js again:

const query = {
action: 'getHelloWorld',
args: [ { name: 'Paolo' } ]
}

A look into service.lookup.js reveals the API method the client in example.js was interacting with. The methods takes the arguments, prepends a Hello to the supplied argument. Later it calls the callback to respond to the client:

getHelloWorld (space, args, cb) {
const name = args.name
const res = 'Hello ' + name
cb(null, res)
}

We have now walked through a basic request/response cycle, giving us a rough overview of theservice. In the next section we will put our DNS feature to use.

The reverse DNS lookup

Sometimes we want to know the hostnames which map to a given IP. In this given example, other services would send an IP to our lookup service and the service would then return the corresponding hostname.

The query would look something like this:

const query = {
action: 'getHostname',
args: [ { ip: '8.8.8.8' } ]
}

We can change the existing query in example.js but when we run the code, we receive the following error: Error: ERR_API_ACTION_NOTFOUND.

The service lets us know that there is no API action available for our action getHostname. So, lets add an empty method getHostname to our API handlers in loc.api/service.dns.js:

getHostname (space, args, cb) {

}

For the actual lookup we can use the dns module, which is part of Node.js core. We require it at the top of service.dns.js:

const dns = require('dns')

According to the Node.js docs, the signature for the reverse-dns lookup is dns.reverse(ip, callback). So we extract the IP from our args argument and pass it to the reverse function. In case of an error we return it to the client:

getHostname (space, args, cb) {
const ip = args.ip
if (!ip) return cb(new Error('ERR_ARGS_NO_IP'))

dns.reverse(ip, (err, res) => {
if (err) return cb(err)
cb(null, [ip, res])
})
}

In case our service was still running we stop it and restart it with the worker: node worker.js --env=development --wtype=wrk-dns-service-api --apiPort 1337.

When we now run example.js we get our result:

$ node example.jsquery response:
[ '8.8.8.8', [ 'google-public-dns-a.google.com' ] ]

Conclusion

Congratulations! You just created your first Microservice backed by the Bittorrent protocol! You will find the whole code used in this tutorial here.

By the way, the DNS lookup method is also part of our reference service, bfx-util-net-js which you can take a look at here.

We’ve recently open-sourced a number of the development libraries most essential to Bitfinex. If you are interested in learning more, visit Github.

Stay up to date with Bitfinex on Twitter, Facebook & LinkedIn.

Join us on our mission to create the most innovative & industry-leading cryptocurrency exchange.

--

--