From 70ea001f000663b71fcddb946f37fd1f95224f68 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 13 Jul 2020 11:49:00 +0200 Subject: [PATCH 1/6] adapt retryCallCluster for new ES client --- src/core/server/elasticsearch/client/index.ts | 1 + .../client/retry_call_cluster.test.ts | 288 ++++++++++++++++++ .../client/retry_call_cluster.ts | 113 +++++++ 3 files changed, 402 insertions(+) create mode 100644 src/core/server/elasticsearch/client/retry_call_cluster.test.ts create mode 100644 src/core/server/elasticsearch/client/retry_call_cluster.ts diff --git a/src/core/server/elasticsearch/client/index.ts b/src/core/server/elasticsearch/client/index.ts index 18e84482024ca..b8125de2ee498 100644 --- a/src/core/server/elasticsearch/client/index.ts +++ b/src/core/server/elasticsearch/client/index.ts @@ -22,3 +22,4 @@ export { IScopedClusterClient, ScopedClusterClient } from './scoped_cluster_clie export { ElasticsearchClientConfig } from './client_config'; export { IClusterClient, ICustomClusterClient, ClusterClient } from './cluster_client'; export { configureClient } from './configure_client'; +export { retryCallCluster, migrationRetryCallCluster } from './retry_call_cluster'; diff --git a/src/core/server/elasticsearch/client/retry_call_cluster.test.ts b/src/core/server/elasticsearch/client/retry_call_cluster.test.ts new file mode 100644 index 0000000000000..0d9eeda683b86 --- /dev/null +++ b/src/core/server/elasticsearch/client/retry_call_cluster.test.ts @@ -0,0 +1,288 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { errors } from '@elastic/elasticsearch'; +import { elasticsearchClientMock } from './mocks'; +import { loggingSystemMock } from '../../logging/logging_system.mock'; +import { retryCallCluster, migrationRetryCallCluster } from './retry_call_cluster'; + +const dummyBody = { foo: 'bar' }; +const createErrorReturn = (err: any) => elasticsearchClientMock.createClientError(err); + +describe('retryCallCluster', () => { + let client: ReturnType; + + beforeEach(() => { + client = elasticsearchClientMock.createElasticSearchClient(); + }); + + it('returns response from ES API call in case of success', async () => { + const successReturn = elasticsearchClientMock.createClientResponse({ ...dummyBody }); + + client.asyncSearch.get.mockReturnValue(successReturn); + + const result = await retryCallCluster(() => client.asyncSearch.get()); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects with `NoLivingConnectionsError`', async () => { + const successReturn = elasticsearchClientMock.createClientResponse({ ...dummyBody }); + + let i = 0; + client.asyncSearch.get.mockImplementation(() => { + i++; + return i <= 2 + ? createErrorReturn(new errors.NoLivingConnectionsError('no living connections', {} as any)) + : successReturn; + }); + + const result = await retryCallCluster(() => client.asyncSearch.get()); + expect(result.body).toEqual(dummyBody); + }); + + it('rejects when ES API calls reject with other errors', async () => { + let i = 0; + client.ping.mockImplementation(() => { + i++; + return i === 1 + ? createErrorReturn(new Error('unknown error')) + : elasticsearchClientMock.createClientResponse({ ...dummyBody }); + }); + + await expect(retryCallCluster(() => client.ping())).rejects.toMatchInlineSnapshot( + `[Error: unknown error]` + ); + }); + + it('stops retrying when ES API calls reject with other errors', async () => { + let i = 0; + client.ping.mockImplementation(() => { + i++; + switch (i) { + case 1: + case 2: + return createErrorReturn( + new errors.NoLivingConnectionsError('no living connections', {} as any) + ); + case 3: + return createErrorReturn(new Error('unknown error')); + default: + return elasticsearchClientMock.createClientResponse({ ...dummyBody }); + } + }); + + await expect(retryCallCluster(() => client.ping())).rejects.toMatchInlineSnapshot( + `[Error: unknown error]` + ); + }); +}); + +describe('migrationRetryCallCluster', () => { + let client: ReturnType; + let logger: ReturnType; + + beforeEach(() => { + client = elasticsearchClientMock.createElasticSearchClient(); + logger = loggingSystemMock.createLogger(); + }); + + const mockClientPingWithErrorBeforeSuccess = (error: any) => { + let i = 0; + client.ping.mockImplementation(() => { + i++; + return i <= 2 + ? createErrorReturn(error) + : elasticsearchClientMock.createClientResponse({ ...dummyBody }); + }); + }; + + it('retries ES API calls that rejects with `NoLivingConnectionsError`', async () => { + mockClientPingWithErrorBeforeSuccess( + new errors.NoLivingConnectionsError('no living connections', {} as any) + ); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects with `ConnectionError`', async () => { + mockClientPingWithErrorBeforeSuccess(new errors.ConnectionError('connection error', {} as any)); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects with `TimeoutError`', async () => { + mockClientPingWithErrorBeforeSuccess(new errors.TimeoutError('timeout error', {} as any)); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects with 503 `ResponseError`', async () => { + mockClientPingWithErrorBeforeSuccess( + new errors.ResponseError({ + statusCode: 503, + } as any) + ); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects 401 `ResponseError`', async () => { + mockClientPingWithErrorBeforeSuccess( + new errors.ResponseError({ + statusCode: 401, + } as any) + ); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects with 403 `ResponseError`', async () => { + mockClientPingWithErrorBeforeSuccess( + new errors.ResponseError({ + statusCode: 403, + } as any) + ); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects with 410 `ResponseError`', async () => { + mockClientPingWithErrorBeforeSuccess( + new errors.ResponseError({ + statusCode: 410, + } as any) + ); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('retries ES API calls that rejects with `snapshot_in_progress_exception` `ResponseError`', async () => { + mockClientPingWithErrorBeforeSuccess( + new errors.ResponseError({ + statusCode: 500, + body: { + error: { + type: 'snapshot_in_progress_exception', + }, + }, + } as any) + ); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + + it('logs only once for each unique error message', async () => { + let i = 0; + client.ping.mockImplementation(() => { + i++; + switch (i) { + case 1: + case 3: + return createErrorReturn( + new errors.ResponseError({ + statusCode: 503, + } as any) + ); + case 2: + case 4: + return createErrorReturn(new errors.ConnectionError('connection error', {} as any)); + case 5: + return createErrorReturn( + new errors.ResponseError({ + statusCode: 500, + body: { + error: { + type: 'snapshot_in_progress_exception', + }, + }, + } as any) + ); + default: + return elasticsearchClientMock.createClientResponse({ ...dummyBody }); + } + }); + + await migrationRetryCallCluster(() => client.ping(), logger, 1); + + expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(` + Array [ + Array [ + "Unable to connect to Elasticsearch. Error: Response Error", + ], + Array [ + "Unable to connect to Elasticsearch. Error: connection error", + ], + Array [ + "Unable to connect to Elasticsearch. Error: snapshot_in_progress_exception", + ], + ] + `); + }); + + it('rejects when ES API calls reject with other errors', async () => { + let i = 0; + client.ping.mockImplementation(() => { + i++; + return i === 1 + ? createErrorReturn( + new errors.ResponseError({ + statusCode: 418, + body: { + error: { + type: `I'm a teapot`, + }, + }, + } as any) + ) + : elasticsearchClientMock.createClientResponse({ ...dummyBody }); + }); + + await expect( + migrationRetryCallCluster(() => client.ping(), logger, 1) + ).rejects.toMatchInlineSnapshot(`[ResponseError: I'm a teapot]`); + }); + + it('stops retrying when ES API calls reject with other errors', async () => { + let i = 0; + client.ping.mockImplementation(() => { + i++; + switch (i) { + case 1: + case 2: + return createErrorReturn(new errors.TimeoutError('timeout error', {} as any)); + case 3: + return createErrorReturn(new Error('unknown error')); + default: + return elasticsearchClientMock.createClientResponse({ ...dummyBody }); + } + }); + + await expect( + migrationRetryCallCluster(() => client.ping(), logger, 1) + ).rejects.toMatchInlineSnapshot(`[Error: unknown error]`); + }); +}); diff --git a/src/core/server/elasticsearch/client/retry_call_cluster.ts b/src/core/server/elasticsearch/client/retry_call_cluster.ts new file mode 100644 index 0000000000000..978fa26cd7980 --- /dev/null +++ b/src/core/server/elasticsearch/client/retry_call_cluster.ts @@ -0,0 +1,113 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { defer, throwError, iif, timer } from 'rxjs'; +import { concatMap, retryWhen } from 'rxjs/operators'; +import { errors as esErrors } from '@elastic/elasticsearch'; +import { ApiResponse, TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; +import { Logger } from '../../logging'; + +type ApiCaller = () => TransportRequestPromise< + ApiResponse +>; + +const retryMigrationStatusCodes = [ + 503, // ServiceUnavailable + 401, // AuthorizationException + 403, // AuthenticationException + 410, // Gone +]; + +/** + * Retries the provided Elasticsearch API call when a `NoLivingConnectionsError` error is + * encountered. The API call will be retried once a second, indefinitely, until + * a successful response or a different error is received. + * + * @example + * ```ts + * const response = await retryCallCluster(() => client.ping()); + * ``` + * + * @internal + */ +export const retryCallCluster = ( + apiCaller: ApiCaller +): TransportRequestPromise> => { + return defer(() => apiCaller()) + .pipe( + retryWhen((errors) => + errors.pipe( + concatMap((error, i) => + iif( + () => error instanceof esErrors.NoLivingConnectionsError, + timer(1000), + throwError(error) + ) + ) + ) + ) + ) + .toPromise(); +}; + +/** + * Retries the provided Elasticsearch API call when an error such as + * `AuthenticationException` `NoConnections`, `ConnectionFault`, + * `ServiceUnavailable` or `RequestTimeout` are encountered. The API call will + * be retried once a second, indefinitely, until a successful response or a + * different error is received. + * + * @example + * ```ts + * const response = await migrationRetryCallCluster(() => client.ping(), logger); + * ``` + * + * @internal + */ +export const migrationRetryCallCluster = ( + apiCaller: ApiCaller, + log: Logger, + delay: number = 2500 +): TransportRequestPromise> => { + const previousErrors: string[] = []; + return defer(() => apiCaller()) + .pipe( + retryWhen((errors) => + errors.pipe( + concatMap((error, i) => { + if (!previousErrors.includes(error.message)) { + log.warn(`Unable to connect to Elasticsearch. Error: ${error.message}`); + previousErrors.push(error.message); + } + return iif( + () => + error instanceof esErrors.NoLivingConnectionsError || + error instanceof esErrors.ConnectionError || + error instanceof esErrors.TimeoutError || + retryMigrationStatusCodes.includes(error.statusCode) || + error?.body?.error?.type === 'snapshot_in_progress_exception', + timer(delay), + throwError(error) + ); + }) + ) + ) + ) + .toPromise(); +}; From fbc2d7b46d94d8079fe1132ac4852225a8b0b37a Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 13 Jul 2020 12:57:06 +0200 Subject: [PATCH 2/6] review comments --- .../client/retry_call_cluster.ts | 23 +++++++------------ .../legacy/retry_call_cluster.ts | 4 ++-- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/core/server/elasticsearch/client/retry_call_cluster.ts b/src/core/server/elasticsearch/client/retry_call_cluster.ts index 978fa26cd7980..d975f3dafc552 100644 --- a/src/core/server/elasticsearch/client/retry_call_cluster.ts +++ b/src/core/server/elasticsearch/client/retry_call_cluster.ts @@ -20,13 +20,8 @@ import { defer, throwError, iif, timer } from 'rxjs'; import { concatMap, retryWhen } from 'rxjs/operators'; import { errors as esErrors } from '@elastic/elasticsearch'; -import { ApiResponse, TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; import { Logger } from '../../logging'; -type ApiCaller = () => TransportRequestPromise< - ApiResponse ->; - const retryMigrationStatusCodes = [ 503, // ServiceUnavailable 401, // AuthorizationException @@ -46,14 +41,12 @@ const retryMigrationStatusCodes = [ * * @internal */ -export const retryCallCluster = ( - apiCaller: ApiCaller -): TransportRequestPromise> => { +export const retryCallCluster = >(apiCaller: () => T): T => { return defer(() => apiCaller()) .pipe( retryWhen((errors) => errors.pipe( - concatMap((error, i) => + concatMap((error) => iif( () => error instanceof esErrors.NoLivingConnectionsError, timer(1000), @@ -63,7 +56,7 @@ export const retryCallCluster = ( ) ) ) - .toPromise(); + .toPromise() as T; }; /** @@ -80,17 +73,17 @@ export const retryCallCluster = ( * * @internal */ -export const migrationRetryCallCluster = ( - apiCaller: ApiCaller, +export const migrationRetryCallCluster = >( + apiCaller: () => T, log: Logger, delay: number = 2500 -): TransportRequestPromise> => { +): T => { const previousErrors: string[] = []; return defer(() => apiCaller()) .pipe( retryWhen((errors) => errors.pipe( - concatMap((error, i) => { + concatMap((error) => { if (!previousErrors.includes(error.message)) { log.warn(`Unable to connect to Elasticsearch. Error: ${error.message}`); previousErrors.push(error.message); @@ -109,5 +102,5 @@ export const migrationRetryCallCluster = ( ) ) ) - .toPromise(); + .toPromise() as T; }; diff --git a/src/core/server/elasticsearch/legacy/retry_call_cluster.ts b/src/core/server/elasticsearch/legacy/retry_call_cluster.ts index 475a76d406017..1b05cb2bf13cd 100644 --- a/src/core/server/elasticsearch/legacy/retry_call_cluster.ts +++ b/src/core/server/elasticsearch/legacy/retry_call_cluster.ts @@ -53,7 +53,7 @@ export function migrationsRetryCallCluster( .pipe( retryWhen((error$) => error$.pipe( - concatMap((error, i) => { + concatMap((error) => { if (!previousErrors.includes(error.message)) { log.warn(`Unable to connect to Elasticsearch. Error: ${error.message}`); previousErrors.push(error.message); @@ -100,7 +100,7 @@ export function retryCallCluster(apiCaller: LegacyAPICaller) { .pipe( retryWhen((errors) => errors.pipe( - concatMap((error, i) => + concatMap((error) => iif( () => error instanceof legacyElasticsearch.errors.NoConnections, timer(1000), From 68c1bd31154813ff9bd61dcdba452a17598b903a Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 13 Jul 2020 13:14:55 +0200 Subject: [PATCH 3/6] retry on 408 ResponseError --- .../elasticsearch/client/retry_call_cluster.test.ts | 11 +++++++++++ .../server/elasticsearch/client/retry_call_cluster.ts | 1 + 2 files changed, 12 insertions(+) diff --git a/src/core/server/elasticsearch/client/retry_call_cluster.test.ts b/src/core/server/elasticsearch/client/retry_call_cluster.test.ts index 0d9eeda683b86..d5406f9a2dd75 100644 --- a/src/core/server/elasticsearch/client/retry_call_cluster.test.ts +++ b/src/core/server/elasticsearch/client/retry_call_cluster.test.ts @@ -168,6 +168,17 @@ describe('migrationRetryCallCluster', () => { expect(result.body).toEqual(dummyBody); }); + it('retries ES API calls that rejects with 408 `ResponseError`', async () => { + mockClientPingWithErrorBeforeSuccess( + new errors.ResponseError({ + statusCode: 408, + } as any) + ); + + const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + expect(result.body).toEqual(dummyBody); + }); + it('retries ES API calls that rejects with 410 `ResponseError`', async () => { mockClientPingWithErrorBeforeSuccess( new errors.ResponseError({ diff --git a/src/core/server/elasticsearch/client/retry_call_cluster.ts b/src/core/server/elasticsearch/client/retry_call_cluster.ts index d975f3dafc552..2ce1a4fcfe9d0 100644 --- a/src/core/server/elasticsearch/client/retry_call_cluster.ts +++ b/src/core/server/elasticsearch/client/retry_call_cluster.ts @@ -26,6 +26,7 @@ const retryMigrationStatusCodes = [ 503, // ServiceUnavailable 401, // AuthorizationException 403, // AuthenticationException + 408, // RequestTimeout 410, // Gone ]; From ba3acd5318c9e8b4186016b7ab49fecd2f18e478 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 13 Jul 2020 17:10:26 +0200 Subject: [PATCH 4/6] use error name instead of instanceof base check --- .../server/elasticsearch/client/retry_call_cluster.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/core/server/elasticsearch/client/retry_call_cluster.ts b/src/core/server/elasticsearch/client/retry_call_cluster.ts index 2ce1a4fcfe9d0..59dd4e14ffb6a 100644 --- a/src/core/server/elasticsearch/client/retry_call_cluster.ts +++ b/src/core/server/elasticsearch/client/retry_call_cluster.ts @@ -22,7 +22,7 @@ import { concatMap, retryWhen } from 'rxjs/operators'; import { errors as esErrors } from '@elastic/elasticsearch'; import { Logger } from '../../logging'; -const retryMigrationStatusCodes = [ +const retryResponseStatuses = [ 503, // ServiceUnavailable 401, // AuthorizationException 403, // AuthenticationException @@ -91,10 +91,11 @@ export const migrationRetryCallCluster = >( } return iif( () => - error instanceof esErrors.NoLivingConnectionsError || - error instanceof esErrors.ConnectionError || - error instanceof esErrors.TimeoutError || - retryMigrationStatusCodes.includes(error.statusCode) || + error.name === 'NoLivingConnectionsError' || + error.name === 'ConnectionError' || + error.name === 'TimeoutError' || + (error.name === 'ResponseError' && + retryResponseStatuses.includes(error.statusCode)) || error?.body?.error?.type === 'snapshot_in_progress_exception', timer(delay), throwError(error) From 1babcb4f17bef87eac1313a2daf2ee0203c3d57a Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 13 Jul 2020 17:13:07 +0200 Subject: [PATCH 5/6] use error name instead of instanceof base check bis --- src/core/server/elasticsearch/client/retry_call_cluster.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/core/server/elasticsearch/client/retry_call_cluster.ts b/src/core/server/elasticsearch/client/retry_call_cluster.ts index 59dd4e14ffb6a..1ad039e512215 100644 --- a/src/core/server/elasticsearch/client/retry_call_cluster.ts +++ b/src/core/server/elasticsearch/client/retry_call_cluster.ts @@ -19,7 +19,6 @@ import { defer, throwError, iif, timer } from 'rxjs'; import { concatMap, retryWhen } from 'rxjs/operators'; -import { errors as esErrors } from '@elastic/elasticsearch'; import { Logger } from '../../logging'; const retryResponseStatuses = [ @@ -48,11 +47,7 @@ export const retryCallCluster = >(apiCaller: () => T) retryWhen((errors) => errors.pipe( concatMap((error) => - iif( - () => error instanceof esErrors.NoLivingConnectionsError, - timer(1000), - throwError(error) - ) + iif(() => error.name === 'NoLivingConnectionsError', timer(1000), throwError(error)) ) ) ) From ed943967ff40c93cc017663ccef1445750f7e6e4 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 20 Jul 2020 09:01:11 +0200 Subject: [PATCH 6/6] use mockImplementationOnce chaining --- .../client/retry_call_cluster.test.ts | 168 ++++++++---------- 1 file changed, 76 insertions(+), 92 deletions(-) diff --git a/src/core/server/elasticsearch/client/retry_call_cluster.test.ts b/src/core/server/elasticsearch/client/retry_call_cluster.test.ts index d5406f9a2dd75..a7177c0b29047 100644 --- a/src/core/server/elasticsearch/client/retry_call_cluster.test.ts +++ b/src/core/server/elasticsearch/client/retry_call_cluster.test.ts @@ -44,26 +44,20 @@ describe('retryCallCluster', () => { it('retries ES API calls that rejects with `NoLivingConnectionsError`', async () => { const successReturn = elasticsearchClientMock.createClientResponse({ ...dummyBody }); - let i = 0; - client.asyncSearch.get.mockImplementation(() => { - i++; - return i <= 2 - ? createErrorReturn(new errors.NoLivingConnectionsError('no living connections', {} as any)) - : successReturn; - }); + client.asyncSearch.get + .mockImplementationOnce(() => + createErrorReturn(new errors.NoLivingConnectionsError('no living connections', {} as any)) + ) + .mockImplementationOnce(() => successReturn); const result = await retryCallCluster(() => client.asyncSearch.get()); expect(result.body).toEqual(dummyBody); }); it('rejects when ES API calls reject with other errors', async () => { - let i = 0; - client.ping.mockImplementation(() => { - i++; - return i === 1 - ? createErrorReturn(new Error('unknown error')) - : elasticsearchClientMock.createClientResponse({ ...dummyBody }); - }); + client.ping + .mockImplementationOnce(() => createErrorReturn(new Error('unknown error'))) + .mockImplementationOnce(() => elasticsearchClientMock.createClientResponse({ ...dummyBody })); await expect(retryCallCluster(() => client.ping())).rejects.toMatchInlineSnapshot( `[Error: unknown error]` @@ -71,21 +65,15 @@ describe('retryCallCluster', () => { }); it('stops retrying when ES API calls reject with other errors', async () => { - let i = 0; - client.ping.mockImplementation(() => { - i++; - switch (i) { - case 1: - case 2: - return createErrorReturn( - new errors.NoLivingConnectionsError('no living connections', {} as any) - ); - case 3: - return createErrorReturn(new Error('unknown error')); - default: - return elasticsearchClientMock.createClientResponse({ ...dummyBody }); - } - }); + client.ping + .mockImplementationOnce(() => + createErrorReturn(new errors.NoLivingConnectionsError('no living connections', {} as any)) + ) + .mockImplementationOnce(() => + createErrorReturn(new errors.NoLivingConnectionsError('no living connections', {} as any)) + ) + .mockImplementationOnce(() => createErrorReturn(new Error('unknown error'))) + .mockImplementationOnce(() => elasticsearchClientMock.createClientResponse({ ...dummyBody })); await expect(retryCallCluster(() => client.ping())).rejects.toMatchInlineSnapshot( `[Error: unknown error]` @@ -103,13 +91,10 @@ describe('migrationRetryCallCluster', () => { }); const mockClientPingWithErrorBeforeSuccess = (error: any) => { - let i = 0; - client.ping.mockImplementation(() => { - i++; - return i <= 2 - ? createErrorReturn(error) - : elasticsearchClientMock.createClientResponse({ ...dummyBody }); - }); + client.ping + .mockImplementationOnce(() => createErrorReturn(error)) + .mockImplementationOnce(() => createErrorReturn(error)) + .mockImplementationOnce(() => elasticsearchClientMock.createClientResponse({ ...dummyBody })); }; it('retries ES API calls that rejects with `NoLivingConnectionsError`', async () => { @@ -207,35 +192,40 @@ describe('migrationRetryCallCluster', () => { }); it('logs only once for each unique error message', async () => { - let i = 0; - client.ping.mockImplementation(() => { - i++; - switch (i) { - case 1: - case 3: - return createErrorReturn( - new errors.ResponseError({ - statusCode: 503, - } as any) - ); - case 2: - case 4: - return createErrorReturn(new errors.ConnectionError('connection error', {} as any)); - case 5: - return createErrorReturn( - new errors.ResponseError({ - statusCode: 500, - body: { - error: { - type: 'snapshot_in_progress_exception', - }, + client.ping + .mockImplementationOnce(() => + createErrorReturn( + new errors.ResponseError({ + statusCode: 503, + } as any) + ) + ) + .mockImplementationOnce(() => + createErrorReturn(new errors.ConnectionError('connection error', {} as any)) + ) + .mockImplementationOnce(() => + createErrorReturn( + new errors.ResponseError({ + statusCode: 503, + } as any) + ) + ) + .mockImplementationOnce(() => + createErrorReturn(new errors.ConnectionError('connection error', {} as any)) + ) + .mockImplementationOnce(() => + createErrorReturn( + new errors.ResponseError({ + statusCode: 500, + body: { + error: { + type: 'snapshot_in_progress_exception', }, - } as any) - ); - default: - return elasticsearchClientMock.createClientResponse({ ...dummyBody }); - } - }); + }, + } as any) + ) + ) + .mockImplementationOnce(() => elasticsearchClientMock.createClientResponse({ ...dummyBody })); await migrationRetryCallCluster(() => client.ping(), logger, 1); @@ -255,22 +245,20 @@ describe('migrationRetryCallCluster', () => { }); it('rejects when ES API calls reject with other errors', async () => { - let i = 0; - client.ping.mockImplementation(() => { - i++; - return i === 1 - ? createErrorReturn( - new errors.ResponseError({ - statusCode: 418, - body: { - error: { - type: `I'm a teapot`, - }, + client.ping + .mockImplementationOnce(() => + createErrorReturn( + new errors.ResponseError({ + statusCode: 418, + body: { + error: { + type: `I'm a teapot`, }, - } as any) - ) - : elasticsearchClientMock.createClientResponse({ ...dummyBody }); - }); + }, + } as any) + ) + ) + .mockImplementationOnce(() => elasticsearchClientMock.createClientResponse({ ...dummyBody })); await expect( migrationRetryCallCluster(() => client.ping(), logger, 1) @@ -278,19 +266,15 @@ describe('migrationRetryCallCluster', () => { }); it('stops retrying when ES API calls reject with other errors', async () => { - let i = 0; - client.ping.mockImplementation(() => { - i++; - switch (i) { - case 1: - case 2: - return createErrorReturn(new errors.TimeoutError('timeout error', {} as any)); - case 3: - return createErrorReturn(new Error('unknown error')); - default: - return elasticsearchClientMock.createClientResponse({ ...dummyBody }); - } - }); + client.ping + .mockImplementationOnce(() => + createErrorReturn(new errors.TimeoutError('timeout error', {} as any)) + ) + .mockImplementationOnce(() => + createErrorReturn(new errors.TimeoutError('timeout error', {} as any)) + ) + .mockImplementationOnce(() => createErrorReturn(new Error('unknown error'))) + .mockImplementationOnce(() => elasticsearchClientMock.createClientResponse({ ...dummyBody })); await expect( migrationRetryCallCluster(() => client.ping(), logger, 1)