@brimble/consul 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +277 -0
- package/lib/config.d.ts +55 -0
- package/lib/config.js +117 -0
- package/lib/consul.d.ts +23 -0
- package/lib/consul.js +9 -0
- package/lib/resolver/algorithms.js +127 -0
- package/lib/resolver/dns.js +182 -0
- package/lib/resolver/health.js +51 -0
- package/lib/resolver/metrics.js +199 -0
- package/lib/resolver/scoring.js +95 -0
- package/lib/resolver/types.js +28 -0
- package/lib/resolver.d.ts +76 -0
- package/lib/resolver.js +290 -0
- package/package.json +6 -1
package/lib/resolver.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { log } = require("@brimble/utils");
|
|
4
|
+
const {
|
|
5
|
+
leastConnectionSelection,
|
|
6
|
+
roundRobinSelection,
|
|
7
|
+
weightedRandomSelection,
|
|
8
|
+
} = require("./resolver/algorithms");
|
|
9
|
+
const { DNSManager } = require("./resolver/dns");
|
|
10
|
+
const { HealthCheckManager } = require("./resolver/health");
|
|
11
|
+
const { MetricsManager } = require("./resolver/metrics");
|
|
12
|
+
const {
|
|
13
|
+
combineHealthAndDNSWeights,
|
|
14
|
+
rankServices,
|
|
15
|
+
} = require("./resolver/scoring");
|
|
16
|
+
const {
|
|
17
|
+
DEFAULT_METRICS,
|
|
18
|
+
DEFAULT_WEIGHTS,
|
|
19
|
+
SelectionAlgorithm,
|
|
20
|
+
} = require("./resolver/types");
|
|
21
|
+
|
|
22
|
+
class Resolver {
|
|
23
|
+
constructor(consul, config) {
|
|
24
|
+
this.consul = consul;
|
|
25
|
+
this.currentIndex = 0;
|
|
26
|
+
this.debug = config.debug || false;
|
|
27
|
+
this.cachePrefix = config.cachePrefix;
|
|
28
|
+
this.cacheEnabled = config.cacheEnabled;
|
|
29
|
+
this.weights = config.weights || DEFAULT_WEIGHTS;
|
|
30
|
+
this.metrics = config.metrics || DEFAULT_METRICS;
|
|
31
|
+
this.redis = config.redis;
|
|
32
|
+
|
|
33
|
+
this.cacheTTL = Math.floor((config.cacheTTL || 60 * 1000) / 1000);
|
|
34
|
+
|
|
35
|
+
if (this.cacheEnabled && config.redis) {
|
|
36
|
+
this.redis = config.redis;
|
|
37
|
+
this.cacheEnabled = true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.metricsManager = new MetricsManager(
|
|
41
|
+
this.redis,
|
|
42
|
+
this.cachePrefix,
|
|
43
|
+
this.metrics,
|
|
44
|
+
this.cacheEnabled,
|
|
45
|
+
this.debug,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
this.dnsManager = new DNSManager(
|
|
49
|
+
this.redis,
|
|
50
|
+
this.cachePrefix,
|
|
51
|
+
this.cacheTTL,
|
|
52
|
+
this.cacheEnabled,
|
|
53
|
+
this.debug,
|
|
54
|
+
config.dnsEndpoints,
|
|
55
|
+
config.dnsTimeout,
|
|
56
|
+
config.dnsRetries,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
this.healthCheckManager = new HealthCheckManager(
|
|
60
|
+
this.consul,
|
|
61
|
+
this.redis,
|
|
62
|
+
this.cachePrefix,
|
|
63
|
+
this.cacheTTL,
|
|
64
|
+
this.cacheEnabled,
|
|
65
|
+
this.debug,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Select the optimal service based on the specified algorithm
|
|
71
|
+
*/
|
|
72
|
+
async selectOptimalService(
|
|
73
|
+
service,
|
|
74
|
+
algorithm = SelectionAlgorithm.RoundRobin,
|
|
75
|
+
) {
|
|
76
|
+
try {
|
|
77
|
+
const [healthChecks, dnsRecords] = await Promise.all([
|
|
78
|
+
this.healthCheckManager.getHealthChecks(service),
|
|
79
|
+
this.dnsManager.resolveDNS(service),
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
if (
|
|
83
|
+
(!healthChecks || healthChecks.length === 0) &&
|
|
84
|
+
dnsRecords.length === 0
|
|
85
|
+
) {
|
|
86
|
+
return { selected: null, services: [] };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const sortedByPriority = this.dnsManager.sortByPriority(dnsRecords);
|
|
90
|
+
|
|
91
|
+
const lowestPriorityValue = sortedByPriority[0]?.priority;
|
|
92
|
+
const highestPriorityRecords = sortedByPriority.filter(
|
|
93
|
+
(record) => record.priority === lowestPriorityValue,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if (!healthChecks || healthChecks.length === 0) {
|
|
97
|
+
const { selected, nextIndex } = this.dnsManager.selectFromSrvRecords(
|
|
98
|
+
highestPriorityRecords,
|
|
99
|
+
algorithm,
|
|
100
|
+
this.currentIndex,
|
|
101
|
+
);
|
|
102
|
+
this.currentIndex = nextIndex;
|
|
103
|
+
|
|
104
|
+
if (!selected) {
|
|
105
|
+
return { selected: null, services: [] };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
await this.metricsManager.updateSelectionMetrics(selected.name);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
selected: {
|
|
112
|
+
ip: selected.ip,
|
|
113
|
+
port: selected.port,
|
|
114
|
+
},
|
|
115
|
+
services: sortedByPriority.map((record) => ({
|
|
116
|
+
ip: record.ip,
|
|
117
|
+
port: record.port,
|
|
118
|
+
})),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const dnsWeights = new Map(
|
|
123
|
+
dnsRecords.map((record) => [
|
|
124
|
+
record.ip,
|
|
125
|
+
{
|
|
126
|
+
weight: record.weight,
|
|
127
|
+
port: record.port,
|
|
128
|
+
priority: record.priority,
|
|
129
|
+
},
|
|
130
|
+
]),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const matchedHealthChecks = healthChecks.filter((check) =>
|
|
134
|
+
dnsWeights.has(check.Service.Address),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (matchedHealthChecks.length === 0) {
|
|
138
|
+
if (this.debug) {
|
|
139
|
+
log.debug(
|
|
140
|
+
"No matching services found between DNS and Consul health checks",
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
const { selected, nextIndex } = this.dnsManager.selectFromSrvRecords(
|
|
144
|
+
highestPriorityRecords,
|
|
145
|
+
algorithm,
|
|
146
|
+
this.currentIndex,
|
|
147
|
+
);
|
|
148
|
+
this.currentIndex = nextIndex;
|
|
149
|
+
|
|
150
|
+
if (!selected) {
|
|
151
|
+
return { selected: null, services: [] };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await this.metricsManager.updateSelectionMetrics(selected.name);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
selected: {
|
|
158
|
+
ip: selected.ip,
|
|
159
|
+
port: selected.port,
|
|
160
|
+
},
|
|
161
|
+
services: sortedByPriority.map((record) => ({
|
|
162
|
+
ip: record.ip,
|
|
163
|
+
port: record.port,
|
|
164
|
+
})),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const highPriorityIPs = new Set(
|
|
169
|
+
highestPriorityRecords.map((record) => record.ip),
|
|
170
|
+
);
|
|
171
|
+
const highPriorityHealthChecks = matchedHealthChecks.filter((check) =>
|
|
172
|
+
highPriorityIPs.has(check.Service.Address),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const targetHealthChecks =
|
|
176
|
+
highPriorityHealthChecks.length > 0
|
|
177
|
+
? highPriorityHealthChecks
|
|
178
|
+
: matchedHealthChecks;
|
|
179
|
+
|
|
180
|
+
const maxDNSWeight = Math.max(...dnsRecords.map((r) => r.weight || 1));
|
|
181
|
+
|
|
182
|
+
const enhancedHealthChecks = targetHealthChecks.map((check) => ({
|
|
183
|
+
...check,
|
|
184
|
+
dnsWeight: combineHealthAndDNSWeights(
|
|
185
|
+
check,
|
|
186
|
+
dnsWeights.get(check.Service.Address)?.weight || 0,
|
|
187
|
+
maxDNSWeight,
|
|
188
|
+
),
|
|
189
|
+
}));
|
|
190
|
+
|
|
191
|
+
const metrics =
|
|
192
|
+
await this.metricsManager.getServicesMetrics(targetHealthChecks);
|
|
193
|
+
let selectedService;
|
|
194
|
+
|
|
195
|
+
switch (algorithm) {
|
|
196
|
+
case SelectionAlgorithm.RoundRobin: {
|
|
197
|
+
const rrResult = roundRobinSelection(
|
|
198
|
+
enhancedHealthChecks,
|
|
199
|
+
this.currentIndex,
|
|
200
|
+
);
|
|
201
|
+
this.currentIndex = rrResult.nextIndex;
|
|
202
|
+
selectedService = { id: rrResult.id, service: rrResult.service };
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
case SelectionAlgorithm.LeastConnection:
|
|
206
|
+
selectedService = leastConnectionSelection(
|
|
207
|
+
enhancedHealthChecks,
|
|
208
|
+
metrics,
|
|
209
|
+
this.metrics,
|
|
210
|
+
);
|
|
211
|
+
break;
|
|
212
|
+
case SelectionAlgorithm.WeightedRoundRobin: {
|
|
213
|
+
const rankedServices = rankServices(
|
|
214
|
+
enhancedHealthChecks,
|
|
215
|
+
metrics,
|
|
216
|
+
this.weights,
|
|
217
|
+
);
|
|
218
|
+
rankedServices.forEach((ranked) => {
|
|
219
|
+
const dnsInfo = dnsWeights.get(ranked.service.Service.Address);
|
|
220
|
+
if (dnsInfo) {
|
|
221
|
+
ranked.score *= 1 + dnsInfo.weight / maxDNSWeight;
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
selectedService = weightedRandomSelection(rankedServices);
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
await this.metricsManager.updateSelectionMetrics(selectedService.id);
|
|
230
|
+
|
|
231
|
+
const selectedDNSInfo = dnsWeights.get(
|
|
232
|
+
selectedService.service.Service.Address,
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
selected: {
|
|
237
|
+
ip: selectedService.service.Service.Address,
|
|
238
|
+
port: selectedDNSInfo?.port || selectedService.service.Service.Port,
|
|
239
|
+
},
|
|
240
|
+
services: matchedHealthChecks.map((check) => {
|
|
241
|
+
const dnsInfo = dnsWeights.get(check.Service.Address);
|
|
242
|
+
return {
|
|
243
|
+
ip: check.Service.Address,
|
|
244
|
+
port: dnsInfo?.port || check.Service.Port,
|
|
245
|
+
};
|
|
246
|
+
}),
|
|
247
|
+
};
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (this.debug) {
|
|
250
|
+
log.error("Error selecting optimal service:", error);
|
|
251
|
+
}
|
|
252
|
+
return { selected: null, services: [] };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async incrementConnections(serviceId) {
|
|
257
|
+
return this.metricsManager.incrementConnections(serviceId);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async decrementConnections(serviceId) {
|
|
261
|
+
return this.metricsManager.decrementConnections(serviceId);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async getSelectionMetrics(serviceId) {
|
|
265
|
+
return this.metricsManager.getSelectionMetrics(serviceId);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async refresh() {
|
|
269
|
+
try {
|
|
270
|
+
if (!this.cacheEnabled) {
|
|
271
|
+
if (this.debug) {
|
|
272
|
+
log.debug("Cache is disabled, no need to refresh");
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const pattern = `${this.cachePrefix}:*`;
|
|
277
|
+
const keys = await this.redis?.keys(pattern);
|
|
278
|
+
|
|
279
|
+
if (keys && keys.length > 0) {
|
|
280
|
+
await this.redis?.del(...keys);
|
|
281
|
+
}
|
|
282
|
+
} catch (error) {
|
|
283
|
+
console.log("Error refreshing Redis caches:", error);
|
|
284
|
+
throw new Error(`Failed to refresh caches: ${error.message}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
exports.Resolver = Resolver;
|
|
290
|
+
exports.SelectionAlgorithm = SelectionAlgorithm;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brimble/consul",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Consul client",
|
|
5
5
|
"main": "./lib",
|
|
6
6
|
"types": "./lib/index.d.ts",
|
|
@@ -8,12 +8,17 @@
|
|
|
8
8
|
"./lib"
|
|
9
9
|
],
|
|
10
10
|
"dependencies": {
|
|
11
|
+
"@brimble/utils": "^1.5.59",
|
|
12
|
+
"dns-query": "^0.11.2",
|
|
13
|
+
"ioredis": "^5.3.2",
|
|
14
|
+
"lru-cache": "^11.1.0",
|
|
11
15
|
"papi": "^1.1.0"
|
|
12
16
|
},
|
|
13
17
|
"devDependencies": {
|
|
14
18
|
"@types/node": "^22.7.6",
|
|
15
19
|
"async": "^3.2.0",
|
|
16
20
|
"debug": "^4.3.1",
|
|
21
|
+
"ioredis-mock": "^8.9.0",
|
|
17
22
|
"jshint": "^2.5.5",
|
|
18
23
|
"mocha": "^10.7.3",
|
|
19
24
|
"nock": "^13.0.7",
|