UNPKG

consul-resolver

Version:

A load balancer for Consul services with Redis-based metrics

190 lines (189 loc) 9.32 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("@brimble/utils"); const consul_1 = __importDefault(require("consul")); const algorithms_1 = require("./algorithms"); const dns_1 = require("./dns"); const health_1 = require("./health"); const metrics_1 = require("./metrics"); const scoring_1 = require("./scoring"); const types_1 = require("./types"); class ConsulResolver { constructor(config) { this.currentIndex = 0; this.debug = config.debug || false; this.cachePrefix = config.cachePrefix; this.cacheEnabled = config.cacheEnabled; this.weights = config.weights || types_1.DEFAULT_WEIGHTS; this.metrics = config.metrics || types_1.DEFAULT_METRICS; this.cacheTTL = Math.floor((config.cacheTTL || 60 * 1000) / 1000); this.consul = new consul_1.default({ host: config.host, port: config.port, secure: config.secure, defaults: { ...(config.token ? { token: config.token } : {}), }, agent: config.agent, }); if (this.cacheEnabled && config.redis) { this.redis = config.redis; this.cacheEnabled = true; } this.metricsManager = new metrics_1.MetricsManager(this.redis, this.cachePrefix, this.metrics, this.cacheEnabled, this.debug); this.dnsManager = new dns_1.DNSManager(this.redis, this.cachePrefix, this.cacheTTL, this.cacheEnabled, this.debug, config.dnsEndpoints, config.dnsTimeout, config.dnsRetries); this.healthCheckManager = new health_1.HealthCheckManager(this.consul, this.redis, this.cachePrefix, this.cacheTTL, this.cacheEnabled, this.debug); } /** * Select the optimal service based on the specified algorithm */ async selectOptimalService(service, algorithm = types_1.SelectionAlgorithm.RoundRobin) { var _a; try { const [healthChecks, dnsRecords] = await Promise.all([ this.healthCheckManager.getHealthChecks(service), this.dnsManager.resolveDNS(service) ]); if ((!healthChecks || healthChecks.length === 0) && dnsRecords.length === 0) { return { selected: null, services: [] }; } const sortedByPriority = this.dnsManager.sortByPriority(dnsRecords); const lowestPriorityValue = (_a = sortedByPriority[0]) === null || _a === void 0 ? void 0 : _a.priority; const highestPriorityRecords = sortedByPriority.filter(record => record.priority === lowestPriorityValue); if (!healthChecks || healthChecks.length === 0) { const { selected, nextIndex } = this.dnsManager.selectFromSrvRecords(highestPriorityRecords, algorithm, this.currentIndex); this.currentIndex = nextIndex; if (!selected) { return { selected: null, services: [] }; } await this.metricsManager.updateSelectionMetrics(selected.name); return { selected: { ip: selected.ip, port: selected.port, }, services: sortedByPriority.map(record => ({ ip: record.ip, port: record.port, })), }; } const dnsWeights = new Map(dnsRecords.map((record) => [record.ip, { weight: record.weight, port: record.port, priority: record.priority }])); const matchedHealthChecks = healthChecks.filter(check => dnsWeights.has(check.Service.Address)); if (matchedHealthChecks.length === 0) { if (this.debug) { utils_1.log.debug('No matching services found between DNS and Consul health checks'); } const { selected, nextIndex } = this.dnsManager.selectFromSrvRecords(highestPriorityRecords, algorithm, this.currentIndex); this.currentIndex = nextIndex; if (!selected) { return { selected: null, services: [] }; } await this.metricsManager.updateSelectionMetrics(selected.name); return { selected: { ip: selected.ip, port: selected.port, }, services: sortedByPriority.map(record => ({ ip: record.ip, port: record.port, })), }; } const highPriorityIPs = new Set(highestPriorityRecords.map(record => record.ip)); const highPriorityHealthChecks = matchedHealthChecks.filter(check => highPriorityIPs.has(check.Service.Address)); const targetHealthChecks = highPriorityHealthChecks.length > 0 ? highPriorityHealthChecks : matchedHealthChecks; const maxDNSWeight = Math.max(...dnsRecords.map((r) => r.weight || 1)); const enhancedHealthChecks = targetHealthChecks.map(check => { var _a; return ({ ...check, dnsWeight: (0, scoring_1.combineHealthAndDNSWeights)(check, ((_a = dnsWeights.get(check.Service.Address)) === null || _a === void 0 ? void 0 : _a.weight) || 0, maxDNSWeight) }); }); const metrics = await this.metricsManager.getServicesMetrics(targetHealthChecks); let selectedService; switch (algorithm) { case types_1.SelectionAlgorithm.RoundRobin: const rrResult = (0, algorithms_1.roundRobinSelection)(enhancedHealthChecks, this.currentIndex); this.currentIndex = rrResult.nextIndex; selectedService = { id: rrResult.id, service: rrResult.service }; break; case types_1.SelectionAlgorithm.LeastConnection: selectedService = (0, algorithms_1.leastConnectionSelection)(enhancedHealthChecks, metrics, this.metrics); break; case types_1.SelectionAlgorithm.WeightedRoundRobin: const rankedServices = (0, scoring_1.rankServices)(enhancedHealthChecks, metrics, this.weights); rankedServices.forEach(ranked => { const dnsInfo = dnsWeights.get(ranked.service.Service.Address); if (dnsInfo) { ranked.score *= (1 + (dnsInfo.weight / maxDNSWeight)); } }); selectedService = (0, algorithms_1.weightedRandomSelection)(rankedServices); break; } await this.metricsManager.updateSelectionMetrics(selectedService.id); const selectedDNSInfo = dnsWeights.get(selectedService.service.Service.Address); return { selected: { ip: selectedService.service.Service.Address, port: (selectedDNSInfo === null || selectedDNSInfo === void 0 ? void 0 : selectedDNSInfo.port) || selectedService.service.Service.Port, }, services: matchedHealthChecks.map(check => { const dnsInfo = dnsWeights.get(check.Service.Address); return { ip: check.Service.Address, port: (dnsInfo === null || dnsInfo === void 0 ? void 0 : dnsInfo.port) || check.Service.Port, }; }), }; } catch (error) { if (this.debug) { utils_1.log.error('Error selecting optimal service:', error); } return { selected: null, services: [] }; } } async incrementConnections(serviceId) { return this.metricsManager.incrementConnections(serviceId); } async decrementConnections(serviceId) { return this.metricsManager.decrementConnections(serviceId); } async getSelectionMetrics(serviceId) { return this.metricsManager.getSelectionMetrics(serviceId); } async refresh() { var _a, _b; try { if (!this.cacheEnabled) { if (this.debug) { utils_1.log.debug("Cache is disabled, no need to refresh"); } return; } const pattern = `${this.cachePrefix}:*`; const keys = await ((_a = this.redis) === null || _a === void 0 ? void 0 : _a.keys(pattern)); if (keys && keys.length > 0) { await ((_b = this.redis) === null || _b === void 0 ? void 0 : _b.del(...keys)); } } catch (error) { console.log("Error refreshing Redis caches:", error); throw new Error(`Failed to refresh caches: ${error.message}`); } } } exports.default = ConsulResolver;