realm-object-server-enterprise
Version:
Realm Object Server Enterprise
262 lines • 10.9 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const realm_object_server_1 = require("realm-object-server");
const consul = require("consul");
const uuid = require("uuid");
class ConsulServiceWatch extends realm_object_server_1.ServiceWatch {
constructor(consulClient, logger, name, tags) {
super(name, tags);
this.consulClient = consulClient;
this.logger = logger;
const events = ["available", "unavailable"];
this.on("newListener", (eventName) => {
if (events.indexOf(eventName) === -1) {
return;
}
if (events.map(this.listenerCount, this).reduce((sum, count) => sum + count) === 0) {
this.startWatch();
}
});
this.on("removeListener", (eventName) => {
if (events.indexOf(eventName) === -1) {
return;
}
if (events.map(this.listenerCount, this).reduce((sum, count) => sum + count) === 0) {
this.watch.end();
this.watch = undefined;
}
});
}
startWatch() {
let oldHandle;
this.watch = this.consulClient.watch({
method: this.consulClient.health.service,
options: {
service: this.serviceName,
},
});
const hasTag = (service, tag) => {
return service.Tags.indexOf(tag) !== -1;
};
const hasAllTags = (service, searchTags) => {
return searchTags.every((tag) => hasTag(service, tag));
};
const serviceMoved = (a, b) => {
if (!a && !b) {
return false;
}
if (!a && b || a && !b) {
return true;
}
return a.address !== b.address || a.port !== b.port;
};
this.watch.on("error", (error) => {
setImmediate(() => {
if (this.watch.isRunning()) {
this.logger.debug("Consul watch threw an error but is retrying.", { error, serviceName: this.serviceName });
}
else {
this.logger.fatal("Consul watch threw an error and stopped running.", { error, serviceName: this.serviceName });
this.emit("error", error);
}
});
});
this.watch.on("change", (changes) => {
try {
let handle;
for (const change of changes) {
const service = change.Service;
if (service) {
if (!this.serviceTags || hasAllTags(service, this.serviceTags)) {
handle = {
address: service.Address,
name: service.Service,
port: service.Port,
tags: service.Tags,
};
break;
}
}
}
if (serviceMoved(oldHandle, handle)) {
if (handle) {
this.emit("available", handle);
}
else {
this.emit("unavailable", oldHandle);
}
oldHandle = handle;
}
}
catch (err) {
this.logger.error(`Consul watch onchange handler threw an error: ${err}`);
}
});
}
}
exports.ConsulServiceWatch = ConsulServiceWatch;
class ConsulDiscovery extends realm_object_server_1.Discovery {
constructor(config = {}) {
super();
this.serviceIdMap = {};
this.intervalMap = {};
this.logger = (config.logger || new realm_object_server_1.MuteLogger()).withContext({ serviceName: "consul" });
this.namespace = config.namespace;
this.checkTtl = config.checkTtl || 30;
this.consul = new consul({
host: config.consulHost || process.env.CONSUL_HOST,
port: config.consulPort || process.env.CONSUL_PORT,
secure: config.secure,
promisify: true,
});
this.advertiseAddress = config.advertiseAddress || process.env.CONSUL_ADVERTISE_ADDRESS;
this.advertisePortMap = config.advertisePortMap;
if (!this.advertisePortMap) {
this.advertisePortMap = Reflect.ownKeys(process.env)
.map((k) => /^CONSUL_ADVERTISE_PORT_(\d+)$/.exec(k))
.filter(Boolean)
.map((m) => ({ port: Number(m[1]), ext: Number(process.env[m[0]]) }))
.reduce((map, m) => { map[m.port] = m.ext; return map; }, {});
}
}
registerService(service, address, port) {
return __awaiter(this, void 0, void 0, function* () {
const id = this.findOrCreateId(service);
const name = Reflect.get(service.constructor, "serviceName");
if (!name) {
return;
}
try {
address = this.advertiseAddress || address;
port = this.advertisePortMap[port] || port;
const tags = this.namespacedTags(service.tags);
const register = () => __awaiter(this, void 0, void 0, function* () {
yield this.consul.agent.service.register({
address,
check: {
deregister_critical_service_after: "1m",
status: "passing",
ttl: `${this.checkTtl}s`,
},
id, name, port,
tags,
});
});
if (name === "sync") {
const existing = yield this.findAll("sync", service.tags);
yield Promise.all(existing.map((handle) => {
return this.consul.agent.service.deregister({ id: handle.ID });
}));
}
this.logger.debug(`Registering service '${name}' at ${address} with tags ${tags}`);
yield register();
this.intervalMap[id] = setInterval(() => __awaiter(this, void 0, void 0, function* () {
if (!this.intervalMap[id]) {
return;
}
try {
yield this.consul.agent.check.pass(`service:${id}`);
}
catch (err) {
const reason = err.message;
this.logger.warn(`failed to renew the check ttl for service:${id}: ${reason}`);
const leader = yield this.consul.status.leader().catch((err) => undefined);
if (leader) {
this.logger.debug(`Re-registering service '${name}' at ${address} with tags ${tags}`);
yield register().catch();
}
else {
}
}
}), Math.floor((this.checkTtl * 1000) / 2));
}
catch (err) {
this.logger.debug(`Error registering service '${name}'(${service.constructor.name}) at '${address}:${port}': ${err.message}`, err);
throw new realm_object_server_1.DiscoveryRegisterError(err.message);
}
});
}
deregisterService(service) {
return __awaiter(this, void 0, void 0, function* () {
const id = this.findId(service);
if (id) {
yield this.consul.agent.service.deregister({ id }).catch((e) => {
this.logger.warn(`failed to deregister service:${id}`);
});
delete this.serviceIdMap[id];
const interval = this.intervalMap[id];
if (interval) {
clearInterval(interval);
delete this.intervalMap[id];
}
}
});
}
findAll(name, tags) {
return __awaiter(this, void 0, void 0, function* () {
const tag = this.namespace ? `namespace=${this.namespace}` : undefined;
const result = yield this.consul.catalog.service.nodes({ service: name, tag });
const hasTag = (node, searchTag) => {
return node.ServiceTags.indexOf(searchTag) !== -1;
};
const hasAllTags = (node, searchTags) => {
return searchTags.every((t) => hasTag(node, t));
};
let mustHaveTags = this.namespacedTags([]);
if (tags) {
mustHaveTags = mustHaveTags.concat(tags);
}
return result.filter((r) => hasAllTags(r, mustHaveTags)).map((r) => {
return this.createConsulServiceHandle(r);
});
});
}
watchService(name, tags) {
return new ConsulServiceWatch(this.consul, this.logger, name, this.namespacedTags(tags));
}
createConsulServiceHandle(r) {
const serviceAddress = r.ServiceAddress !== "" ? r.ServiceAddress : r.Address;
return {
address: serviceAddress,
name: r.ServiceName,
port: r.ServicePort,
tags: r.ServiceTags,
ID: r.ID,
};
}
findId(service) {
for (const id in this.serviceIdMap) {
if (this.serviceIdMap.hasOwnProperty(id)) {
if (this.serviceIdMap[id] === service) {
return id;
}
}
}
}
findOrCreateId(service) {
const id = this.findId(service);
if (id) {
return id;
}
const newId = uuid.v4();
this.serviceIdMap[newId] = service;
return newId;
}
namespacedTags(tags) {
const ret = tags ? tags : [];
if (this.namespace) {
return ret.concat(`namespace=${this.namespace}`);
}
return ret;
}
}
exports.ConsulDiscovery = ConsulDiscovery;
//# sourceMappingURL=ConsulDiscovery.js.map