@infect/rda-service-registry-client
Version:
RDA Service Registry Client
299 lines (218 loc) • 9.32 kB
JavaScript
import logd from 'logd';
import HTTP2Client from '@distributed-systems/http2-client';
import { v4 as uuidv4 } from 'uuid'
import os from 'os';
import v8 from 'v8';
import machineId from 'ee-machine-id';
import Delay from '@distributed-systems/delay';
const log = logd.module('rda-service-registry-client');
/**
* client interface for the service registry
*/
export default class ServiceRegistryClient {
/**
* set up the client
*
* @param {string} registryHost The registry host
*/
constructor(registryHost) {
this.registryHost = registryHost;
this.machineId = machineId();
this.baseURL = `${this.registryHost}/rda-service-registry.service-instance`;
this.httpClient = new HTTP2Client();
// cache lookup requests since they may be sent in a short time
this.lookupCache = new Map();
this.loolkupCachetimeout = 60 * 1000;
// if set top true we've been de-registered
this.isDeregistered = false;
}
/**
* shut down the client
*
* @return {Promise} undefined
*/
async end() {
log.debug(`Shutting down service registry client for ${this.identifier} of type ${this.serviceName}`);
if (this.delay) {
this.delay.cancel();
}
}
/**
* call the registry in double the required frequency
*
* @return {Promise} undefined
*/
async pollRegistry() {
this.delay = new Delay();
await this.delay.wait(this.ttl / 2);
// don't update after the service was de-registered
if (this.isDeregistered) return;
// call the registry and let them know that we're alive
const response = await this.httpClient.patch(`${this.baseURL}/${this.identifier}`)
.expect(200)
.send();
// consume the data, else the http2 stream will stay open
await response.getData();
// run again. important: without the setImmediate call the process may
// leak memory
setImmediate(() => {
this.pollRegistry().catch((err) => {
log.error(`The service registry client for ${this.identifier} of type ${this.serviceName} has encountered an error: ${err.message}`, err);
});
});
}
/**
* remove service registration
*
* @return {Promise} undefined
*/
async deregister() {
this.isDeregistered = true;
clearTimeout(this.timeout);
log.debug(`Deregistering service with identifier ${this.identifier} of type ${this.serviceName}`);
await this.httpClient.delete(`${this.baseURL}/${this.identifier}`)
.expect(200)
.send();
}
/**
* set the port of the registry server
*
* @param {number} port port of the server
*/
setPort(port) {
this.port = port;
}
/**
* register service
*
* @param {Object} arg1 options
* @param {string} arg1.identifier the unique identifier for this service instance
* @param {number} arg1.port the port this service is listening on
* @param {string} arg1.protocol the protocol this service provides services through
* @param {string} arg1.serviceName the name of this service
* @return {Promise} undefined
*/
async register({
identifier = uuidv4(),
port,
protocol = 'http://',
serviceName,
}) {
this.serviceName = serviceName;
if (this.isDeregistered) {
throw new Error('Cannot register service, it was de-registered and cannot be registered anymore!');
}
// eslint-disable-next-line no-param-reassign
port = port || this.port;
if (!port) {
throw new Error('Cannot register service: the option.port parameter was not passed to the register method!');
} else if (!serviceName) {
throw new Error('Cannot register service: the option.serviceName parameter was not passed to the register method!');
}
// store the identifier, it is used for de-registering later on
this.identifier = identifier;
// get network interfaces, use the first ipv4
// and the first ipv6 interfaces that are not
// private or internal
const addresses = this.getPublicNetworkInterfaces();
// report the available heap size to the registry, it is used
// to distribute the load on compute clients
const stats = v8.getHeapStatistics();
log.debug(`Registering service ${serviceName} on port ${port} with identifier ${identifier}`);
const response = await this.httpClient.post(this.baseURL)
.expect(201)
.send({
availableMemory: stats.total_available_size,
identifier,
ipv4address: addresses.ipv4 ? `${protocol}${addresses.ipv4}:${port}` : null,
ipv6address: addresses.ipv6 ? `${protocol}${addresses.ipv6}:${port}` : null,
machineId: this.machineId,
serviceType: serviceName,
});
const data = await response.getData();
// the registry tells us how often we need to
// update our records (convert from sec to msec)
this.ttl = data.ttl * 1000;
// start polling the registry, it else will assume
// that we have died!
this.pollRegistry().catch((err) => {
log.error(`The service registry client for ${this.identifier} of type ${this.serviceName} has encountered an error: ${err.message}`, err);
});
}
/**
* get the first ipv4 and ipv6 interfaces that are publicly accessible
*
* @return {object} The public network interfaces.
*/
getPublicNetworkInterfaces() {
const interfaces = os.networkInterfaces();
const interfaceNames = Object.keys(interfaces);
const result = {};
for (const interfaceName of interfaceNames) {
for (const networkInterface of interfaces[interfaceName]) {
if (!networkInterface.internal) {
if (!result[networkInterface.family.toLowerCase()]) {
result[networkInterface.family.toLowerCase()] = networkInterface.address;
}
}
}
}
return result;
}
/**
* do a lookup and get the address for a service
*
* @param {string} serviceName The service name
* @param {Object} arg2 options
* @param {string} arg2.family the ip family
* @param {(Function|number)} arg2.timeout timeout fo the lookup
* @return {Promise} a randomized address for a service
*/
async resolve(serviceName, {
family = 'ipv4',
timeout = 10000,
} = {}) {
const cacheKey = `${serviceName}-${family}`;
if (this.lookupCache.has(cacheKey)) {
const { addresses, created } = this.lookupCache.get(cacheKey);
if (created + this.loolkupCachetimeout > Date.now()) {
this.lookupCache.delete(cacheKey);
} else {
// if multiple addresses are returned, return a random one
const index = Math.floor(Math.random() * addresses.length);
return addresses[index];
}
}
const response = await this.httpClient.get(this.baseURL)
.timeout(timeout)
.expect(200)
.query({
serviceType: serviceName,
})
.send();
const addresses = await response.getData();
if (addresses.length) {
if (family === 'ipv4') {
const v4Adresses = addresses.filter(address => address.ipv4address).map((address) => address.ipv4address);
if (v4Adresses.length) {
this.lookupCache.set(cacheKey, {
addresses: v4Adresses,
created: Date.now(),
});
const index = Math.floor(Math.random() * v4Adresses.length);
return v4Adresses[index];
} else throw new Error(`Failed to resolve address for service '${serviceName}': the service has no IPv4 address registered!`);
} else if (family === 'ipv6') {
const v6Adresses = addresses.filter(address => address.ipv6address).map((address) => address.ipv6address);
if (v6Adresses.length) {
this.lookupCache.set(cacheKey, {
addresses: v6Adresses,
created: Date.now(),
});
const index = Math.floor(Math.random() * v6Adresses.length);
return v6Adresses[index];
} else throw new Error(`Failed to resolve address for service '${serviceName}': the service has no IPv6 address registered!`);
}
} else throw new Error(`Failed to resolve address for service '${serviceName}': service not found!`);
}
}