@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
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { log } = require("@brimble/utils");
|
|
4
|
+
const { query } = require("dns-query");
|
|
5
|
+
const {
|
|
6
|
+
roundRobinSrvSelection,
|
|
7
|
+
weightedSrvRecordSelection,
|
|
8
|
+
} = require("./algorithms");
|
|
9
|
+
const { SelectionAlgorithm } = require("./types");
|
|
10
|
+
|
|
11
|
+
class DNSManager {
|
|
12
|
+
constructor(
|
|
13
|
+
redis,
|
|
14
|
+
cachePrefix,
|
|
15
|
+
cacheTTL,
|
|
16
|
+
cacheEnabled,
|
|
17
|
+
debug,
|
|
18
|
+
dnsEndpoints,
|
|
19
|
+
dnsTimeout,
|
|
20
|
+
dnsRetries,
|
|
21
|
+
) {
|
|
22
|
+
this.redis = redis;
|
|
23
|
+
this.cachePrefix = cachePrefix;
|
|
24
|
+
this.cacheTTL = cacheTTL;
|
|
25
|
+
this.cacheEnabled = cacheEnabled;
|
|
26
|
+
this.debug = debug;
|
|
27
|
+
this.dnsEndpoints = dnsEndpoints;
|
|
28
|
+
this.dnsTimeout = dnsTimeout;
|
|
29
|
+
this.dnsRetries = dnsRetries;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getDNSCacheKey(service) {
|
|
33
|
+
return `${this.cachePrefix}:dns:${service}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async resolveDNS(service) {
|
|
37
|
+
const cacheKey = this.getDNSCacheKey(service);
|
|
38
|
+
|
|
39
|
+
if (this.cacheEnabled) {
|
|
40
|
+
try {
|
|
41
|
+
const cachedData = await Promise.race([
|
|
42
|
+
this.redis?.get(cacheKey),
|
|
43
|
+
new Promise((_, reject) =>
|
|
44
|
+
setTimeout(() => reject(new Error("Redis get timeout")), 200),
|
|
45
|
+
),
|
|
46
|
+
]);
|
|
47
|
+
if (cachedData) {
|
|
48
|
+
if (this.debug) {
|
|
49
|
+
log.debug(`DNS cache hit for ${service}`);
|
|
50
|
+
}
|
|
51
|
+
return JSON.parse(cachedData);
|
|
52
|
+
}
|
|
53
|
+
} catch (e) {
|
|
54
|
+
if (this.debug) log.error("Redis get error or timeout:", e);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const result = await query(
|
|
60
|
+
{
|
|
61
|
+
question: {
|
|
62
|
+
type: "SRV",
|
|
63
|
+
name: service,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
endpoints: this.dnsEndpoints,
|
|
68
|
+
timeout: this.dnsTimeout ?? 1500,
|
|
69
|
+
retries: this.dnsRetries ?? 2,
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (this.debug) {
|
|
74
|
+
log.debug("DNS QUERY RESULT", result);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!result.answers || result.answers.length === 0) {
|
|
78
|
+
if (this.debug) {
|
|
79
|
+
log.debug(`No SRV records found for ${service}`);
|
|
80
|
+
}
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const additionalsByName = {};
|
|
85
|
+
if (result.additionals) {
|
|
86
|
+
for (const additional of result.additionals) {
|
|
87
|
+
if (additional.type === "A") {
|
|
88
|
+
additionalsByName[additional.name] = additional;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const records = result.answers
|
|
93
|
+
.map((answer) => {
|
|
94
|
+
const target = answer.data.target;
|
|
95
|
+
const aRecord = additionalsByName[target];
|
|
96
|
+
return {
|
|
97
|
+
name: target,
|
|
98
|
+
ip: aRecord?.data || "",
|
|
99
|
+
port: answer.data.port,
|
|
100
|
+
priority: answer.data.priority,
|
|
101
|
+
weight: answer.data.weight,
|
|
102
|
+
};
|
|
103
|
+
})
|
|
104
|
+
.filter((record) => record.ip);
|
|
105
|
+
|
|
106
|
+
if (this.cacheEnabled) {
|
|
107
|
+
try {
|
|
108
|
+
await Promise.race([
|
|
109
|
+
this.redis?.set(
|
|
110
|
+
cacheKey,
|
|
111
|
+
JSON.stringify(records),
|
|
112
|
+
"EX",
|
|
113
|
+
this.cacheTTL,
|
|
114
|
+
),
|
|
115
|
+
new Promise((_, reject) =>
|
|
116
|
+
setTimeout(() => reject(new Error("Redis set timeout")), 200),
|
|
117
|
+
),
|
|
118
|
+
]);
|
|
119
|
+
} catch (e) {
|
|
120
|
+
if (this.debug) log.error("Redis set error or timeout:", e);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return records;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (this.debug) {
|
|
127
|
+
log.error("DNS resolution error:", error);
|
|
128
|
+
}
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
sortByPriority(records) {
|
|
134
|
+
return [...records].sort((a, b) => (a.priority || 0) - (b.priority || 0));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
selectFromSrvRecords(records, algorithm, currentIndex) {
|
|
138
|
+
if (!records || records.length === 0) {
|
|
139
|
+
return { selected: null, nextIndex: currentIndex };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
switch (algorithm) {
|
|
143
|
+
case SelectionAlgorithm.RoundRobin: {
|
|
144
|
+
const rrResult = roundRobinSrvSelection(records, currentIndex);
|
|
145
|
+
if (!rrResult) {
|
|
146
|
+
return { selected: null, nextIndex: currentIndex };
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
selected: rrResult.selected,
|
|
150
|
+
nextIndex: rrResult.nextIndex,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
case SelectionAlgorithm.WeightedRoundRobin: {
|
|
155
|
+
const wrResult = weightedSrvRecordSelection(records, currentIndex);
|
|
156
|
+
if (!wrResult) {
|
|
157
|
+
return { selected: null, nextIndex: currentIndex };
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
selected: wrResult.selected,
|
|
161
|
+
nextIndex: wrResult.nextIndex,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
case SelectionAlgorithm.LeastConnection: {
|
|
166
|
+
const lcResult = roundRobinSrvSelection(records, currentIndex);
|
|
167
|
+
if (!lcResult) {
|
|
168
|
+
return { selected: null, nextIndex: currentIndex };
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
selected: lcResult.selected,
|
|
172
|
+
nextIndex: lcResult.nextIndex,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
default:
|
|
177
|
+
return { selected: records[0], nextIndex: currentIndex };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
exports.DNSManager = DNSManager;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { log } = require("@brimble/utils");
|
|
4
|
+
|
|
5
|
+
class HealthCheckManager {
|
|
6
|
+
constructor(consul, redis, cachePrefix, cacheTTL, cacheEnabled, debug) {
|
|
7
|
+
this.consul = consul;
|
|
8
|
+
this.redis = redis;
|
|
9
|
+
this.cachePrefix = cachePrefix;
|
|
10
|
+
this.cacheTTL = cacheTTL;
|
|
11
|
+
this.cacheEnabled = cacheEnabled;
|
|
12
|
+
this.debug = debug;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getHealthCacheKey(service) {
|
|
16
|
+
return `${this.cachePrefix}:health:${service}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async getHealthChecks(service) {
|
|
20
|
+
const cacheKey = this.getHealthCacheKey(service);
|
|
21
|
+
|
|
22
|
+
if (this.cacheEnabled) {
|
|
23
|
+
const cachedHealth = await this.redis?.get(cacheKey);
|
|
24
|
+
if (cachedHealth) {
|
|
25
|
+
return JSON.parse(cachedHealth);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const healthChecks = await this.consul.health.service(service);
|
|
31
|
+
|
|
32
|
+
if (this.cacheEnabled) {
|
|
33
|
+
await this.redis?.set(
|
|
34
|
+
cacheKey,
|
|
35
|
+
JSON.stringify(healthChecks),
|
|
36
|
+
"EX",
|
|
37
|
+
this.cacheTTL,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return healthChecks;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (this.debug) {
|
|
44
|
+
log.error(`Error fetching health checks for ${service}:`, error);
|
|
45
|
+
}
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
exports.HealthCheckManager = HealthCheckManager;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { log } = require("@brimble/utils");
|
|
4
|
+
|
|
5
|
+
class MetricsManager {
|
|
6
|
+
constructor(redis, cachePrefix, metrics, cacheEnabled, debug) {
|
|
7
|
+
this.redis = redis;
|
|
8
|
+
this.cachePrefix = cachePrefix;
|
|
9
|
+
this.metrics = metrics;
|
|
10
|
+
this.cacheEnabled = cacheEnabled;
|
|
11
|
+
this.debug = debug;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getConnectionKey(serviceId) {
|
|
15
|
+
return `${this.cachePrefix}:connections:${serviceId}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async getServicesMetrics(services) {
|
|
19
|
+
const metricsMap = new Map();
|
|
20
|
+
const pipeline = this.redis?.pipeline();
|
|
21
|
+
|
|
22
|
+
if (this.cacheEnabled) {
|
|
23
|
+
services.forEach((service) => {
|
|
24
|
+
pipeline?.get(service.Service.ID);
|
|
25
|
+
pipeline?.get(this.getConnectionKey(service.Service.ID));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const results = await pipeline?.exec();
|
|
31
|
+
if (!results) {
|
|
32
|
+
services.forEach((service) => {
|
|
33
|
+
metricsMap.set(service.Service.ID, { ...this.metrics });
|
|
34
|
+
});
|
|
35
|
+
return metricsMap;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
services.forEach((service, index) => {
|
|
39
|
+
const serviceId = service.Service.ID;
|
|
40
|
+
const metricsResult = results[index * 2];
|
|
41
|
+
const connectionsResult = results[index * 2 + 1];
|
|
42
|
+
|
|
43
|
+
let metrics;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
if (metricsResult?.[1]) {
|
|
47
|
+
metrics = JSON.parse(metricsResult[1]);
|
|
48
|
+
} else {
|
|
49
|
+
metrics = { ...this.metrics };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (connectionsResult?.[1]) {
|
|
53
|
+
const connData = JSON.parse(connectionsResult[1]);
|
|
54
|
+
metrics.activeConnections = connData.activeConnections || 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
metricsMap.set(serviceId, metrics);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (this.debug) {
|
|
60
|
+
log.error(
|
|
61
|
+
`Error processing metrics for service ${serviceId}:`,
|
|
62
|
+
error,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
metricsMap.set(serviceId, { ...this.metrics });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (this.debug) {
|
|
70
|
+
log.error("Error executing Redis pipeline:", error);
|
|
71
|
+
}
|
|
72
|
+
services.forEach((service) => {
|
|
73
|
+
metricsMap.set(service.Service.ID, { ...this.metrics });
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return metricsMap;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async incrementConnections(serviceId) {
|
|
81
|
+
try {
|
|
82
|
+
const connectionKey = this.getConnectionKey(serviceId);
|
|
83
|
+
const existingMetrics = await this.redis?.get(connectionKey);
|
|
84
|
+
let metrics;
|
|
85
|
+
|
|
86
|
+
if (existingMetrics) {
|
|
87
|
+
metrics = JSON.parse(existingMetrics);
|
|
88
|
+
metrics.activeConnections = (metrics.activeConnections || 0) + 1;
|
|
89
|
+
} else {
|
|
90
|
+
metrics = {
|
|
91
|
+
...this.metrics,
|
|
92
|
+
activeConnections: 1,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (this.cacheEnabled) {
|
|
97
|
+
await this.redis?.set(
|
|
98
|
+
connectionKey,
|
|
99
|
+
JSON.stringify(metrics),
|
|
100
|
+
"EX",
|
|
101
|
+
24 * 60 * 60,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (this.debug) {
|
|
106
|
+
log.error(
|
|
107
|
+
`Failed to increment connections for service ${serviceId}:`,
|
|
108
|
+
error,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async decrementConnections(serviceId) {
|
|
115
|
+
try {
|
|
116
|
+
const connectionKey = this.getConnectionKey(serviceId);
|
|
117
|
+
const existingMetrics = await this.redis?.get(connectionKey);
|
|
118
|
+
let metrics;
|
|
119
|
+
|
|
120
|
+
if (existingMetrics) {
|
|
121
|
+
metrics = JSON.parse(existingMetrics);
|
|
122
|
+
metrics.activeConnections = Math.max(
|
|
123
|
+
0,
|
|
124
|
+
(metrics.activeConnections || 1) - 1,
|
|
125
|
+
);
|
|
126
|
+
} else {
|
|
127
|
+
metrics = {
|
|
128
|
+
...this.metrics,
|
|
129
|
+
activeConnections: 0,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (this.cacheEnabled) {
|
|
134
|
+
await this.redis?.set(
|
|
135
|
+
connectionKey,
|
|
136
|
+
JSON.stringify(metrics),
|
|
137
|
+
"EX",
|
|
138
|
+
24 * 60 * 60,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (this.debug) {
|
|
143
|
+
log.error(
|
|
144
|
+
`Failed to decrement connections for service ${serviceId}:`,
|
|
145
|
+
error,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async updateSelectionMetrics(serviceId) {
|
|
152
|
+
try {
|
|
153
|
+
const connectionKey = this.getConnectionKey(serviceId);
|
|
154
|
+
const existingMetrics = await this.redis?.get(connectionKey);
|
|
155
|
+
let metrics;
|
|
156
|
+
|
|
157
|
+
if (existingMetrics) {
|
|
158
|
+
metrics = JSON.parse(existingMetrics);
|
|
159
|
+
} else {
|
|
160
|
+
metrics = { ...this.metrics };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
metrics.lastSelectedTime = Date.now();
|
|
164
|
+
|
|
165
|
+
if (this.cacheEnabled) {
|
|
166
|
+
await this.redis?.set(
|
|
167
|
+
connectionKey,
|
|
168
|
+
JSON.stringify(metrics),
|
|
169
|
+
"EX",
|
|
170
|
+
24 * 60 * 60,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (this.debug) {
|
|
175
|
+
log.error(
|
|
176
|
+
`Failed to update selection metrics for service ${serviceId}:`,
|
|
177
|
+
error,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async getSelectionMetrics(serviceId) {
|
|
184
|
+
try {
|
|
185
|
+
if (!this.cacheEnabled) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
const metrics = await this.redis?.get(this.getConnectionKey(serviceId));
|
|
189
|
+
return metrics ? JSON.parse(metrics) : null;
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (this.debug) {
|
|
192
|
+
log.error("Error getting service metrics:", error);
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
exports.MetricsManager = MetricsManager;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { DEFAULT_WEIGHTS } = require("./types");
|
|
4
|
+
|
|
5
|
+
function calculateHealthScore(service) {
|
|
6
|
+
const checks = service.Checks;
|
|
7
|
+
const totalChecks = checks.length;
|
|
8
|
+
if (totalChecks === 0) return 0;
|
|
9
|
+
|
|
10
|
+
const passingChecks = checks.filter(
|
|
11
|
+
(check) => check.Status === "passing",
|
|
12
|
+
).length;
|
|
13
|
+
return passingChecks / totalChecks;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeScore(value, max, inverse = false) {
|
|
17
|
+
const normalized = Math.max(0, Math.min(1, value / max));
|
|
18
|
+
return inverse ? 1 - normalized : normalized;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function calculateResourceScore(metrics) {
|
|
22
|
+
const cpuScore = normalizeScore(metrics.cpuUsage, 100, true);
|
|
23
|
+
const memoryScore = normalizeScore(metrics.memoryUsage, 100, true);
|
|
24
|
+
return (cpuScore + memoryScore) / 2;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function calculateDistributionScore(lastSelectedTime) {
|
|
28
|
+
if (!lastSelectedTime) return 1;
|
|
29
|
+
|
|
30
|
+
const timeSinceLastSelection = Date.now() - lastSelectedTime;
|
|
31
|
+
|
|
32
|
+
return Math.min(timeSinceLastSelection / (5 * 60 * 1000), 1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function rankServices(services, metrics, weights = DEFAULT_WEIGHTS) {
|
|
36
|
+
return services
|
|
37
|
+
.map((service) => {
|
|
38
|
+
const serviceId = service.Service.ID;
|
|
39
|
+
|
|
40
|
+
const serviceMetrics = metrics.get(serviceId);
|
|
41
|
+
|
|
42
|
+
if (!serviceMetrics) {
|
|
43
|
+
throw new Error(`No metrics found for service ${serviceId}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const healthScore = calculateHealthScore(service);
|
|
47
|
+
const responseTimeScore = normalizeScore(
|
|
48
|
+
serviceMetrics.responseTime,
|
|
49
|
+
500,
|
|
50
|
+
true,
|
|
51
|
+
);
|
|
52
|
+
const errorRateScore = normalizeScore(
|
|
53
|
+
serviceMetrics.errorRate,
|
|
54
|
+
100,
|
|
55
|
+
true,
|
|
56
|
+
);
|
|
57
|
+
const resourceScore = calculateResourceScore(serviceMetrics);
|
|
58
|
+
const connectionScore = normalizeScore(
|
|
59
|
+
serviceMetrics.activeConnections,
|
|
60
|
+
1000,
|
|
61
|
+
true,
|
|
62
|
+
);
|
|
63
|
+
const distributionScore = calculateDistributionScore(
|
|
64
|
+
serviceMetrics.lastSelectedTime,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const totalScore =
|
|
68
|
+
healthScore * weights.health +
|
|
69
|
+
responseTimeScore * weights.responseTime +
|
|
70
|
+
errorRateScore * weights.errorRate +
|
|
71
|
+
resourceScore * weights.resources +
|
|
72
|
+
connectionScore * weights.connections +
|
|
73
|
+
distributionScore * weights.distribution;
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
score: totalScore,
|
|
77
|
+
id: serviceId,
|
|
78
|
+
service,
|
|
79
|
+
};
|
|
80
|
+
})
|
|
81
|
+
.sort((a, b) => b.score - a.score);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function combineHealthAndDNSWeights(service, dnsWeight, maxDNSWeight) {
|
|
85
|
+
const healthScore = calculateHealthScore(service);
|
|
86
|
+
const normalizedDNSWeight = dnsWeight / maxDNSWeight;
|
|
87
|
+
return healthScore * 0.7 + normalizedDNSWeight * 0.3;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
exports.calculateHealthScore = calculateHealthScore;
|
|
91
|
+
exports.calculateResourceScore = calculateResourceScore;
|
|
92
|
+
exports.calculateDistributionScore = calculateDistributionScore;
|
|
93
|
+
exports.normalizeScore = normalizeScore;
|
|
94
|
+
exports.rankServices = rankServices;
|
|
95
|
+
exports.combineHealthAndDNSWeights = combineHealthAndDNSWeights;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_WEIGHTS = {
|
|
4
|
+
health: 0.25,
|
|
5
|
+
responseTime: 0.2,
|
|
6
|
+
errorRate: 0.2,
|
|
7
|
+
resources: 0.15,
|
|
8
|
+
connections: 0.1,
|
|
9
|
+
distribution: 0.1,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const DEFAULT_METRICS = {
|
|
13
|
+
responseTime: 100,
|
|
14
|
+
errorRate: 0,
|
|
15
|
+
cpuUsage: 50,
|
|
16
|
+
memoryUsage: 50,
|
|
17
|
+
activeConnections: 0,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const SelectionAlgorithm = {
|
|
21
|
+
RoundRobin: "round-robin",
|
|
22
|
+
LeastConnection: "least-connection",
|
|
23
|
+
WeightedRoundRobin: "weighted-round-robin",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
exports.DEFAULT_WEIGHTS = DEFAULT_WEIGHTS;
|
|
27
|
+
exports.DEFAULT_METRICS = DEFAULT_METRICS;
|
|
28
|
+
exports.SelectionAlgorithm = SelectionAlgorithm;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Agent as httpAgent } from "http";
|
|
2
|
+
import { Agent as httpsAgent } from "https";
|
|
3
|
+
import { Redis as IORedis } from "ioredis";
|
|
4
|
+
import { Consul } from "./consul";
|
|
5
|
+
|
|
6
|
+
export interface ConsulResolverConfig {
|
|
7
|
+
redis?: IORedis;
|
|
8
|
+
cacheEnabled: boolean;
|
|
9
|
+
cachePrefix: string;
|
|
10
|
+
debug?: boolean;
|
|
11
|
+
weights?: {
|
|
12
|
+
health: number;
|
|
13
|
+
responseTime: number;
|
|
14
|
+
errorRate: number;
|
|
15
|
+
resources: number;
|
|
16
|
+
connections: number;
|
|
17
|
+
distribution: number;
|
|
18
|
+
};
|
|
19
|
+
metrics?: {
|
|
20
|
+
responseTime: number;
|
|
21
|
+
errorRate: number;
|
|
22
|
+
cpuUsage: number;
|
|
23
|
+
memoryUsage: number;
|
|
24
|
+
activeConnections: number;
|
|
25
|
+
};
|
|
26
|
+
cacheTTL?: number;
|
|
27
|
+
dnsEndpoints?: string[];
|
|
28
|
+
dnsTimeout?: number;
|
|
29
|
+
dnsRetries?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ServiceInfo {
|
|
33
|
+
ip: string;
|
|
34
|
+
port: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface OptimalServiceResult {
|
|
38
|
+
selected: ServiceInfo | null;
|
|
39
|
+
services: ServiceInfo[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ServiceMetrics {
|
|
43
|
+
responseTime: number;
|
|
44
|
+
errorRate: number;
|
|
45
|
+
cpuUsage: number;
|
|
46
|
+
memoryUsage: number;
|
|
47
|
+
activeConnections: number;
|
|
48
|
+
lastSelectedTime?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export enum SelectionAlgorithm {
|
|
52
|
+
RoundRobin = "round-robin",
|
|
53
|
+
LeastConnection = "least-connection",
|
|
54
|
+
WeightedRoundRobin = "weighted-round-robin",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
declare class Resolver {
|
|
58
|
+
constructor(consul: Consul, config: ConsulResolverConfig);
|
|
59
|
+
|
|
60
|
+
consul: Consul;
|
|
61
|
+
|
|
62
|
+
selectOptimalService(
|
|
63
|
+
service: string,
|
|
64
|
+
algorithm?: SelectionAlgorithm,
|
|
65
|
+
): Promise<OptimalServiceResult>;
|
|
66
|
+
|
|
67
|
+
incrementConnections(serviceId: string): Promise<void>;
|
|
68
|
+
|
|
69
|
+
decrementConnections(serviceId: string): Promise<void>;
|
|
70
|
+
|
|
71
|
+
getSelectionMetrics(serviceId: string): Promise<ServiceMetrics | null>;
|
|
72
|
+
|
|
73
|
+
refresh(): Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export { Resolver };
|