consul-balancer
Version:
consul service discovery and balancing
261 lines (260 loc) • 10.9 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
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) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConsulBalancer = void 0;
const os = __importStar(require("os"));
const consul_1 = __importDefault(require("consul"));
const urllib = __importStar(require("urllib"));
const grpc = __importStar(require("@grpc/grpc-js"));
const lodash_1 = require("./utils/lodash");
class ConsulBalancer {
constructor(arg1, arg2, arg3) {
this.cachedServices = new Map();
this.conculInstance = null;
this.options = this.parseOptions(arg1, arg2, arg3);
this.address = this.findFirstNonLoopbackHostInfo();
this.serviceRegisterId = this.getServiceRegisterId();
if (this.options.discovery.register) {
this.register();
}
}
getConsulInstance() {
if (!this.conculInstance) {
const consulOptions = (0, lodash_1.omit)(this.options, 'discovery');
this.conculInstance = new consul_1.default(consulOptions);
}
return this.conculInstance;
}
// default register discovery service
register(options = {}) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
const check = {
interval: '15s',
timeout: '10s',
ttl: '60s',
deregistercriticalserviceafter: '5m',
};
if (this.options.discovery.healthCheckHTTP) {
check.http = `http://${this.address}:${this.options.discovery.servicePort}${this.options.discovery.healthCheckHTTP}`;
}
else {
check.tcp = `${this.address}:${this.options.discovery.servicePort}`;
}
if (this.options.gRPC) {
(_a = options.meta) !== null && _a !== void 0 ? _a : (options.meta = {});
Object.assign(options.meta, { gRPC_port: this.options.gRPC.port });
}
const registerOptions = (0, lodash_1.defaults)(options, {
id: this.serviceRegisterId,
name: this.options.discovery.serviceName,
address: this.address,
port: this.options.discovery.servicePort,
tags: [this.options.discovery.serviceName],
check,
});
try {
return yield this.getConsulInstance().agent.service.register(registerOptions);
}
catch (e) {
console.error(`[${new Date()}] consul-balancer register ${e}`);
throw e;
}
});
}
// default deregister discovery service
deregister(serviceId) {
return this.getConsulInstance().agent.service.deregister(serviceId !== null && serviceId !== void 0 ? serviceId : this.serviceRegisterId);
}
/**
* grpc load balancer
* @param serviceName - name of the service | address:port
* @param targe - grpc client instance target
* @param method - grpc method name
* @param data - grpc request data
*/
grpc(serviceName, target, method, data) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
let service = null;
const [address, port] = serviceName.split(':');
if (address && port) {
// directly use address:port
service = { address, port };
}
else {
// Get random healthy service
service = yield this.getPassingServiceByRandom(serviceName);
if (!service)
throw new Error(`${serviceName} 服务不可用`);
if (!((_a = service.meta) === null || _a === void 0 ? void 0 : _a['gRPC_port'])) {
throw new Error('gRPC service meta not found gRPC_port');
}
service.port = +service.meta['gRPC_port'];
}
// Create client
const grpcClient = new target(`${service.address}:${service.port}`, grpc.credentials.createInsecure());
console.log(`[${new Date()}] consul-balancer grpc ~ ${target.serviceName}#${method}`);
return new Promise((ok, fail) => {
grpcClient[method](data, (err, result) => {
if (err)
return fail(err);
return ok(result);
});
});
});
}
rest(serviceName, pathName, options = {}) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
const service = yield this.getPassingServiceByRandom(serviceName);
if (!service)
throw new Error(`${serviceName} 服务不可用`);
const { address, port } = service;
const defaultOptions = { dataType: 'json' };
if (!((_a = options === null || options === void 0 ? void 0 : options.headers) === null || _a === void 0 ? void 0 : _a['content-type'])) {
(0, lodash_1.set)(options, ['headers', 'content-type'], 'application/json');
}
const { href } = new URL(pathName, `http://${address}:${port}`);
console.log(`[${new Date()}] consul-balancer rest ~ ${href}`);
return urllib.curl(href, Object.assign(Object.assign({}, defaultOptions), options));
});
}
getPassingServiceByRandom(serviceName = this.options.discovery.serviceName) {
return __awaiter(this, void 0, void 0, function* () {
let services = yield this.getServiceListFromConsul(serviceName);
if (!services.length) {
const cache = this.cachedServices.get(this.options.discovery.serviceName);
if (cache && Date.now() - 60 * 1000 < cache.cacheTime) {
services = cache.services;
}
}
if (services.length)
return (0, lodash_1.sample)(services);
return null;
});
}
getServiceListFromConsul(serviceName, passing = true) {
return __awaiter(this, void 0, void 0, function* () {
const serviceList = (yield this.getConsulInstance().health.service({
service: serviceName,
passing,
}));
const services = (serviceList !== null && serviceList !== void 0 ? serviceList : []).map((service) => {
const result = {
node: service.Node.Node,
address: service.Service.Address,
port: service.Service.Port,
meta: service.Service.Meta,
status: '',
};
service.Checks.some((check) => {
if (check.ServiceName === this.options.discovery.serviceName) {
result.status = check.Status;
return true;
}
return false;
});
return result;
});
if (services.length) {
this.cachedServices.set(this.options.discovery.serviceName, {
services,
cacheTime: Date.now(),
});
}
return services;
});
}
parseOptions(...args) {
const options = {};
for (const arg of args) {
if (arg === null || typeof arg === 'undefined') {
continue;
}
if (typeof arg === 'object') {
(0, lodash_1.defaults)(options, arg);
}
switch (typeof arg) {
case 'object':
(0, lodash_1.defaults)(options, arg);
break;
case 'number':
options.port = arg;
break;
case 'string':
options.host = arg;
break;
default:
throw new Error(`[consul-balancer] Invalid argument ${arg}`);
}
}
return (0, lodash_1.defaults)(options, ConsulBalancer.defaultOptions);
}
getServiceRegisterId() {
return [
this.options.discovery.serviceName,
this.address,
this.options.discovery.servicePort,
].join('-');
}
findFirstNonLoopbackHostInfo() {
const netInfo = os.networkInterfaces();
for (const netName of Object.keys(netInfo)) {
const net = netInfo[netName];
for (const addr of net !== null && net !== void 0 ? net : []) {
if (addr.internal === false && addr.family === 'IPv4') {
return addr.address;
}
}
}
return this.options.host;
}
}
exports.ConsulBalancer = ConsulBalancer;
ConsulBalancer.defaultOptions = {
host: '127.0.0.1',
port: 8500,
secure: false,
discovery: {
enable: true,
register: false,
deregister: true,
serviceName: '',
servicePort: 8080,
}
};