[Search] Reuse uiSettings within bsearch request (#114088) (#114232)

Co-authored-by: Anton Dosov <anton.dosov@elastic.co>
This commit is contained in:
Kibana Machine 2021-10-07 06:18:02 -04:00 committed by GitHub
parent 54d903ad41
commit 2e104ea56e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 121 additions and 6 deletions

View file

@ -25,13 +25,13 @@ export function registerBsearchRoute(
{ request: IKibanaSearchRequest; options?: ISearchOptionsSerializable },
IKibanaSearchResponse
>('/internal/bsearch', (request) => {
const search = getScoped(request);
return {
/**
* @param requestOptions
* @throws `KibanaServerError`
*/
onBatchItem: async ({ request: requestData, options }) => {
const search = getScoped(request);
const { executionContext, ...restOptions } = options || {};
return executionContextService.withContext(executionContext, () =>

View file

@ -89,6 +89,7 @@ import { getKibanaContext } from './expressions/kibana_context';
import { enhancedEsSearchStrategyProvider } from './strategies/ese_search';
import { eqlSearchStrategyProvider } from './strategies/eql_search';
import { NoSearchIdInSessionError } from './errors/no_search_id_in_session';
import { CachedUiSettingsClient } from './services';
type StrategyMap = Record<string, ISearchStrategy<any, any>>;
@ -458,7 +459,9 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
searchSessionsClient,
savedObjectsClient,
esClient: elasticsearch.client.asScoped(request),
uiSettingsClient: uiSettings.asScopedToClient(savedObjectsClient),
uiSettingsClient: new CachedUiSettingsClient(
uiSettings.asScopedToClient(savedObjectsClient)
),
request,
};
return {

View file

@ -0,0 +1,59 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { coreMock, httpServerMock } from '../../../../../core/server/mocks';
import { CachedUiSettingsClient } from './cached_ui_settings_client';
test('fetching uiSettings once for the same key', async () => {
const request = httpServerMock.createKibanaRequest();
const soClient = coreMock.createStart().savedObjects.getScopedClient(request);
const uiSettings = coreMock.createStart().uiSettings.asScopedToClient(soClient);
const key = 'key';
const value = 'value';
const spy = jest
.spyOn(uiSettings, 'getAll')
.mockImplementation(() => Promise.resolve({ [key]: value }));
const cachedUiSettings = new CachedUiSettingsClient(uiSettings);
const res1 = cachedUiSettings.get(key);
const res2 = cachedUiSettings.get(key);
expect(spy).toHaveBeenCalledTimes(1); // check that internally uiSettings.getAll() called only once
expect(await res1).toBe(value);
expect(await res2).toBe(value);
});
test('fetching uiSettings once for different keys', async () => {
const request = httpServerMock.createKibanaRequest();
const soClient = coreMock.createStart().savedObjects.getScopedClient(request);
const uiSettings = coreMock.createStart().uiSettings.asScopedToClient(soClient);
const key1 = 'key1';
const value1 = 'value1';
const key2 = 'key2';
const value2 = 'value2';
const spy = jest
.spyOn(uiSettings, 'getAll')
.mockImplementation(() => Promise.resolve({ [key1]: value1, [key2]: value2 }));
const cachedUiSettings = new CachedUiSettingsClient(uiSettings);
const res1 = cachedUiSettings.get(key1);
const res2 = cachedUiSettings.get(key2);
expect(spy).toHaveBeenCalledTimes(1); // check that internally uiSettings.getAll() called only once
expect(await res1).toBe(value1);
expect(await res2).toBe(value2);
});

View file

@ -0,0 +1,44 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { IUiSettingsClient } from 'kibana/server';
/**
* {@link IUiSettingsClient} wrapper to ensure uiSettings requested only once within a single KibanaRequest,
* {@link IUiSettingsClient} has its own cache, but it doesn't cache pending promises, so this produces two requests:
*
* const promise1 = uiSettings.get(1); // fetches config
* const promise2 = uiSettings.get(2); // fetches config
*
* And {@link CachedUiSettingsClient} solves it, so this produced a single request:
*
* const promise1 = cachedUiSettingsClient.get(1); // fetches config
* const promise2 = cachedUiSettingsClient.get(2); // reuses existing promise
*
* @internal
*/
export class CachedUiSettingsClient implements Pick<IUiSettingsClient, 'get'> {
private cache: Promise<Record<string, unknown>> | undefined;
constructor(private readonly client: IUiSettingsClient) {}
async get<T = any>(key: string): Promise<T> {
if (!this.cache) {
// caching getAll() instead of just get(key) because internally uiSettings calls `getAll()` anyways
// this way we reuse cache in case settings for different keys were requested
this.cache = this.client.getAll();
}
return this.cache
.then((cache) => cache[key] as T)
.catch((e) => {
this.cache = undefined;
throw e;
});
}
}

View file

@ -0,0 +1,9 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
export { CachedUiSettingsClient } from './cached_ui_settings_client';

View file

@ -17,7 +17,7 @@ export function getShardTimeout(config: SharedGlobalConfig): Pick<Search, 'timeo
}
export async function getDefaultSearchParams(
uiSettingsClient: IUiSettingsClient
uiSettingsClient: Pick<IUiSettingsClient, 'get'>
): Promise<
Pick<Search, 'max_concurrent_shard_requests' | 'ignore_unavailable' | 'track_total_hits'>
> {

View file

@ -20,7 +20,7 @@ import { SearchSessionsConfigSchema } from '../../../../config';
* @internal
*/
export async function getIgnoreThrottled(
uiSettingsClient: IUiSettingsClient
uiSettingsClient: Pick<IUiSettingsClient, 'get'>
): Promise<Pick<Search, 'ignore_throttled'>> {
const includeFrozen = await uiSettingsClient.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN);
return includeFrozen ? { ignore_throttled: false } : {};
@ -30,7 +30,7 @@ export async function getIgnoreThrottled(
@internal
*/
export async function getDefaultAsyncSubmitParams(
uiSettingsClient: IUiSettingsClient,
uiSettingsClient: Pick<IUiSettingsClient, 'get'>,
searchSessionsConfig: SearchSessionsConfigSchema | null,
options: ISearchOptions
): Promise<

View file

@ -35,7 +35,7 @@ export interface SearchEnhancements {
export interface SearchStrategyDependencies {
savedObjectsClient: SavedObjectsClientContract;
esClient: IScopedClusterClient;
uiSettingsClient: IUiSettingsClient;
uiSettingsClient: Pick<IUiSettingsClient, 'get'>;
searchSessionsClient: IScopedSearchSessionsClient;
request: KibanaRequest;
}