UNPKG

realm-object-server-enterprise

Version:

Realm Object Server Enterprise

262 lines 10.9 kB
"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