Skip to content

Commit 3496526

Browse files
authored
[BUG] fix healthcheck logic to expect object and return ids (#2277)
Original implementation incorrectly assumed the return list of nodes was an object array. This PR: #2232 Addressed the return but didn't catch the nodes.find in the return which is a function for an array. Also, refactors to return a list of node_ids because the original implementation indicated that it should but it can return node ids but it never did. It only returned `null` or `_local`, the problem with this approach is that it doesn't expect valid node version with different DIs or filter out nodes when we pass `null` as the node ID for the node info call because it was fan out the request to all nodes. Now this function will return `_local` if all the nodes share the same cluster_id using a greedy approach since we can assume it is all the same version. Will return node ids, if the cluster_id are different so it will pass a CSV to the node info call and return the info for those nodes. And null if no cluster_id is present, ie, fan out the response. Original issue: #2203 Signed-off-by: Kawika Avilla <kavilla414@gmail.com>
1 parent 4a06f5a commit 3496526

File tree

2 files changed

+53
-47
lines changed

2 files changed

+53
-47
lines changed

src/core/server/opensearch/version_check/ensure_opensearch_version.test.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -253,18 +253,18 @@ describe('pollOpenSearchNodesVersion', () => {
253253
it('returns compatibility results and isCompatible=true with filters', (done) => {
254254
expect.assertions(2);
255255
const target = {
256-
id: '0',
256+
cluster_id: '0',
257257
attribute: 'foo',
258258
};
259259
const filter = {
260-
id: '1',
260+
cluster_id: '1',
261261
attribute: 'bar',
262262
};
263263

264264
// will filter out every odd index
265265
const nodes = createNodesWithAttribute(
266-
target.id,
267-
filter.id,
266+
target.cluster_id,
267+
filter.cluster_id,
268268
target.attribute,
269269
filter.attribute,
270270
'5.1.0',
@@ -273,14 +273,18 @@ describe('pollOpenSearchNodesVersion', () => {
273273
'5.1.1-Beta1'
274274
);
275275

276+
const filteredNodes = nodes;
277+
delete filteredNodes.nodes['node-1'];
278+
delete filteredNodes.nodes['node-3'];
279+
276280
// @ts-expect-error we need to return an incompatible type to use the testScheduler here
277281
internalClient.cluster.state.mockReturnValueOnce({ body: nodes });
278282

279283
nodeInfosSuccessOnce(nodes);
280284

281285
pollOpenSearchNodesVersion({
282286
internalClient,
283-
optimizedHealthcheck: { id: target.id, filters: { custom_attribute: filter.attribute } },
287+
optimizedHealthcheck: { id: 'cluster_id', filters: { custom_attribute: filter.attribute } },
284288
opensearchVersionCheckInterval: 1,
285289
ignoreVersionMismatch: false,
286290
opensearchDashboardsVersion: OPENSEARCH_DASHBOARDS_VERSION,
@@ -290,7 +294,7 @@ describe('pollOpenSearchNodesVersion', () => {
290294
.subscribe({
291295
next: (result) => {
292296
expect(result).toEqual(
293-
mapNodesVersionCompatibility(nodes, OPENSEARCH_DASHBOARDS_VERSION, false)
297+
mapNodesVersionCompatibility(filteredNodes, OPENSEARCH_DASHBOARDS_VERSION, false)
294298
);
295299
expect(result.isCompatible).toBe(true);
296300
},
@@ -302,18 +306,18 @@ describe('pollOpenSearchNodesVersion', () => {
302306
it('returns compatibility results and isCompatible=false with filters', (done) => {
303307
expect.assertions(2);
304308
const target = {
305-
id: '0',
309+
cluster_id: '0',
306310
attribute: 'foo',
307311
};
308312
const filter = {
309-
id: '1',
313+
cluster_id: '1',
310314
attribute: 'bar',
311315
};
312316

313317
// will filter out every odd index
314318
const nodes = createNodesWithAttribute(
315-
target.id,
316-
filter.id,
319+
target.cluster_id,
320+
filter.cluster_id,
317321
target.attribute,
318322
filter.attribute,
319323
'5.1.0',
@@ -322,14 +326,18 @@ describe('pollOpenSearchNodesVersion', () => {
322326
'5.1.1-Beta1'
323327
);
324328

329+
const filteredNodes = nodes;
330+
delete filteredNodes.nodes['node-1'];
331+
delete filteredNodes.nodes['node-3'];
332+
325333
// @ts-expect-error we need to return an incompatible type to use the testScheduler here
326334
internalClient.cluster.state.mockReturnValueOnce({ body: nodes });
327335

328336
nodeInfosSuccessOnce(nodes);
329337

330338
pollOpenSearchNodesVersion({
331339
internalClient,
332-
optimizedHealthcheck: { id: target.id, filters: { custom_attribute: filter.attribute } },
340+
optimizedHealthcheck: { id: 'cluster_id', filters: { custom_attribute: filter.attribute } },
333341
opensearchVersionCheckInterval: 1,
334342
ignoreVersionMismatch: false,
335343
opensearchDashboardsVersion: OPENSEARCH_DASHBOARDS_VERSION,
@@ -339,7 +347,7 @@ describe('pollOpenSearchNodesVersion', () => {
339347
.subscribe({
340348
next: (result) => {
341349
expect(result).toEqual(
342-
mapNodesVersionCompatibility(nodes, OPENSEARCH_DASHBOARDS_VERSION, false)
350+
mapNodesVersionCompatibility(filteredNodes, OPENSEARCH_DASHBOARDS_VERSION, false)
343351
);
344352
expect(result.isCompatible).toBe(false);
345353
},

src/core/server/opensearch/version_check/ensure_opensearch_version.ts

Lines changed: 33 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535

3636
import { timer, of, from, Observable } from 'rxjs';
3737
import { map, distinctUntilChanged, catchError, exhaustMap, mergeMap } from 'rxjs/operators';
38-
import { get } from 'lodash';
3938
import { ApiResponse } from '@opensearch-project/opensearch';
4039
import {
4140
opensearchVersionCompatibleWithOpenSearchDashboards,
@@ -49,67 +48,66 @@ import type { OpenSearchClient } from '../client';
4948
* that is supplied through the healthcheck param. This node attribute is configurable
5049
* in opensearch_dashboards.yml. It can also filter attributes out by key-value pair.
5150
* If all nodes have the same cluster id then we do not fan out the healthcheck and use '_local' node
52-
* If there are multiple cluster ids then we use the default fan out behavior
51+
* If there are multiple cluster ids then we return an array of node ids to check.
5352
* If the supplied node attribute is missing then we return null and use default fan out behavior
5453
* @param {OpenSearchClient} internalClient
5554
* @param {OptimizedHealthcheck} healthcheck
56-
* @returns {string|null} '_local' if all nodes have the same cluster_id, otherwise null
55+
* @returns {string|string[]|null} '_local' if all nodes have the same cluster_id, array of node ids if different cluster_id, null if no cluster_id or nodes returned
5756
*/
5857
export const getNodeId = async (
5958
internalClient: OpenSearchClient,
6059
healthcheck: OptimizedHealthcheck
61-
): Promise<string | null> => {
60+
): Promise<'_local' | string[] | null> => {
6261
try {
62+
// If missing an id, we have nothing to check
63+
if (!healthcheck.id) return null;
64+
6365
let path = `nodes.*.attributes.${healthcheck.id}`;
6466
const filters = healthcheck.filters;
65-
if (filters) {
66-
Object.keys(filters).forEach((key) => {
67-
path += `,nodes.*.attributes.${key}`;
68-
});
67+
const filterKeys = filters ? Object.keys(filters) : [];
68+
69+
for (const key of filterKeys) {
70+
path += `,nodes.*.attributes.${key}`;
6971
}
7072

73+
/*
74+
* Using _cluster/state/nodes to retrieve the cluster_id of each node from cluster manager node which
75+
* is considered to be a lightweight operation to aggegrate different cluster_ids from the OpenSearch nodes.
76+
*/
7177
const state = (await internalClient.cluster.state({
7278
metric: 'nodes',
7379
filter_path: [path],
7480
})) as ApiResponse;
75-
/* Aggregate different cluster_ids from the OpenSearch nodes
76-
* if all the nodes have the same cluster_id, retrieve nodes.info from _local node only
77-
* Using _cluster/state/nodes to retrieve the cluster_id of each node from cluster manager node which is considered to be a lightweight operation
78-
* else if the nodes have different cluster_ids then fan out the request to all nodes
79-
* else there are no nodes in the cluster
80-
*/
81+
8182
const nodes = state.body.nodes;
82-
let nodeIds = Object.keys(nodes);
83-
if (nodeIds.length === 0) {
84-
return null;
85-
}
83+
const nodeIds = new Set(Object.keys(nodes));
8684

8785
/*
8886
* If filters are set look for the key and value and filter out any node that matches
8987
* the value for that attribute.
9088
*/
91-
if (filters) {
92-
nodeIds.forEach((id) => {
93-
Object.keys(filters).forEach((key) => {
94-
const attributeValue = get(nodes[id], `attributes.${key}`, null);
95-
if (attributeValue === filters[key]) {
96-
delete nodes[id];
97-
}
98-
});
99-
});
89+
for (const id of nodeIds) {
90+
for (const key of filterKeys) {
91+
const attributeValue = nodes[id].attributes?.[key] ?? null;
10092

101-
nodeIds = Object.keys(nodes);
102-
if (nodeIds.length === 0) {
103-
return null;
93+
if (attributeValue === filters![key]) nodeIds.delete(id);
10494
}
10595
}
10696

107-
const sharedClusterId = get(nodes[nodeIds[0]], `attributes.${healthcheck.id}`, null);
97+
if (nodeIds.size === 0) return null;
98+
99+
const [firstNodeId] = nodeIds;
100+
const sharedClusterId = nodes[firstNodeId].attributes?.[healthcheck.id] ?? null;
101+
// If cluster_id is not set then fan out
102+
if (sharedClusterId === null) return null;
103+
104+
// If a node is found to have a different cluster_id, return node ids
105+
for (const id of nodeIds) {
106+
if (nodes[id].attributes?.[healthcheck.id] !== sharedClusterId) return Array.from(nodeIds);
107+
}
108108

109-
return sharedClusterId === null ||
110-
nodes.find((node: any) => sharedClusterId !== get(node, `attributes.${healthcheck.id}`, null))
111-
? null
112-
: '_local';
109+
// When all nodes share the same cluster_id, return _local
110+
return '_local';
113111
} catch (e) {
114112
return null;
115113
}

0 commit comments

Comments
 (0)