Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ src/core/packages/elasticsearch/client-server-mocks @elastic/kibana-core
src/core/packages/elasticsearch/server @elastic/kibana-core
src/core/packages/elasticsearch/server-internal @elastic/kibana-core
src/core/packages/elasticsearch/server-mocks @elastic/kibana-core
src/core/packages/elasticsearch/server-utils @elastic/kibana-core
src/core/packages/environment/server-internal @elastic/kibana-core
src/core/packages/environment/server-mocks @elastic/kibana-core
src/core/packages/execution-context/browser @elastic/kibana-core
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@
"@kbn/core-elasticsearch-client-server-internal": "link:src/core/packages/elasticsearch/client-server-internal",
"@kbn/core-elasticsearch-server": "link:src/core/packages/elasticsearch/server",
"@kbn/core-elasticsearch-server-internal": "link:src/core/packages/elasticsearch/server-internal",
"@kbn/core-elasticsearch-server-utils": "link:src/core/packages/elasticsearch/server-utils",
"@kbn/core-environment-server-internal": "link:src/core/packages/environment/server-internal",
"@kbn/core-execution-context-browser": "link:src/core/packages/execution-context/browser",
"@kbn/core-execution-context-browser-internal": "link:src/core/packages/execution-context/browser-internal",
Expand Down
1 change: 0 additions & 1 deletion src/core/packages/elasticsearch/server-internal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,4 @@ export { CoreElasticsearchRouteHandlerContext } from './src/elasticsearch_route_
export { retryCallCluster, migrationRetryCallCluster } from './src/retry_call_cluster';
export { isInlineScriptingEnabled } from './src/is_scripting_enabled';
export { getCapabilitiesFromClient } from './src/get_capabilities';
export { isRetryableEsClientError } from './src/retryable_es_client_errors';
export type { ClusterInfo } from './src/get_cluster_info';
1 change: 1 addition & 0 deletions src/core/packages/elasticsearch/server-internal/moon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dependsOn:
- '@kbn/core-http-server-internal'
- '@kbn/core-execution-context-server-internal'
- '@kbn/core-elasticsearch-server'
- '@kbn/core-elasticsearch-server-utils'
- '@kbn/core-elasticsearch-client-server-internal'
- '@kbn/core-test-helpers-deprecations-getters'
- '@kbn/config'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { isRetryableEsClientErrorMock } from './is_scripting_enabled.test.mocks';
import * as estypes from '@elastic/elasticsearch/lib/api/types';
import type { estypes } from '@elastic/elasticsearch';
import { errors as esErrors } from '@elastic/elasticsearch';
import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
import { isInlineScriptingEnabled } from './is_scripting_enabled';

Expand Down Expand Up @@ -98,55 +98,53 @@ describe('isInlineScriptingEnabled', () => {
});

