consul-resolver
Version:
A load balancer for Consul services with Redis-based metrics
190 lines (189 loc) • 9.32 kB
JavaScript
"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;