@terascope/elasticsearch-api 3.3.1 → 3.3.3
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/index.js +77 -59
- package/package.json +5 -3
- package/test/bulk-send-limit-spec.js +28 -8
- package/test/retry-spec.js +38 -0
- package/types/index.d.ts +15 -15
- package/test/helpers/config.js +0 -22
- package/test/helpers/data.js +0 -10023
- package/test/helpers/elasticsearch.js +0 -130
package/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* eslint-disable camelcase */
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
// polyfill because opensearch has references to an api that won't exist
|
|
@@ -6,31 +8,15 @@ require('setimmediate');
|
|
|
6
8
|
|
|
7
9
|
const Promise = require('bluebird');
|
|
8
10
|
const {
|
|
9
|
-
isTest,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
get,
|
|
16
|
-
toNumber,
|
|
17
|
-
isString,
|
|
18
|
-
isSimpleObject,
|
|
19
|
-
castArray,
|
|
20
|
-
flatten,
|
|
21
|
-
toBoolean,
|
|
22
|
-
uniq,
|
|
23
|
-
random,
|
|
24
|
-
cloneDeep,
|
|
25
|
-
DataEntity,
|
|
26
|
-
isDeepEqual,
|
|
27
|
-
getTypeOf,
|
|
28
|
-
isProd
|
|
11
|
+
isTest, TSError, isFatalError,
|
|
12
|
+
parseError, getBackoffDelay, isRetryableError,
|
|
13
|
+
get, toNumber, isString, isSimpleObject,
|
|
14
|
+
castArray, flatten, toBoolean,
|
|
15
|
+
uniq, random, cloneDeep, DataEntity,
|
|
16
|
+
isDeepEqual, getTypeOf, isProd
|
|
29
17
|
} = require('@terascope/utils');
|
|
30
18
|
const { ElasticsearchDistribution } = require('@terascope/types');
|
|
31
19
|
|
|
32
|
-
const { inspect } = require('util');
|
|
33
|
-
|
|
34
20
|
const DOCUMENT_EXISTS = 409;
|
|
35
21
|
const TOO_MANY_REQUESTS = 429;
|
|
36
22
|
|
|
@@ -55,9 +41,13 @@ module.exports = function elasticsearchApi(client, logger, _opConfig) {
|
|
|
55
41
|
|
|
56
42
|
const { connection = 'unknown' } = config;
|
|
57
43
|
|
|
58
|
-
function count(query) {
|
|
44
|
+
async function count(query) {
|
|
59
45
|
query.size = 0;
|
|
60
|
-
|
|
46
|
+
const response = await _searchES(query);
|
|
47
|
+
|
|
48
|
+
const data = get(response, 'hits.total.value', get(response, 'hits.total'));
|
|
49
|
+
|
|
50
|
+
return data;
|
|
61
51
|
}
|
|
62
52
|
|
|
63
53
|
function convertDocToDataEntity(doc) {
|
|
@@ -80,18 +70,24 @@ module.exports = function elasticsearchApi(client, logger, _opConfig) {
|
|
|
80
70
|
}
|
|
81
71
|
|
|
82
72
|
function search(query) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
73
|
+
const {
|
|
74
|
+
_sourceInclude, _source_includes,
|
|
75
|
+
_sourceExclude, _source_excludes,
|
|
76
|
+
...safeQuery
|
|
77
|
+
} = query;
|
|
78
|
+
|
|
79
|
+
const sourceIncludes = _sourceInclude || _source_includes;
|
|
80
|
+
const sourceExcludes = _sourceExclude || _source_excludes;
|
|
81
|
+
|
|
82
|
+
if (sourceIncludes) {
|
|
83
|
+
safeQuery._source_includes = sourceIncludes;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (sourceExcludes) {
|
|
87
|
+
safeQuery._source_excludes = sourceExcludes;
|
|
92
88
|
}
|
|
93
89
|
|
|
94
|
-
return _searchES(
|
|
90
|
+
return _searchES(safeQuery).then((data) => {
|
|
95
91
|
if (config.full_response) {
|
|
96
92
|
return data;
|
|
97
93
|
}
|
|
@@ -342,7 +338,7 @@ module.exports = function elasticsearchApi(client, logger, _opConfig) {
|
|
|
342
338
|
if (record.action == null) {
|
|
343
339
|
let dbg = '';
|
|
344
340
|
if (!isProd) {
|
|
345
|
-
dbg = `, dbg: ${
|
|
341
|
+
dbg = `, dbg: ${JSON.stringify({ record, index })}`;
|
|
346
342
|
}
|
|
347
343
|
throw new Error(`Bulk send record is missing the action property${dbg}`);
|
|
348
344
|
}
|
|
@@ -796,19 +792,33 @@ module.exports = function elasticsearchApi(client, logger, _opConfig) {
|
|
|
796
792
|
});
|
|
797
793
|
}
|
|
798
794
|
|
|
795
|
+
function isConnectionErrorMessage(err) {
|
|
796
|
+
const msg = get(err, 'message', '');
|
|
797
|
+
return msg.includes('No Living connections') || msg.includes('ECONNREFUSED');
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function isErrorRetryable(err) {
|
|
801
|
+
const checkErrorMsg = isRetryableError(err);
|
|
802
|
+
|
|
803
|
+
if (checkErrorMsg) {
|
|
804
|
+
return true;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const isRejectedError = get(err, 'body.error.type') === 'es_rejected_execution_exception';
|
|
808
|
+
const isConnectionError = isConnectionErrorMessage(err);
|
|
809
|
+
|
|
810
|
+
if (isRejectedError || isConnectionError) {
|
|
811
|
+
return true;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
|
|
799
817
|
function _errorHandler(fn, data, reject, fnName = '->unknown()') {
|
|
800
818
|
const retry = _retryFn(fn, data, reject);
|
|
819
|
+
|
|
801
820
|
return function _errorHandlerFn(err) {
|
|
802
|
-
|
|
803
|
-
if (isRetryableError(err)) {
|
|
804
|
-
retryable = true;
|
|
805
|
-
} else {
|
|
806
|
-
const isRejectedError = get(err, 'body.error.type') === 'es_rejected_execution_exception';
|
|
807
|
-
const isConnectionError = get(err, 'message', '').includes('No Living connections');
|
|
808
|
-
if (isRejectedError || isConnectionError) {
|
|
809
|
-
retryable = true;
|
|
810
|
-
}
|
|
811
|
-
}
|
|
821
|
+
const retryable = isErrorRetryable(err);
|
|
812
822
|
|
|
813
823
|
if (retryable) {
|
|
814
824
|
retry();
|
|
@@ -906,8 +916,12 @@ module.exports = function elasticsearchApi(client, logger, _opConfig) {
|
|
|
906
916
|
* please use getClientMetadata
|
|
907
917
|
* */
|
|
908
918
|
function getESVersion() {
|
|
909
|
-
const newClientVersion = get(client, '__meta.
|
|
910
|
-
|
|
919
|
+
const newClientVersion = get(client, '__meta.majorVersion');
|
|
920
|
+
|
|
921
|
+
if (newClientVersion) return newClientVersion;
|
|
922
|
+
|
|
923
|
+
// legacy
|
|
924
|
+
const esVersion = get(client, 'transport._config.apiVersion', '6.5');
|
|
911
925
|
|
|
912
926
|
if (esVersion && isString(esVersion)) {
|
|
913
927
|
const [majorVersion] = esVersion.split('.');
|
|
@@ -918,28 +932,30 @@ module.exports = function elasticsearchApi(client, logger, _opConfig) {
|
|
|
918
932
|
}
|
|
919
933
|
|
|
920
934
|
function getClientMetadata() {
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
935
|
+
if (client.__meta) {
|
|
936
|
+
return client.__meta;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const esVersion = get(client, 'transport._config.apiVersion', '6.5');
|
|
940
|
+
const distribution = ElasticsearchDistribution.elasticsearch;
|
|
941
|
+
const [majorVersion = 6, minorVersion = 5] = esVersion.split('.').map(toNumber);
|
|
924
942
|
|
|
925
943
|
return {
|
|
926
944
|
distribution,
|
|
927
|
-
version: esVersion
|
|
945
|
+
version: esVersion,
|
|
946
|
+
majorVersion,
|
|
947
|
+
minorVersion
|
|
928
948
|
};
|
|
929
949
|
}
|
|
930
950
|
|
|
931
951
|
function isElasticsearch6() {
|
|
932
|
-
const { distribution,
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
return distribution === ElasticsearchDistribution.elasticsearch && parsedVersion === 6;
|
|
952
|
+
const { distribution, majorVersion } = getClientMetadata();
|
|
953
|
+
return distribution === ElasticsearchDistribution.elasticsearch && majorVersion === 6;
|
|
936
954
|
}
|
|
937
955
|
|
|
938
956
|
function isElasticsearch8() {
|
|
939
|
-
const { distribution,
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
return distribution === ElasticsearchDistribution.elasticsearch && parsedVersion === 8;
|
|
957
|
+
const { distribution, majorVersion } = getClientMetadata();
|
|
958
|
+
return distribution === ElasticsearchDistribution.elasticsearch && majorVersion === 8;
|
|
943
959
|
}
|
|
944
960
|
|
|
945
961
|
function _fixMappingRequest(_params, isTemplate) {
|
|
@@ -1245,11 +1261,13 @@ module.exports = function elasticsearchApi(client, logger, _opConfig) {
|
|
|
1245
1261
|
validateGeoParameters,
|
|
1246
1262
|
getClientMetadata,
|
|
1247
1263
|
isElasticsearch6,
|
|
1264
|
+
isElasticsearch8,
|
|
1248
1265
|
// The APIs below are deprecated and should be removed.
|
|
1249
1266
|
index_exists: indexExists,
|
|
1250
1267
|
index_create: indexCreate,
|
|
1251
1268
|
index_refresh: indexRefresh,
|
|
1252
1269
|
index_recovery: indexRecovery,
|
|
1253
1270
|
getESVersion,
|
|
1271
|
+
isErrorRetryable
|
|
1254
1272
|
};
|
|
1255
1273
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terascope/elasticsearch-api",
|
|
3
3
|
"displayName": "Elasticsearch API",
|
|
4
|
-
"version": "3.3.
|
|
4
|
+
"version": "3.3.3",
|
|
5
5
|
"description": "Elasticsearch client api used across multiple services, handles retries and exponential backoff",
|
|
6
6
|
"homepage": "https://github.com/terascope/teraslice/tree/master/packages/elasticsearch-api#readme",
|
|
7
7
|
"bugs": {
|
|
@@ -18,18 +18,20 @@
|
|
|
18
18
|
"test:8": "ELASTICSEARCH_VERSION='8.1.2' yarn run test",
|
|
19
19
|
"test:debug": "ts-scripts test --debug . --",
|
|
20
20
|
"test:legacy": "LEGACY_CLIENT=true yarn run test",
|
|
21
|
-
"test:opensearch": "TEST_OPENSEARCH='true' yarn run test",
|
|
21
|
+
"test:opensearch": "TEST_OPENSEARCH='true' RESTRAINED_OPENSEARCH='true' yarn run test",
|
|
22
22
|
"test:watch": "ts-scripts test --watch . --"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
+
"@terascope/types": "^0.11.0",
|
|
25
26
|
"@terascope/utils": "^0.45.1",
|
|
26
27
|
"bluebird": "^3.7.2",
|
|
27
28
|
"setimmediate": "^1.0.5"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
|
-
"@opensearch-project/opensearch": "^1.0
|
|
31
|
+
"@opensearch-project/opensearch": "^1.1.0",
|
|
31
32
|
"@types/elasticsearch": "^5.0.40",
|
|
32
33
|
"elasticsearch": "^15.4.1",
|
|
34
|
+
"elasticsearch-store": "^0.65.1",
|
|
33
35
|
"elasticsearch6": "npm:@elastic/elasticsearch@^6.7.0",
|
|
34
36
|
"elasticsearch7": "npm:@elastic/elasticsearch@^7.0.0",
|
|
35
37
|
"elasticsearch8": "npm:@elastic/elasticsearch@^8.0.0"
|
|
@@ -1,29 +1,49 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { debugLogger, chunk, pMap } = require('@terascope/utils');
|
|
4
|
+
const { ElasticsearchTestHelpers } = require('elasticsearch-store');
|
|
4
5
|
const elasticsearchAPI = require('../index');
|
|
5
|
-
|
|
6
|
-
const { TEST_INDEX_PREFIX } = require('./helpers/config');
|
|
6
|
+
|
|
7
7
|
const {
|
|
8
|
-
makeClient, cleanupIndex,
|
|
9
|
-
|
|
10
|
-
} =
|
|
8
|
+
makeClient, cleanupIndex, waitForData,
|
|
9
|
+
EvenDateData, TEST_INDEX_PREFIX
|
|
10
|
+
} = ElasticsearchTestHelpers;
|
|
11
11
|
|
|
12
12
|
const THREE_MINUTES = 3 * 60 * 1000;
|
|
13
13
|
|
|
14
14
|
jest.setTimeout(THREE_MINUTES + 30000);
|
|
15
15
|
|
|
16
|
+
function formatUploadData(
|
|
17
|
+
index, data, isES8ClientTest = false
|
|
18
|
+
) {
|
|
19
|
+
const results = [];
|
|
20
|
+
|
|
21
|
+
data.forEach((record) => {
|
|
22
|
+
const meta = { _index: index };
|
|
23
|
+
|
|
24
|
+
if (!isES8ClientTest) {
|
|
25
|
+
meta._type = '_doc';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
results.push({ action: { index: meta }, data: record });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return results;
|
|
32
|
+
}
|
|
33
|
+
|
|
16
34
|
describe('bulkSend can work with congested queues', () => {
|
|
17
35
|
const logger = debugLogger('congested_test');
|
|
18
36
|
const index = `${TEST_INDEX_PREFIX}_congested_queues_`;
|
|
19
37
|
|
|
20
38
|
let client;
|
|
21
39
|
let api;
|
|
40
|
+
let isElasticsearch8 = false;
|
|
22
41
|
|
|
23
42
|
beforeAll(async () => {
|
|
24
43
|
client = await makeClient();
|
|
25
44
|
await cleanupIndex(client, index);
|
|
26
45
|
api = elasticsearchAPI(client, logger);
|
|
46
|
+
isElasticsearch8 = api.isElasticsearch8();
|
|
27
47
|
});
|
|
28
48
|
|
|
29
49
|
afterAll(async () => {
|
|
@@ -31,13 +51,13 @@ describe('bulkSend can work with congested queues', () => {
|
|
|
31
51
|
});
|
|
32
52
|
|
|
33
53
|
it('can get correct data even with congested queues', async () => {
|
|
34
|
-
const chunkedData = chunk(data, 50);
|
|
54
|
+
const chunkedData = chunk(EvenDateData.data, 50);
|
|
35
55
|
|
|
36
56
|
await pMap(chunkedData, async (cData) => {
|
|
37
|
-
const formattedData = formatUploadData(index, cData);
|
|
57
|
+
const formattedData = formatUploadData(index, cData, isElasticsearch8);
|
|
38
58
|
return api.bulkSend(formattedData);
|
|
39
59
|
}, { concurrency: 9 });
|
|
40
60
|
|
|
41
|
-
await waitForData(client, index, data.length, logger, THREE_MINUTES);
|
|
61
|
+
await waitForData(client, index, EvenDateData.data.length, logger, THREE_MINUTES);
|
|
42
62
|
});
|
|
43
63
|
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { ElasticsearchTestHelpers } = require('elasticsearch-store');
|
|
4
|
+
const { debugLogger } = require('@terascope/utils');
|
|
5
|
+
const opensearch = require('@opensearch-project/opensearch');
|
|
6
|
+
const API = require('../index');
|
|
7
|
+
|
|
8
|
+
const logger = debugLogger('retry spec');
|
|
9
|
+
const port = '1111';
|
|
10
|
+
|
|
11
|
+
const { data } = ElasticsearchTestHelpers.EvenDateData;
|
|
12
|
+
|
|
13
|
+
const body = ElasticsearchTestHelpers.formatUploadData('retry-error-test', data);
|
|
14
|
+
|
|
15
|
+
describe('retry behavior', () => {
|
|
16
|
+
it('will work with opensearch when connection cannot be established', async () => {
|
|
17
|
+
expect.assertions(4);
|
|
18
|
+
const client = new opensearch.Client({ node: `http://127.0.0.1:${port}` });
|
|
19
|
+
const { isErrorRetryable } = API(client, logger);
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await client.bulk({ body });
|
|
23
|
+
throw Error('should have thrown');
|
|
24
|
+
} catch (err) {
|
|
25
|
+
expect(err).toBeDefined();
|
|
26
|
+
expect(isErrorRetryable(err)).toBeTrue();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// we try again as we have seen the second call be marked as unretryable
|
|
30
|
+
try {
|
|
31
|
+
await client.bulk({ body });
|
|
32
|
+
throw Error('should have thrown');
|
|
33
|
+
} catch (err) {
|
|
34
|
+
expect(err).toBeDefined();
|
|
35
|
+
expect(isErrorRetryable(err)).toBeTrue();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
package/types/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Type definitions for elasticsearch-api
|
|
2
2
|
// Project: @terascope/elasticsearch-api
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
import { ClientParams, ClientResponse } from '@terascope/types';
|
|
5
5
|
import { Logger } from '@terascope/utils';
|
|
6
6
|
import { ClientMetadata } from '@terascope/types'
|
|
7
7
|
|
|
@@ -17,16 +17,16 @@ declare namespace elasticsearchAPI {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export interface Client {
|
|
20
|
-
search: (query:
|
|
21
|
-
count: (query:
|
|
22
|
-
get: (query:
|
|
23
|
-
mget: (query:
|
|
24
|
-
index: (query:
|
|
25
|
-
indexWithId: (query:
|
|
20
|
+
search: (query: ClientParams.SearchParams) => Promise<ClientResponse.SearchResponse | any[]>;
|
|
21
|
+
count: (query: ClientParams.CountParams) => Promise<number>;
|
|
22
|
+
get: (query: ClientParams.GetParams, fullResponse?: boolean) => Promise<any>;
|
|
23
|
+
mget: (query: ClientParams.MGetParams) => Promise<any>;
|
|
24
|
+
index: (query: ClientParams.IndexParams) => Promise<any>;
|
|
25
|
+
indexWithId: (query: ClientParams.IndexParams) => Promise<any>;
|
|
26
26
|
isAvailable: (index: string, recordType?: string) => Promise<any>;
|
|
27
|
-
create: (query:
|
|
28
|
-
update: (query:
|
|
29
|
-
remove: (query:
|
|
27
|
+
create: (query: ClientParams.CreateParams) => Promise<any>;
|
|
28
|
+
update: (query: ClientParams.UpdateParams) => Promise<any>;
|
|
29
|
+
remove: (query: ClientParams.DeleteParams) => Promise<es.DeleteDocumentResponse>;
|
|
30
30
|
version: () => Promise<boolean>;
|
|
31
31
|
putTemplate: (template: any, name: string) => Promise<any>;
|
|
32
32
|
/**
|
|
@@ -37,11 +37,11 @@ declare namespace elasticsearchAPI {
|
|
|
37
37
|
bulkSend: (data: BulkRecord[]) => Promise<number>;
|
|
38
38
|
nodeInfo: (query: any) => Promise<any>;
|
|
39
39
|
nodeStats: (query: any) => Promise<any>;
|
|
40
|
-
buildQuery: (opConfig: Config, msg: any) =>
|
|
41
|
-
indexExists: (query:
|
|
42
|
-
indexCreate: (query:
|
|
43
|
-
indexRefresh: (query:
|
|
44
|
-
indexRecovery: (query:
|
|
40
|
+
buildQuery: (opConfig: Config, msg: any) => ClientParams.SearchParams;
|
|
41
|
+
indexExists: (query: ClientParams.ExistsParams) => Promise<boolean>;
|
|
42
|
+
indexCreate: (query: ClientParams.IndicesCreateParams) => Promise<any>;
|
|
43
|
+
indexRefresh: (query: ClientParams.IndicesRefreshParams) => Promise<any>;
|
|
44
|
+
indexRecovery: (query: ClientParams.IndicesRecoveryParams) => Promise<any>;
|
|
45
45
|
indexSetup: (clusterName, newIndex, migrantIndexName, mapping, recordType, clientName) => Promise<boolean>;
|
|
46
46
|
verifyClient: () => boolean;
|
|
47
47
|
validateGeoParameters: (opConfig: any) => any;
|
package/test/helpers/config.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const {
|
|
4
|
-
TEST_INDEX_PREFIX = 'teratest_',
|
|
5
|
-
ELASTICSEARCH_HOST = 'http://localhost:9200',
|
|
6
|
-
ELASTICSEARCH_API_VERSION = '6.5',
|
|
7
|
-
ELASTICSEARCH_VERSION = '6.8.6',
|
|
8
|
-
OPENSEARCH_USER = 'admin',
|
|
9
|
-
OPENSEARCH_PASSWORD = 'admin',
|
|
10
|
-
OPENSEARCH_VERSION = '1.3.0',
|
|
11
|
-
RESTRAINED_OPENSEARCH_PORT = process.env.RESTRAINED_OPENSEARCH_PORT || '49206',
|
|
12
|
-
RESTRAINED_OPENSEARCH_HOST = `http://${OPENSEARCH_USER}:${OPENSEARCH_PASSWORD}@http://localhost:${RESTRAINED_OPENSEARCH_PORT}`,
|
|
13
|
-
} = process.env;
|
|
14
|
-
|
|
15
|
-
module.exports = {
|
|
16
|
-
TEST_INDEX_PREFIX,
|
|
17
|
-
ELASTICSEARCH_HOST,
|
|
18
|
-
ELASTICSEARCH_API_VERSION,
|
|
19
|
-
ELASTICSEARCH_VERSION,
|
|
20
|
-
RESTRAINED_OPENSEARCH_HOST,
|
|
21
|
-
OPENSEARCH_VERSION
|
|
22
|
-
};
|