describe('resiliency', () => {
beforeEach(() => {
isRetryableEsClientErrorMock.mockReset();
});

const mockSuccessOnce = () => {
client.cluster.getSettings.mockResolvedValueOnce({
transient: {},
persistent: {},
defaults: {},
});
};
const mockErrorOnce = () => {
client.cluster.getSettings.mockResponseImplementationOnce(() => {
throw Error('ERR CON REFUSED');
});

const mockRetryableErrorOnce = () => {
client.cluster.getSettings.mockRejectedValueOnce(
new esErrors.ConnectionError(
'Connection failed',
elasticsearchClientMock.createApiResponse()
)
);
};

it('retries the ES api call in case of retryable error', async () => {
isRetryableEsClientErrorMock.mockReturnValue(true);
const mockNonRetryableErrorOnce = () => {
client.cluster.getSettings.mockRejectedValueOnce(new Error('Non-retryable error'));
};

mockErrorOnce();
it('retries the ES api call in case of retryable error', async () => {
mockRetryableErrorOnce();
mockSuccessOnce();

await expect(isInlineScriptingEnabled({ client, maxRetryDelay: 1 })).resolves.toEqual(true);
expect(client.cluster.getSettings).toHaveBeenCalledTimes(2);
});

it('throws in case of non-retryable error', async () => {
isRetryableEsClientErrorMock.mockReturnValue(false);

mockErrorOnce();
mockNonRetryableErrorOnce();
mockSuccessOnce();

await expect(isInlineScriptingEnabled({ client, maxRetryDelay: 0.1 })).rejects.toThrowError(
'ERR CON REFUSED'
'Non-retryable error'
);
});

it('retries up to `maxRetries` times', async () => {
isRetryableEsClientErrorMock.mockReturnValue(true);

mockErrorOnce();
mockErrorOnce();
mockErrorOnce();
mockRetryableErrorOnce();
mockRetryableErrorOnce();
mockRetryableErrorOnce();
mockSuccessOnce();

await expect(
isInlineScriptingEnabled({ client, maxRetryDelay: 0.1, maxRetries: 2 })
).rejects.toThrowError('ERR CON REFUSED');
).rejects.toThrowError('Connection failed');
expect(client.cluster.getSettings).toHaveBeenCalledTimes(3);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
*/

import { defer, map, retry, timer, firstValueFrom, throwError } from 'rxjs';
import { isRetryableEsClientError } from '@kbn/core-elasticsearch-server-utils';
import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import { isRetryableEsClientError } from './retryable_es_client_errors';

const scriptAllowedTypesKey = 'script.allowed_types';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@kbn/core-http-server-internal",
"@kbn/core-execution-context-server-internal",
"@kbn/core-elasticsearch-server",
"@kbn/core-elasticsearch-server-utils",
"@kbn/core-elasticsearch-client-server-internal",
"@kbn/core-test-helpers-deprecations-getters",
"@kbn/config",
Expand Down
3 changes: 3 additions & 0 deletions src/core/packages/elasticsearch/server-utils/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @kbn/core-elasticsearch-server-utils

Utilities for working with Elasticsearch
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,4 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

export const isRetryableEsClientErrorMock = jest.fn();

jest.doMock('./retryable_es_client_errors', () => {
return {
isRetryableEsClientError: isRetryableEsClientErrorMock,
};
});
export { isRetryableEsClientError } from './src/is_retryable_es_client_error';
14 changes: 14 additions & 0 deletions src/core/packages/elasticsearch/server-utils/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../../../../..',
roots: ['<rootDir>/src/core/packages/elasticsearch/server-utils'],
};
7 changes: 7 additions & 0 deletions src/core/packages/elasticsearch/server-utils/kibana.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "shared-common",
"id": "@kbn/core-elasticsearch-server-utils",
"owner": "@elastic/kibana-core",
"group": "platform",
"visibility": "shared"
}
45 changes: 45 additions & 0 deletions src/core/packages/elasticsearch/server-utils/moon.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# This file is generated by the @kbn/moon package. Any manual edits will be erased!
# To extend this, write your extensions/overrides to 'moon.extend.yml'
# then regenerate this file with: 'node scripts/regenerate_moon_projects.js --update --filter @kbn/core-elasticsearch-server-utils'

$schema: https://moonrepo.dev/schemas/project.json
id: '@kbn/core-elasticsearch-server-utils'
type: unknown
owners:
defaultOwner: '@elastic/kibana-core'
toolchain:
default: node
language: typescript
project:
name: '@kbn/core-elasticsearch-server-utils'
description: Moon project for @kbn/core-elasticsearch-server-utils
channel: ''
owner: '@elastic/kibana-core'
metadata:
sourceRoot: src/core/packages/elasticsearch/server-utils
dependsOn:
- '@kbn/core-elasticsearch-client-server-mocks'
tags:
- shared-common
- package
- prod
- group-platform
- shared
- jest-unit-tests
fileGroups:
src:
- '**/*.ts'
- '!target/**/*'
tasks:
jest:
args:
- '--config'
- $projectRoot/jest.config.js
inputs:
- '@group(src)'
jestCI:
args:
- '--config'
- $projectRoot/jest.config.js
inputs:
- '@group(src)'
6 changes: 6 additions & 0 deletions src/core/packages/elasticsearch/server-utils/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "@kbn/core-elasticsearch-server-utils",
"private": true,
"version": "1.0.0",
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import { errors as esErrors } from '@elastic/elasticsearch';
import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
import { isRetryableEsClientError } from './retryable_es_client_errors';
import { isRetryableEsClientError } from './is_retryable_es_client_error';

describe('isRetryableEsClientError', () => {
describe('returns `false` for', () => {
Expand Down Expand Up @@ -51,27 +51,34 @@ describe('isRetryableEsClientError', () => {
expect(isRetryableEsClientError(error)).toEqual(true);
});

it('ResponseError of type snapshot_in_progress_exception', () => {
it.each([503, 504, 408, 410, 429])('ResponseError with %p status code', (statusCode) => {
const error = new esErrors.ResponseError(
elasticsearchClientMock.createApiResponse({
body: { error: { type: 'snapshot_in_progress_exception' } },
statusCode,
body: { error: { type: 'reason' } },
})
);

expect(isRetryableEsClientError(error)).toEqual(true);
});

it.each([503, 504, 401, 403, 408, 410, 429])(
'ResponseError with %p status code',
(statusCode) => {
const error = new esErrors.ResponseError(
elasticsearchClientMock.createApiResponse({
statusCode,
body: { error: { type: 'reason' } },
})
);
it('custom response status codes', () => {
const retryableError = new esErrors.ResponseError(
elasticsearchClientMock.createApiResponse({
statusCode: 418, // I'm a retryable teapot
body: { error: { type: 'reason' } },
})
);

expect(isRetryableEsClientError(error)).toEqual(true);
}
);
const nonRetryableError = new esErrors.ResponseError(
elasticsearchClientMock.createApiResponse({
statusCode: 503, // 503 is retryable by default but not in our custom retry codes
body: { error: { type: 'reason' } },
})
);

expect(isRetryableEsClientError(retryableError, [418])).toEqual(true);
expect(isRetryableEsClientError(nonRetryableError, [418])).toEqual(false);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@

import { errors as EsErrors } from '@elastic/elasticsearch';

const retryResponseStatuses = [
401, // AuthorizationException
403, // AuthenticationException
const DEFAULT_RETRY_STATUS_CODES = [
408, // RequestTimeout
410, // Gone
429, // TooManyRequests -> ES circuit breaker
Expand All @@ -21,20 +19,32 @@ const retryResponseStatuses = [

/**
* Returns true if the given elasticsearch error should be retried
* by retry-based resiliency systems such as the SO migration, false otherwise.
*
* Retryable errors include:
* - NoLivingConnectionsError
* - ConnectionError
* - TimeoutError
* - ResponseError with status codes:
* - 408 RequestTimeout
* - 410 Gone
* - 429 TooManyRequests (ES circuit breaker)
* - 503 ServiceUnavailable
* - 504 GatewayTimeout
* - OR custom status codes if provided
* @param e The error to check
* @param customRetryStatusCodes Custom response status codes to consider as retryable
* @returns true if the error is retryable, false otherwise
*/
export const isRetryableEsClientError = (e: EsErrors.ElasticsearchClientError): boolean => {
export const isRetryableEsClientError = (
e: EsErrors.ElasticsearchClientError,
customRetryStatusCodes?: number[]
): boolean => {
if (
e instanceof EsErrors.NoLivingConnectionsError ||
e instanceof EsErrors.ConnectionError ||
e instanceof EsErrors.TimeoutError ||
(e instanceof EsErrors.ResponseError &&
(retryResponseStatuses.includes(e?.statusCode!) ||
// ES returns a 400 Bad Request when trying to close or delete an
// index while snapshots are in progress. This should have been a 503
// so once https://github.com/elastic/elasticsearch/issues/65883 is
// fixed we can remove this.
e?.body?.error?.type === 'snapshot_in_progress_exception'))
(customRetryStatusCodes ?? DEFAULT_RETRY_STATUS_CODES).includes(e?.statusCode!))
) {
return true;
}
Expand Down
19 changes: 19 additions & 0 deletions src/core/packages/elasticsearch/server-utils/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"extends": "@kbn/tsconfig-base/tsconfig.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node"
]
},
"include": [
"**/*.ts"
],
"kbn_references": [
"@kbn/core-elasticsearch-client-server-mocks"
],
"exclude": [
"target/**/*",
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dependsOn:
- '@kbn/std'
- '@kbn/core-doc-links-server'
- '@kbn/core-elasticsearch-server'
- '@kbn/core-elasticsearch-server-utils'
- '@kbn/core-elasticsearch-client-server-internal'
- '@kbn/core-saved-objects-common'
- '@kbn/core-saved-objects-server'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,6 @@ describe('catchRetryableEsClientErrors', () => {
type: 'retryable_es_client_error',
});
});
it('ResponseError of type snapshot_in_progress_exception', async () => {
const error = new esErrors.ResponseError(
elasticsearchClientMock.createApiResponse({
body: { error: { type: 'snapshot_in_progress_exception' } },
})
);
expect(
((await Promise.reject(error).catch(catchRetryableEsClientErrors)) as any).left
).toMatchObject({
message: 'snapshot_in_progress_exception',
type: 'retryable_es_client_error',
});
});
it.each([503, 401, 403, 408, 410, 429])(
'ResponseError with retryable status code (%d)',
async (status) => {
Expand Down
Loading