From 1c820503c24b4f4fbdd26b2a1a20f36212d58871 Mon Sep 17 00:00:00 2001 From: Liza K Date: Tue, 24 Nov 2020 16:51:57 +0200 Subject: [PATCH 01/26] Monitor ids --- .../data/server/search/search_service.ts | 6 +- .../search/session/session_service.test.ts | 245 +++++++++++++++++- .../server/search/session/session_service.ts | 133 +++++++++- 3 files changed, 372 insertions(+), 12 deletions(-) diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index b44980164d097..42f829cd64ec6 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -106,12 +106,14 @@ export class SearchService implements Plugin { private readonly searchSourceService = new SearchSourceService(); private defaultSearchStrategyName: string = ES_SEARCH_STRATEGY; private searchStrategies: StrategyMap = {}; - private sessionService: BackgroundSessionService = new BackgroundSessionService(); + private sessionService: BackgroundSessionService; constructor( private initializerContext: PluginInitializerContext, private readonly logger: Logger - ) {} + ) { + this.sessionService = new BackgroundSessionService(logger); + } public setup( core: CoreSetup<{}, DataPluginStart>, diff --git a/src/plugins/data/server/search/session/session_service.test.ts b/src/plugins/data/server/search/session/session_service.test.ts index 1ceebae967d4c..3a204e2a77fdf 100644 --- a/src/plugins/data/server/search/session/session_service.test.ts +++ b/src/plugins/data/server/search/session/session_service.test.ts @@ -17,16 +17,82 @@ * under the License. */ -import type { SavedObject, SavedObjectsClientContract } from 'kibana/server'; +import { coreMock } from 'src/core/server/mocks'; +import type { SavedObject, SavedObjectsClientContract } from 'src/core/server'; import { savedObjectsClientMock } from '../../../../../core/server/mocks'; import { BackgroundSessionStatus } from '../../../common'; import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; -import { BackgroundSessionService } from './session_service'; +import { + BackgroundSessionService, + INMEM_TRACKING_INTERVAL, + MAX_UPDATE_RETRIES, + SessionInfo, +} from './session_service'; import { createRequestHash } from './utils'; +import moment from 'moment'; describe('BackgroundSessionService', () => { let savedObjectsClient: jest.Mocked; let service: BackgroundSessionService; + const mockCoreStart = coreMock.createStart(); + + const MOCK_SESSION_ID = 'session-id-mock'; + const MOCK_ASYNC_ID = '123456'; + const MOCK_KEY_HASH = '608de49a4600dbb5b173492759792e4a'; + + const createMockInternalSavedObjectClient = ( + bulkGetSpy?: jest.SpyInstance, + bulkUpdateSpy?: jest.SpyInstance + ) => { + Object.defineProperty(service, 'internalSavedObjectsClient', { + get: () => { + const bulkGet = + bulkGetSpy || + (() => { + return { + saved_objects: [ + { + attributes: { + sessionId: MOCK_SESSION_ID, + idMapping: { + 'another-key': 'another-async-id', + }, + }, + id: MOCK_SESSION_ID, + version: '1', + }, + ], + }; + }); + + const bulkUpdate = + bulkUpdateSpy || + (() => { + return { + saved_objects: [], + }; + }); + return { + bulkGet, + bulkUpdate, + }; + }, + }); + }; + + const createMockIdMapping = ( + mapValues: any[], + insertTime?: moment.Moment, + retryCount?: number + ): Map => { + const fakeMap = new Map(); + fakeMap.set(MOCK_SESSION_ID, { + ids: new Map(mapValues), + insertTime: insertTime || moment(), + retryCount: retryCount || 0, + }); + return fakeMap; + }; const mockSavedObject: SavedObject = { id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', @@ -40,7 +106,11 @@ describe('BackgroundSessionService', () => { beforeEach(() => { savedObjectsClient = savedObjectsClientMock.create(); - service = new BackgroundSessionService(); + const mockLogger: any = { + debug: jest.fn(), + error: jest.fn(), + }; + service = new BackgroundSessionService(mockLogger); }); it('save throws if `name` is not provided', () => { @@ -230,4 +300,173 @@ describe('BackgroundSessionService', () => { expect(id).toBe(searchId); }); }); + + describe('Monitor', () => { + beforeEach(() => { + jest.useFakeTimers(); + service.start(mockCoreStart); + }); + + afterEach(() => { + jest.useRealTimers(); + service.stop(); + }); + + it('should delete expired IDs', async () => { + const bulkGetSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [] }); + createMockInternalSavedObjectClient(bulkGetSpy); + + const mockIdMapping = createMockIdMapping( + [[MOCK_KEY_HASH, MOCK_ASYNC_ID]], + moment().subtract(1, 'm') + ); + + const deleteSpy = jest.spyOn(mockIdMapping, 'delete'); + Object.defineProperty(service, 'sessionSearchMap', { + get: () => mockIdMapping, + }); + + // Get setInterval to fire + jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL); + + expect(bulkGetSpy).not.toHaveBeenCalled(); + expect(deleteSpy).toHaveBeenCalledTimes(1); + }); + + it('should delete IDs that passed max retries', async () => { + const bulkGetSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [] }); + createMockInternalSavedObjectClient(bulkGetSpy); + + const mockIdMapping = createMockIdMapping( + [[MOCK_KEY_HASH, MOCK_ASYNC_ID]], + moment().subtract(1, 'm'), + MAX_UPDATE_RETRIES + ); + + const deleteSpy = jest.spyOn(mockIdMapping, 'delete'); + Object.defineProperty(service, 'sessionSearchMap', { + get: () => mockIdMapping, + }); + + // Get setInterval to fire + jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL); + + expect(bulkGetSpy).not.toHaveBeenCalled(); + expect(deleteSpy).toHaveBeenCalledTimes(1); + }); + + it('should bot fetch when no IDs are mapped', async () => { + const bulkGetSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [] }); + createMockInternalSavedObjectClient(bulkGetSpy); + + jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL); + expect(bulkGetSpy).not.toHaveBeenCalled(); + }); + + it('should try to fetch saved objects if some ids are mapped', async () => { + const mockIdMapping = createMockIdMapping([[MOCK_KEY_HASH, MOCK_ASYNC_ID]]); + Object.defineProperty(service, 'sessionSearchMap', { + get: () => mockIdMapping, + }); + + const bulkGetSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [] }); + const bulkUpdateSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [] }); + createMockInternalSavedObjectClient(bulkGetSpy, bulkUpdateSpy); + + jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL); + expect(bulkGetSpy).toHaveBeenCalledTimes(1); + expect(bulkUpdateSpy).not.toHaveBeenCalled(); + }); + + it('should update saved objects if they are found, and delete ids on success', async () => { + const mockIdMapping = createMockIdMapping([[MOCK_KEY_HASH, MOCK_ASYNC_ID]], undefined, 1); + const mockMapDeleteSpy = jest.fn(); + mockIdMapping.get(MOCK_SESSION_ID)!.ids.delete = mockMapDeleteSpy; + Object.defineProperty(service, 'sessionSearchMap', { + get: () => mockIdMapping, + }); + + const bulkGetSpy = jest.fn().mockResolvedValueOnce({ + saved_objects: [ + { + id: MOCK_SESSION_ID, + attributes: { + idMapping: { + b: 'c', + }, + }, + }, + ], + }); + const bulkUpdateSpy = jest.fn().mockResolvedValueOnce({ + saved_objects: [ + { + id: MOCK_SESSION_ID, + attributes: { + idMapping: { + b: 'c', + [MOCK_KEY_HASH]: MOCK_ASYNC_ID, + }, + }, + }, + ], + }); + createMockInternalSavedObjectClient(bulkGetSpy, bulkUpdateSpy); + + jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL); + + // Release timers to call check after test actions are done. + jest.useRealTimers(); + await new Promise((r) => setTimeout(r, 15)); + + expect(bulkGetSpy).toHaveBeenCalledTimes(1); + expect(bulkUpdateSpy).toHaveBeenCalledTimes(1); + expect(mockMapDeleteSpy).toHaveBeenCalledTimes(2); + expect(mockMapDeleteSpy).toBeCalledWith('b'); + expect(mockMapDeleteSpy).toBeCalledWith(MOCK_KEY_HASH); + expect(mockIdMapping.get(MOCK_SESSION_ID)?.retryCount).toBe(0); + }); + + it('should update saved objects if they are found, and increase retryCount on error', async () => { + const mockIdMapping = createMockIdMapping([[MOCK_KEY_HASH, MOCK_ASYNC_ID]]); + const mockMapDeleteSpy = jest.fn(); + mockIdMapping.get(MOCK_SESSION_ID)!.ids.delete = mockMapDeleteSpy; + Object.defineProperty(service, 'sessionSearchMap', { + get: () => mockIdMapping, + }); + + const bulkGetSpy = jest.fn().mockResolvedValueOnce({ + saved_objects: [ + { + id: MOCK_SESSION_ID, + attributes: { + idMapping: { + b: 'c', + }, + }, + }, + ], + }); + const bulkUpdateSpy = jest.fn().mockResolvedValueOnce({ + saved_objects: [ + { + id: MOCK_SESSION_ID, + error: 'not ok', + }, + ], + }); + createMockInternalSavedObjectClient(bulkGetSpy, bulkUpdateSpy); + + jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL); + + // Release timers to call check after test actions are done. + jest.useRealTimers(); + await new Promise((r) => setTimeout(r, 15)); + + expect(bulkGetSpy).toHaveBeenCalledTimes(1); + expect(bulkUpdateSpy).toHaveBeenCalledTimes(1); + expect(mockMapDeleteSpy).not.toHaveBeenCalled(); + expect(mockIdMapping.get(MOCK_SESSION_ID)?.retryCount).toBe(1); + }); + }); }); diff --git a/src/plugins/data/server/search/session/session_service.ts b/src/plugins/data/server/search/session/session_service.ts index eca5f428b8555..b3c1aec6d380e 100644 --- a/src/plugins/data/server/search/session/session_service.ts +++ b/src/plugins/data/server/search/session/session_service.ts @@ -17,7 +17,16 @@ * under the License. */ -import { CoreStart, KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; +import { + CoreStart, + KibanaRequest, + Logger, + SavedObject, + SavedObjectsClient, + SavedObjectsClientContract, + SavedObjectsServiceStart, +} from 'src/core/server'; +import moment, { Moment } from 'moment'; import { BackgroundSessionSavedObjectAttributes, IKibanaSearchRequest, @@ -29,6 +38,9 @@ import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; import { createRequestHash } from './utils'; const DEFAULT_EXPIRATION = 7 * 24 * 60 * 60 * 1000; +export const INMEM_TRACKING_INTERVAL = 2000; +export const INMEM_TRACKING_TIMEOUT_SEC = 60; +export const MAX_UPDATE_RETRIES = 3; export interface BackgroundSessionDependencies { savedObjectsClient: SavedObjectsClientContract; @@ -38,24 +50,127 @@ export type ISearchSessionClient = ReturnType< ReturnType >; +export interface SessionInfo { + insertTime: Moment; + retryCount: number; + ids: Map; +} + export class BackgroundSessionService { /** * Map of sessionId to { [requestHash]: searchId } * @private */ - private sessionSearchMap = new Map>(); + private sessionSearchMap = new Map(); + private internalSavedObjectsClient!: SavedObjectsClientContract; + private monitorInterval!: NodeJS.Timeout; + + constructor(private readonly logger: Logger) {} + + private setupMonitoring = (savedObjects: SavedObjectsServiceStart) => { + const internalRepo = savedObjects.createInternalRepository(); + this.internalSavedObjectsClient = new SavedObjectsClient(internalRepo); + this.monitorInterval = setInterval(this.monitorMappedIds.bind(this), INMEM_TRACKING_INTERVAL); + }; + + /** + * Gets all {@link SessionSavedObjectAttributes | Background Searches} that + * currently being tracked by the service. + * * + * @remarks + * Uses `internalSavedObjectsClient` as this is called asynchronously, not within the + * context of a user's session. + */ + private async getAllMappedSavedObjects() { + const activeMappingIds = Array.from(this.sessionSearchMap.keys()).map((sessionId) => { + return { + id: sessionId, + type: BACKGROUND_SESSION_TYPE, + }; + }); + const res = await this.internalSavedObjectsClient.bulkGet( + activeMappingIds + ); + return res.saved_objects; + } + + private clearSessions = () => { + this.logger.debug(`clearSessions`); + const curTime = moment(); + this.sessionSearchMap.forEach((sessionInfo, sessionId) => { + if ( + moment.duration(curTime.diff(sessionInfo.insertTime)).asSeconds() > + INMEM_TRACKING_TIMEOUT_SEC + ) { + this.logger.debug(`Deleting expired session ${sessionId}`); + this.sessionSearchMap.delete(sessionId); + } else if (sessionInfo.retryCount >= MAX_UPDATE_RETRIES) { + this.logger.warn(`Deleting failed session ${sessionId}`); + this.sessionSearchMap.delete(sessionId); + } + }); + }; + + private async monitorMappedIds() { + try { + this.logger.debug(`monitorMappedIds. Map contains ${this.sessionSearchMap.size} items`); + this.clearSessions(); + + if (!this.sessionSearchMap.size) return; + + const savedSessions = await this.getAllMappedSavedObjects(); + const updatedSessions = await this.updateAllSavedObjects(savedSessions); + + updatedSessions.forEach((updatedSavedObject) => { + const sessionInfo = this.sessionSearchMap.get(updatedSavedObject.id)!; + if (updatedSavedObject.error) { + // Retry next time + sessionInfo.retryCount++; + } else if (updatedSavedObject.attributes.idMapping) { + // Delete the ids that we just saved, avoiding a potential new ids being lost (?) + Object.keys(updatedSavedObject.attributes.idMapping).forEach((key) => { + sessionInfo.ids.delete(key); + }); + sessionInfo.retryCount = 0; + } + }); + } catch (e) { + this.logger.error(`Error fetching sessions. ${e}`); + } + } + + private async updateAllSavedObjects( + activeMappingObjects: Array> + ) { + if (!activeMappingObjects.length) return []; - constructor() {} + const updatedSessions = activeMappingObjects?.map((sessionSavedObject) => { + const sessionInfo = this.sessionSearchMap.get(sessionSavedObject.id); + const idMapping = sessionInfo ? Object.fromEntries(sessionInfo.ids.entries()) : {}; + sessionSavedObject.attributes.idMapping = { + ...sessionSavedObject.attributes.idMapping, + ...idMapping, + }; + return sessionSavedObject; + }); + + const updateResults = await this.internalSavedObjectsClient.bulkUpdate( + updatedSessions + ); + return updateResults.saved_objects; + } public setup = () => {}; public start = (core: CoreStart) => { + this.setupMonitoring(core.savedObjects); return { asScoped: this.asScopedProvider(core), }; }; public stop = () => { + clearInterval(this.monitorInterval); this.sessionSearchMap.clear(); }; @@ -75,8 +190,8 @@ export class BackgroundSessionService { if (!name) throw new Error('Name is required'); // Get the mapping of request hash/search ID for this session - const searchMap = this.sessionSearchMap.get(sessionId) ?? new Map(); - const idMapping = Object.fromEntries(searchMap.entries()); + const searchMap = this.sessionSearchMap.get(sessionId); + const idMapping = searchMap ? Object.fromEntries(searchMap.ids.entries()) : {}; const attributes = { name, created, expires, status, initialState, restoreState, idMapping }; const session = await savedObjectsClient.create( BACKGROUND_SESSION_TYPE, @@ -147,8 +262,12 @@ export class BackgroundSessionService { const attributes = { idMapping: { [requestHash]: searchId } }; await this.update(sessionId, attributes, deps); } else { - const map = this.sessionSearchMap.get(sessionId) ?? new Map(); - map.set(requestHash, searchId); + const map = this.sessionSearchMap.get(sessionId) ?? { + insertTime: moment(), + retryCount: 0, + ids: new Map(), + }; + map.ids.set(requestHash, searchId); this.sessionSearchMap.set(sessionId, map); } }; From 52032fc2108bc60bb776b92e1e9c07447d896a4e Mon Sep 17 00:00:00 2001 From: Liza K Date: Tue, 24 Nov 2020 18:24:19 +0200 Subject: [PATCH 02/26] import fix --- .../data/server/search/search_service.ts | 1 + .../server/search/session/session_service.ts | 39 ++++++++++--------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 42f829cd64ec6..7fc53a67f350d 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -188,6 +188,7 @@ export class SearchService implements Plugin { ): ISearchStart { const { elasticsearch, savedObjects, uiSettings } = core; const asScoped = this.asScopedProvider(core); + this.sessionService.start(core); return { aggs: this.aggsService.start({ fieldFormats, diff --git a/src/plugins/data/server/search/session/session_service.ts b/src/plugins/data/server/search/session/session_service.ts index b3c1aec6d380e..288071652e0b7 100644 --- a/src/plugins/data/server/search/session/session_service.ts +++ b/src/plugins/data/server/search/session/session_service.ts @@ -17,6 +17,7 @@ * under the License. */ +import moment, { Moment } from 'moment'; import { CoreStart, KibanaRequest, @@ -25,8 +26,7 @@ import { SavedObjectsClient, SavedObjectsClientContract, SavedObjectsServiceStart, -} from 'src/core/server'; -import moment, { Moment } from 'moment'; +} from '../../../../../core/server'; import { BackgroundSessionSavedObjectAttributes, IKibanaSearchRequest, @@ -38,7 +38,7 @@ import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; import { createRequestHash } from './utils'; const DEFAULT_EXPIRATION = 7 * 24 * 60 * 60 * 1000; -export const INMEM_TRACKING_INTERVAL = 2000; +export const INMEM_TRACKING_INTERVAL = 10 * 1000; export const INMEM_TRACKING_TIMEOUT_SEC = 60; export const MAX_UPDATE_RETRIES = 3; @@ -88,14 +88,13 @@ export class BackgroundSessionService { type: BACKGROUND_SESSION_TYPE, }; }); - const res = await this.internalSavedObjectsClient.bulkGet( - activeMappingIds - ); + const res = await this.internalSavedObjectsClient.bulkGet< + BackgroundSessionSavedObjectAttributes + >(activeMappingIds); return res.saved_objects; } private clearSessions = () => { - this.logger.debug(`clearSessions`); const curTime = moment(); this.sessionSearchMap.forEach((sessionInfo, sessionId) => { if ( @@ -144,19 +143,21 @@ export class BackgroundSessionService { ) { if (!activeMappingObjects.length) return []; - const updatedSessions = activeMappingObjects?.map((sessionSavedObject) => { - const sessionInfo = this.sessionSearchMap.get(sessionSavedObject.id); - const idMapping = sessionInfo ? Object.fromEntries(sessionInfo.ids.entries()) : {}; - sessionSavedObject.attributes.idMapping = { - ...sessionSavedObject.attributes.idMapping, - ...idMapping, - }; - return sessionSavedObject; - }); + const updatedSessions = activeMappingObjects + ?.filter((so) => !so.error) + .map((sessionSavedObject) => { + const sessionInfo = this.sessionSearchMap.get(sessionSavedObject.id); + const idMapping = sessionInfo ? Object.fromEntries(sessionInfo.ids.entries()) : {}; + sessionSavedObject.attributes.idMapping = { + ...sessionSavedObject.attributes.idMapping, + ...idMapping, + }; + return sessionSavedObject; + }); - const updateResults = await this.internalSavedObjectsClient.bulkUpdate( - updatedSessions - ); + const updateResults = await this.internalSavedObjectsClient.bulkUpdate< + BackgroundSessionSavedObjectAttributes + >(updatedSessions); return updateResults.saved_objects; } From 8a1299197d4cad8c9f185506b155deeb224bba00 Mon Sep 17 00:00:00 2001 From: Liza K Date: Wed, 25 Nov 2020 16:47:28 +0200 Subject: [PATCH 03/26] solve circular dep --- .../create_or_upgrade_saved_config.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts index 10a30db038174..3f61db4292604 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts @@ -19,10 +19,10 @@ import Chance from 'chance'; +import { getUpgradeableConfigMock } from './get_upgradeable_config.test.mock'; import { SavedObjectsErrorHelpers } from '../../saved_objects'; import { savedObjectsClientMock } from '../../saved_objects/service/saved_objects_client.mock'; import { loggingSystemMock } from '../../logging/logging_system.mock'; -import { getUpgradeableConfigMock } from './get_upgradeable_config.test.mock'; import { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; From d692c1e4e2748ff9d9bb6ed29b1f72f1e2ef0ea1 Mon Sep 17 00:00:00 2001 From: Liza K Date: Mon, 30 Nov 2020 19:49:09 +0200 Subject: [PATCH 04/26] eslint --- .../data/server/search/session/session_service.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/plugins/data/server/search/session/session_service.ts b/src/plugins/data/server/search/session/session_service.ts index 288071652e0b7..c984e6a7679be 100644 --- a/src/plugins/data/server/search/session/session_service.ts +++ b/src/plugins/data/server/search/session/session_service.ts @@ -88,9 +88,9 @@ export class BackgroundSessionService { type: BACKGROUND_SESSION_TYPE, }; }); - const res = await this.internalSavedObjectsClient.bulkGet< - BackgroundSessionSavedObjectAttributes - >(activeMappingIds); + const res = await this.internalSavedObjectsClient.bulkGet( + activeMappingIds + ); return res.saved_objects; } @@ -155,9 +155,9 @@ export class BackgroundSessionService { return sessionSavedObject; }); - const updateResults = await this.internalSavedObjectsClient.bulkUpdate< - BackgroundSessionSavedObjectAttributes - >(updatedSessions); + const updateResults = await this.internalSavedObjectsClient.bulkUpdate( + updatedSessions + ); return updateResults.saved_objects; } From 1092c7ab8f4cb2951fd26656f9b71ecc8074a1da Mon Sep 17 00:00:00 2001 From: Liza K Date: Mon, 30 Nov 2020 19:58:36 +0200 Subject: [PATCH 05/26] mock circular dep --- .../spaces/server/routes/api/external/copy_to_space.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts index a6e1c11d011a0..aecdd785fa9d1 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts @@ -25,6 +25,10 @@ import { SpacesService } from '../../../spaces_service'; import { initCopyToSpacesApi } from './copy_to_space'; import { spacesConfig } from '../../../lib/__fixtures__'; import { ObjectType } from '@kbn/config-schema'; + +// Mock out circular dependency +jest.mock('../../../../../../../src/core/server/saved_objects/es_query', () => {}); + jest.mock('../../../../../../../src/core/server', () => { return { ...(jest.requireActual('../../../../../../../src/core/server') as Record), From 500f15d1f3fdc6e4bfb69760bfbfa13fa254bc2b Mon Sep 17 00:00:00 2001 From: Liza K Date: Mon, 30 Nov 2020 19:59:44 +0200 Subject: [PATCH 06/26] max retries test --- src/plugins/data/server/search/session/session_service.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/data/server/search/session/session_service.test.ts b/src/plugins/data/server/search/session/session_service.test.ts index 3a204e2a77fdf..117e2188a9332 100644 --- a/src/plugins/data/server/search/session/session_service.test.ts +++ b/src/plugins/data/server/search/session/session_service.test.ts @@ -340,7 +340,7 @@ describe('BackgroundSessionService', () => { const mockIdMapping = createMockIdMapping( [[MOCK_KEY_HASH, MOCK_ASYNC_ID]], moment().subtract(1, 'm'), - MAX_UPDATE_RETRIES + MAX_UPDATE_RETRIES + 1 ); const deleteSpy = jest.spyOn(mockIdMapping, 'delete'); From c6fea683eef1ca3431547ef2491457fc705a4186 Mon Sep 17 00:00:00 2001 From: Liza K Date: Mon, 30 Nov 2020 20:40:12 +0200 Subject: [PATCH 07/26] mock circular dep --- src/plugins/data/server/search/session/session_service.ts | 1 + .../spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/plugins/data/server/search/session/session_service.ts b/src/plugins/data/server/search/session/session_service.ts index c984e6a7679be..caa37f80a89bf 100644 --- a/src/plugins/data/server/search/session/session_service.ts +++ b/src/plugins/data/server/search/session/session_service.ts @@ -68,6 +68,7 @@ export class BackgroundSessionService { constructor(private readonly logger: Logger) {} private setupMonitoring = (savedObjects: SavedObjectsServiceStart) => { + // TODO: setup monitoring only if BGS is enabled const internalRepo = savedObjects.createInternalRepository(); this.internalSavedObjectsClient = new SavedObjectsClient(internalRepo); this.monitorInterval = setInterval(this.monitorMappedIds.bind(this), INMEM_TRACKING_INTERVAL); diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts index d1a8e93bff929..945a2bdbf6daf 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts @@ -18,6 +18,9 @@ import { } from 'src/core/server/mocks'; import { copySavedObjectsToSpacesFactory } from './copy_to_spaces'; +// Mock out circular dependency +jest.mock('../../../../../../src/core/server/saved_objects/es_query', () => {}); + jest.mock('../../../../../../src/core/server', () => { return { ...(jest.requireActual('../../../../../../src/core/server') as Record), From e5325e9cfc418890c0f14ec5c3b9906e368fdcb5 Mon Sep 17 00:00:00 2001 From: Liza K Date: Mon, 30 Nov 2020 20:45:43 +0200 Subject: [PATCH 08/26] test --- .../data/server/search/session/session_service.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/plugins/data/server/search/session/session_service.test.ts b/src/plugins/data/server/search/session/session_service.test.ts index 117e2188a9332..be8ffda0779d6 100644 --- a/src/plugins/data/server/search/session/session_service.test.ts +++ b/src/plugins/data/server/search/session/session_service.test.ts @@ -108,6 +108,7 @@ describe('BackgroundSessionService', () => { savedObjectsClient = savedObjectsClientMock.create(); const mockLogger: any = { debug: jest.fn(), + warn: jest.fn(), error: jest.fn(), }; service = new BackgroundSessionService(mockLogger); @@ -339,8 +340,8 @@ describe('BackgroundSessionService', () => { const mockIdMapping = createMockIdMapping( [[MOCK_KEY_HASH, MOCK_ASYNC_ID]], - moment().subtract(1, 'm'), - MAX_UPDATE_RETRIES + 1 + moment(), + MAX_UPDATE_RETRIES ); const deleteSpy = jest.spyOn(mockIdMapping, 'delete'); From c540fdea0581983e26e8094be9602cab0bad25e6 Mon Sep 17 00:00:00 2001 From: Liza K Date: Tue, 1 Dec 2020 10:41:29 +0200 Subject: [PATCH 09/26] jest <(-:C --- .../server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts index f1b475ee9d1b3..0e54412ee61ae 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts @@ -18,6 +18,9 @@ import { } from 'src/core/server/mocks'; import { resolveCopySavedObjectsToSpacesConflictsFactory } from './resolve_copy_conflicts'; +// Mock out circular dependency +jest.mock('../../../../../../src/core/server/saved_objects/es_query', () => {}); + jest.mock('../../../../../../src/core/server', () => { return { ...(jest.requireActual('../../../../../../src/core/server') as Record), From 5f7db2da24c838aae322ce5e90342db38b5f220e Mon Sep 17 00:00:00 2001 From: Liza K Date: Tue, 1 Dec 2020 10:59:07 +0200 Subject: [PATCH 10/26] jestttttt --- src/plugins/data/server/search/session/session_service.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/data/server/search/session/session_service.test.ts b/src/plugins/data/server/search/session/session_service.test.ts index be8ffda0779d6..2c905ca205c30 100644 --- a/src/plugins/data/server/search/session/session_service.test.ts +++ b/src/plugins/data/server/search/session/session_service.test.ts @@ -319,7 +319,7 @@ describe('BackgroundSessionService', () => { const mockIdMapping = createMockIdMapping( [[MOCK_KEY_HASH, MOCK_ASYNC_ID]], - moment().subtract(1, 'm') + moment().subtract(2, 'm') ); const deleteSpy = jest.spyOn(mockIdMapping, 'delete'); From 4886819db0e4b7084ce10b91e4f12de762ca8018 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Tue, 1 Dec 2020 16:07:19 -0700 Subject: [PATCH 11/26] [data.search] Move search method inside session service and add tests --- .../data/server/search/search_service.ts | 35 +++----- .../search/session/session_service.test.ts | 89 ++++++++++++++++--- .../server/search/session/session_service.ts | 37 ++++++-- 3 files changed, 122 insertions(+), 39 deletions(-) diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index a9539a8fd3c15..03cbcd534b15f 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { BehaviorSubject, from, Observable } from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; import { pick } from 'lodash'; import { CoreSetup, @@ -29,7 +29,7 @@ import { SharedGlobalConfig, StartServicesAccessor, } from 'src/core/server'; -import { catchError, first, map, switchMap } from 'rxjs/operators'; +import { catchError, first, map } from 'rxjs/operators'; import { BfetchServerSetup } from 'src/plugins/bfetch/server'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { @@ -50,7 +50,11 @@ import { DataPluginStart } from '../plugin'; import { UsageCollectionSetup } from '../../../usage_collection/server'; import { registerUsageCollector } from './collectors/register'; import { usageProvider } from './collectors/usage'; -import { BACKGROUND_SESSION_TYPE, searchTelemetry } from '../saved_objects'; +import { + BACKGROUND_SESSION_TYPE, + backgroundSessionMapping, + searchTelemetry, +} from '../saved_objects'; import { IEsSearchRequest, IEsSearchResponse, @@ -73,8 +77,6 @@ import { aggShardDelay } from '../../common/search/aggs/buckets/shard_delay_fn'; import { ConfigSchema } from '../../config'; import { BackgroundSessionService, ISearchSessionClient } from './session'; import { registerSessionRoutes } from './routes/session'; -import { backgroundSessionMapping } from '../saved_objects'; -import { tapFirst } from '../../common/utils'; declare module 'src/core/server' { interface RequestHandlerContext { @@ -295,7 +297,7 @@ export class SearchService implements Plugin { SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse >( - searchRequest: SearchStrategyRequest, + request: SearchStrategyRequest, options: ISearchOptions, deps: SearchStrategyDependencies ) => { @@ -303,24 +305,9 @@ export class SearchService implements Plugin { options.strategy ); - // If this is a restored background search session, look up the ID using the provided sessionId - const getSearchRequest = async () => - !options.isRestore || searchRequest.id - ? searchRequest - : { - ...searchRequest, - id: await this.sessionService.getId(searchRequest, options, deps), - }; - - return from(getSearchRequest()).pipe( - switchMap((request) => strategy.search(request, options, deps)), - tapFirst((response) => { - if (searchRequest.id || !options.sessionId || !response.id || options.isRestore) return; - this.sessionService.trackId(searchRequest, response.id, options, { - savedObjectsClient: deps.savedObjectsClient, - }); - }) - ); + return options.sessionId + ? this.sessionService.search(strategy, request, options, deps) + : strategy.search(request, options, deps); }; private cancel = (id: string, options: ISearchOptions, deps: SearchStrategyDependencies) => { diff --git a/src/plugins/data/server/search/session/session_service.test.ts b/src/plugins/data/server/search/session/session_service.test.ts index 5ff6d4b932487..167aa8c4099e0 100644 --- a/src/plugins/data/server/search/session/session_service.test.ts +++ b/src/plugins/data/server/search/session/session_service.test.ts @@ -17,7 +17,9 @@ * under the License. */ +import { of } from 'rxjs'; import type { SavedObject, SavedObjectsClientContract } from 'kibana/server'; +import type { SearchStrategyDependencies } from '../types'; import { savedObjectsClientMock } from '../../../../../core/server/mocks'; import { BackgroundSessionStatus } from '../../../common'; import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; @@ -28,6 +30,7 @@ describe('BackgroundSessionService', () => { let savedObjectsClient: jest.Mocked; let service: BackgroundSessionService; + const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; const mockSavedObject: SavedObject = { id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', type: BACKGROUND_SESSION_TYPE, @@ -45,9 +48,13 @@ describe('BackgroundSessionService', () => { service = new BackgroundSessionService(); }); - it('save throws if `name` is not provided', () => { - const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + it('search throws if `name` is not provided', () => { + expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot( + `[Error: Name is required]` + ); + }); + it('save throws if `name` is not provided', () => { expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot( `[Error: Name is required]` ); @@ -56,7 +63,6 @@ describe('BackgroundSessionService', () => { it('get calls saved objects client', async () => { savedObjectsClient.get.mockResolvedValue(mockSavedObject); - const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; const response = await service.get(sessionId, { savedObjectsClient }); expect(response).toBe(mockSavedObject); @@ -93,7 +99,6 @@ describe('BackgroundSessionService', () => { }; savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); - const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; const attributes = { name: 'new_name' }; const response = await service.update(sessionId, attributes, { savedObjectsClient }); @@ -108,19 +113,87 @@ describe('BackgroundSessionService', () => { it('delete calls saved objects client', async () => { savedObjectsClient.delete.mockResolvedValue({}); - const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; const response = await service.delete(sessionId, { savedObjectsClient }); expect(response).toEqual({}); expect(savedObjectsClient.delete).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); }); + describe('search', () => { + const mockSearch = jest.fn().mockReturnValue(of({})); + const mockStrategy = { search: mockSearch }; + const mockDeps = {} as SearchStrategyDependencies; + + beforeEach(() => { + mockSearch.mockClear(); + }); + + it('searches using the original request if not restoring', async () => { + const searchRequest = { params: {} }; + const options = { sessionId, isStored: false, isRestore: false }; + + await service.search(mockStrategy, searchRequest, options, mockDeps).toPromise(); + + expect(mockSearch).toBeCalledWith(searchRequest, options, mockDeps); + }); + + it('searches using the original request if `id` is provided', async () => { + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + const searchRequest = { id: searchId, params: {} }; + const options = { sessionId, isStored: true, isRestore: true }; + + await service.search(mockStrategy, searchRequest, options, mockDeps).toPromise(); + + expect(mockSearch).toBeCalledWith(searchRequest, options, mockDeps); + }); + + it('searches by looking up an `id` if restoring and `id` is not provided', async () => { + const searchRequest = { params: {} }; + const options = { sessionId, isStored: true, isRestore: true }; + const spyGetId = jest.spyOn(service, 'getId').mockResolvedValueOnce('my_id'); + + await service.search(mockStrategy, searchRequest, options, mockDeps).toPromise(); + + expect(mockSearch).toBeCalledWith({ ...searchRequest, id: 'my_id' }, options, mockDeps); + + spyGetId.mockRestore(); + }); + + it('calls `trackId` once if the response contains an `id` and not restoring', async () => { + const searchRequest = { params: {} }; + const options = { sessionId, isStored: false, isRestore: false }; + const spyTrackId = jest.spyOn(service, 'trackId').mockResolvedValue(); + mockSearch.mockReturnValueOnce(of({ id: 'my_id' }, { id: 'my_id' })); + + await service.search(mockStrategy, searchRequest, options, mockDeps).toPromise(); + + expect(spyTrackId).toBeCalledTimes(1); + expect(spyTrackId).toBeCalledWith(searchRequest, 'my_id', options, mockDeps); + + spyTrackId.mockRestore(); + }); + + it('does not call `trackId` if restoring', async () => { + const searchRequest = { params: {} }; + const options = { sessionId, isStored: true, isRestore: true }; + const spyGetId = jest.spyOn(service, 'getId').mockResolvedValueOnce('my_id'); + const spyTrackId = jest.spyOn(service, 'trackId').mockResolvedValue(); + mockSearch.mockReturnValueOnce(of({ id: 'my_id' })); + + await service.search(mockStrategy, searchRequest, options, mockDeps).toPromise(); + + expect(spyTrackId).not.toBeCalled(); + + spyGetId.mockRestore(); + spyTrackId.mockRestore(); + }); + }); + describe('trackId', () => { it('stores hash in memory when `isStored` is `false` for when `save` is called', async () => { const searchRequest = { params: {} }; const requestHash = createRequestHash(searchRequest.params); const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; const isStored = false; const name = 'my saved background search session'; const appId = 'my_app_id'; @@ -164,7 +237,6 @@ describe('BackgroundSessionService', () => { const searchRequest = { params: {} }; const requestHash = createRequestHash(searchRequest.params); const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; const isStored = true; await service.trackId( @@ -191,7 +263,6 @@ describe('BackgroundSessionService', () => { it('throws if there is not a saved object', () => { const searchRequest = { params: {} }; - const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; expect(() => service.getId(searchRequest, { sessionId, isStored: false }, { savedObjectsClient }) @@ -202,7 +273,6 @@ describe('BackgroundSessionService', () => { it('throws if not restoring a saved session', () => { const searchRequest = { params: {} }; - const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; expect(() => service.getId( @@ -219,7 +289,6 @@ describe('BackgroundSessionService', () => { const searchRequest = { params: {} }; const requestHash = createRequestHash(searchRequest.params); const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; const mockSession = { id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', type: BACKGROUND_SESSION_TYPE, diff --git a/src/plugins/data/server/search/session/session_service.ts b/src/plugins/data/server/search/session/session_service.ts index b9a738413ede4..d997af728b60c 100644 --- a/src/plugins/data/server/search/session/session_service.ts +++ b/src/plugins/data/server/search/session/session_service.ts @@ -18,14 +18,19 @@ */ import { CoreStart, KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; +import { from } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; import { BackgroundSessionSavedObjectAttributes, + BackgroundSessionStatus, IKibanaSearchRequest, + IKibanaSearchResponse, ISearchOptions, SearchSessionFindOptions, - BackgroundSessionStatus, + tapFirst, } from '../../../common'; import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; +import { ISearchStrategy, SearchStrategyDependencies } from '../types'; import { createRequestHash } from './utils'; const DEFAULT_EXPIRATION = 7 * 24 * 60 * 60 * 1000; @@ -59,6 +64,32 @@ export class BackgroundSessionService { this.sessionSearchMap.clear(); }; + public search = ( + strategy: ISearchStrategy, + searchRequest: Request, + options: ISearchOptions, + deps: SearchStrategyDependencies + ) => { + // If this is a restored background search session, look up the ID using the provided sessionId + const getSearchRequest = async () => + !options.isRestore || searchRequest.id + ? searchRequest + : { + ...searchRequest, + id: await this.getId(searchRequest, options, deps), + }; + + return from(getSearchRequest()).pipe( + switchMap((request) => strategy.search(request, options, deps)), + tapFirst((response) => { + if (searchRequest.id || !options.sessionId || !response.id || options.isRestore) return; + this.trackId(searchRequest, response.id, options, { + savedObjectsClient: deps.savedObjectsClient, + }); + }) + ); + }; + // TODO: Generate the `userId` from the realm type/realm name/username public save = async ( sessionId: string, @@ -208,10 +239,6 @@ export class BackgroundSessionService { update: (sessionId: string, attributes: Partial) => this.update(sessionId, attributes, deps), delete: (sessionId: string) => this.delete(sessionId, deps), - trackId: (searchRequest: IKibanaSearchRequest, searchId: string, options: ISearchOptions) => - this.trackId(searchRequest, searchId, options, deps), - getId: (searchRequest: IKibanaSearchRequest, options: ISearchOptions) => - this.getId(searchRequest, options, deps), }; }; }; From a91ea100387420f77aac01f239dd7cee102350d7 Mon Sep 17 00:00:00 2001 From: Liza K Date: Wed, 2 Dec 2020 16:56:47 +0200 Subject: [PATCH 12/26] merge --- src/plugins/data/server/search/session/session_service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/data/server/search/session/session_service.ts b/src/plugins/data/server/search/session/session_service.ts index b57ceb1b924a3..a70a6dc0780aa 100644 --- a/src/plugins/data/server/search/session/session_service.ts +++ b/src/plugins/data/server/search/session/session_service.ts @@ -196,8 +196,8 @@ export class BackgroundSessionService { if (!urlGeneratorId) throw new Error('UrlGeneratorId is required'); // Get the mapping of request hash/search ID for this session - const searchMap = this.sessionSearchMap.get(sessionId) ?? new Map(); - const idMapping = Object.fromEntries(searchMap.entries()); + const searchMap = this.sessionSearchMap.get(sessionId); + const idMapping = searchMap ? Object.fromEntries(searchMap.ids.entries()) : {}; const attributes = { name, created, From 0de773085dc0ff83508ac33244c967a41052c250 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Wed, 2 Dec 2020 14:31:22 -0700 Subject: [PATCH 13/26] Move background session service to data_enhanced plugin --- .../kibana-plugin-plugins-data-public.md | 2 +- ...blic.searchsessioninfoprovider.getname.md} | 2 +- ...essioninfoprovider.geturlgeneratordata.md} | 2 +- ...-data-public.searchsessioninfoprovider.md} | 6 +- ...server.isessionservice.asscopedprovider.md | 11 + ...gin-plugins-data-server.isessionservice.md | 18 + .../kibana-plugin-plugins-data-server.md | 2 + ...ata-server.sessionservice._constructor_.md | 13 + ...-server.sessionservice.asscopedprovider.md | 26 ++ ...ugin-plugins-data-server.sessionservice.md | 27 ++ ...ugins-data-server.sessionservice.search.md | 23 ++ src/plugins/data/common/search/index.ts | 1 - .../data/common/search/session/index.ts | 21 -- .../data/common/search/session/status.ts | 26 -- src/plugins/data/public/public.api.md | 1 + .../public/search/session/sessions_client.ts | 16 +- src/plugins/data/server/index.ts | 2 + .../data/server/saved_objects/index.ts | 1 - src/plugins/data/server/search/index.ts | 1 + src/plugins/data/server/search/mocks.ts | 8 +- .../data/server/search/search_service.ts | 37 +-- .../data/server/search/session/index.ts | 3 +- .../search/session/session_service.test.ts | 298 +---------------- .../server/search/session/session_service.ts | 238 +------------ .../search/session/types.ts | 35 +- .../data/server/search/session/utils.test.ts | 37 --- .../data/server/search/session/utils.ts | 30 -- src/plugins/data/server/search/types.ts | 2 + src/plugins/data/server/server.api.md | 68 ++-- src/plugins/embeddable/public/public.api.md | 1 + x-pack/plugins/data_enhanced/common/index.ts | 3 + .../data_enhanced/common/search/index.ts | 1 + .../common/search/session/index.ts | 8 + .../common/search/session/status.ts | 13 + .../common/search/session/types.ts | 31 ++ x-pack/plugins/data_enhanced/server/plugin.ts | 23 +- .../data_enhanced/server/routes/index.ts | 7 + .../server}/routes/session.test.ts | 25 +- .../data_enhanced/server}/routes/session.ts | 19 +- .../saved_objects/background_session.ts | 19 +- .../server/saved_objects/index.ts | 7 + .../data_enhanced/server/search/index.ts | 1 + .../server/search/session/index.ts | 7 + .../search/session/session_service.test.ts | 312 ++++++++++++++++++ .../server/search/session/session_service.ts | 225 +++++++++++++ .../server/search/session/utils.test.ts | 24 ++ .../server/search/session/utils.ts | 17 + 47 files changed, 925 insertions(+), 775 deletions(-) rename docs/development/plugins/data/public/{kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.getname.md => kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md} (83%) rename docs/development/plugins/data/public/{kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.geturlgeneratordata.md => kibana-plugin-plugins-data-public.searchsessioninfoprovider.geturlgeneratordata.md} (82%) rename docs/development/plugins/data/public/{kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md => kibana-plugin-plugins-data-public.searchsessioninfoprovider.md} (84%) create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isessionservice.asscopedprovider.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isessionservice.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice._constructor_.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.asscopedprovider.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.search.md delete mode 100644 src/plugins/data/common/search/session/index.ts delete mode 100644 src/plugins/data/common/search/session/status.ts rename src/plugins/data/{common => server}/search/session/types.ts (55%) delete mode 100644 src/plugins/data/server/search/session/utils.test.ts delete mode 100644 src/plugins/data/server/search/session/utils.ts create mode 100644 x-pack/plugins/data_enhanced/common/search/session/index.ts create mode 100644 x-pack/plugins/data_enhanced/common/search/session/status.ts create mode 100644 x-pack/plugins/data_enhanced/common/search/session/types.ts create mode 100644 x-pack/plugins/data_enhanced/server/routes/index.ts rename {src/plugins/data/server/search => x-pack/plugins/data_enhanced/server}/routes/session.test.ts (78%) rename {src/plugins/data/server/search => x-pack/plugins/data_enhanced/server}/routes/session.ts (85%) rename {src/plugins/data => x-pack/plugins/data_enhanced}/server/saved_objects/background_session.ts (51%) create mode 100644 x-pack/plugins/data_enhanced/server/saved_objects/index.ts create mode 100644 x-pack/plugins/data_enhanced/server/search/session/index.ts create mode 100644 x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts create mode 100644 x-pack/plugins/data_enhanced/server/search/session/session_service.ts create mode 100644 x-pack/plugins/data_enhanced/server/search/session/utils.test.ts create mode 100644 x-pack/plugins/data_enhanced/server/search/session/utils.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 9121b0aade470..08ed14b92d24c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -89,7 +89,7 @@ | [SavedQueryService](./kibana-plugin-plugins-data-public.savedqueryservice.md) | | | [SearchError](./kibana-plugin-plugins-data-public.searcherror.md) | | | [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) | | -| [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) | Provide info about current search session to be stored in backgroundSearch saved object | +| [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) | Provide info about current search session to be stored in backgroundSearch saved object | | [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) | search source fields | | [TabbedAggColumn](./kibana-plugin-plugins-data-public.tabbedaggcolumn.md) | \* | | [TabbedTable](./kibana-plugin-plugins-data-public.tabbedtable.md) | \* | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.getname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md similarity index 83% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.getname.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md index 0f0b616066dd6..2a5e1d2a3135f 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.getname.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md @@ -1,6 +1,6 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) > [getName](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.getname.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) > [getName](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md) ## SearchSessionInfoProvider.getName property diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.geturlgeneratordata.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.geturlgeneratordata.md similarity index 82% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.geturlgeneratordata.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.geturlgeneratordata.md index 207adaf2bd50b..01558ed3dddad 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.geturlgeneratordata.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.geturlgeneratordata.md @@ -1,6 +1,6 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) > [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.geturlgeneratordata.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) > [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.geturlgeneratordata.md) ## SearchSessionInfoProvider.getUrlGeneratorData property diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.md similarity index 84% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.md index a3d294f5e3303..bcc4a5508eb59 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessionrestorationinfoprovider.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsessioninfoprovider.md @@ -1,6 +1,6 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) ## SearchSessionInfoProvider interface @@ -16,6 +16,6 @@ export interface SearchSessionInfoProvider() => Promise<string> | User-facing name of the session. e.g. will be displayed in background sessions management list | -| [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.geturlgeneratordata.md) | () => Promise<{
urlGeneratorId: ID;
initialState: UrlGeneratorStateMapping[ID]['State'];
restoreState: UrlGeneratorStateMapping[ID]['State'];
}> | | +| [getName](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md) | () => Promise<string> | User-facing name of the session. e.g. will be displayed in background sessions management list | +| [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.geturlgeneratordata.md) | () => Promise<{
urlGeneratorId: ID;
initialState: UrlGeneratorStateMapping[ID]['State'];
restoreState: UrlGeneratorStateMapping[ID]['State'];
}> | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isessionservice.asscopedprovider.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isessionservice.asscopedprovider.md new file mode 100644 index 0000000000000..d52b9b783919b --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isessionservice.asscopedprovider.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISessionService](./kibana-plugin-plugins-data-server.isessionservice.md) > [asScopedProvider](./kibana-plugin-plugins-data-server.isessionservice.asscopedprovider.md) + +## ISessionService.asScopedProvider property + +Signature: + +```typescript +asScopedProvider: (core: CoreStart) => (request: KibanaRequest) => IScopedSessionService; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isessionservice.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isessionservice.md new file mode 100644 index 0000000000000..dcc7dfc8bb946 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isessionservice.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISessionService](./kibana-plugin-plugins-data-server.isessionservice.md) + +## ISessionService interface + +Signature: + +```typescript +export interface ISessionService +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [asScopedProvider](./kibana-plugin-plugins-data-server.isessionservice.asscopedprovider.md) | (core: CoreStart) => (request: KibanaRequest) => IScopedSessionService | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index c85f294d162bc..55504e01b6c9a 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -14,6 +14,7 @@ | [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) | | | [OptionedParamType](./kibana-plugin-plugins-data-server.optionedparamtype.md) | | | [Plugin](./kibana-plugin-plugins-data-server.plugin.md) | | +| [SessionService](./kibana-plugin-plugins-data-server.sessionservice.md) | The OSS session service. See data\_enhanced in X-Pack for the background session service. | ## Enumerations @@ -54,6 +55,7 @@ | [ISearchSetup](./kibana-plugin-plugins-data-server.isearchsetup.md) | | | [ISearchStart](./kibana-plugin-plugins-data-server.isearchstart.md) | | | [ISearchStrategy](./kibana-plugin-plugins-data-server.isearchstrategy.md) | Search strategy interface contains a search method that takes in a request and returns a promise that resolves to a response. | +| [ISessionService](./kibana-plugin-plugins-data-server.isessionservice.md) | | | [KueryNode](./kibana-plugin-plugins-data-server.kuerynode.md) | | | [OptionedValueProp](./kibana-plugin-plugins-data-server.optionedvalueprop.md) | | | [PluginSetup](./kibana-plugin-plugins-data-server.pluginsetup.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice._constructor_.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice._constructor_.md new file mode 100644 index 0000000000000..73d658455a66f --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice._constructor_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SessionService](./kibana-plugin-plugins-data-server.sessionservice.md) > [(constructor)](./kibana-plugin-plugins-data-server.sessionservice._constructor_.md) + +## SessionService.(constructor) + +Constructs a new instance of the `SessionService` class + +Signature: + +```typescript +constructor(); +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.asscopedprovider.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.asscopedprovider.md new file mode 100644 index 0000000000000..f3af7fb0f61d9 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.asscopedprovider.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SessionService](./kibana-plugin-plugins-data-server.sessionservice.md) > [asScopedProvider](./kibana-plugin-plugins-data-server.sessionservice.asscopedprovider.md) + +## SessionService.asScopedProvider() method + +Signature: + +```typescript +asScopedProvider(core: CoreStart): (request: KibanaRequest) => { + search: , Response_1 extends IKibanaSearchResponse>(strategy: ISearchStrategy, request: Request_1, options: import("../../../common").ISearchOptions, deps: import("../types").SearchStrategyDependencies) => import("rxjs").Observable; + }; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreStart | | + +Returns: + +`(request: KibanaRequest) => { + search: , Response_1 extends IKibanaSearchResponse>(strategy: ISearchStrategy, request: Request_1, options: import("../../../common").ISearchOptions, deps: import("../types").SearchStrategyDependencies) => import("rxjs").Observable; + }` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.md new file mode 100644 index 0000000000000..2369b00644548 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SessionService](./kibana-plugin-plugins-data-server.sessionservice.md) + +## SessionService class + +The OSS session service. See data\_enhanced in X-Pack for the background session service. + +Signature: + +```typescript +export declare class SessionService implements ISessionService +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)()](./kibana-plugin-plugins-data-server.sessionservice._constructor_.md) | | Constructs a new instance of the SessionService class | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [asScopedProvider(core)](./kibana-plugin-plugins-data-server.sessionservice.asscopedprovider.md) | | | +| [search(strategy, args)](./kibana-plugin-plugins-data-server.sessionservice.search.md) | | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.search.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.search.md new file mode 100644 index 0000000000000..8f8620a6ed5ee --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.sessionservice.search.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SessionService](./kibana-plugin-plugins-data-server.sessionservice.md) > [search](./kibana-plugin-plugins-data-server.sessionservice.search.md) + +## SessionService.search() method + +Signature: + +```typescript +search(strategy: ISearchStrategy, ...args: Parameters['search']>): import("rxjs").Observable; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| strategy | ISearchStrategy<Request, Response> | | +| args | Parameters<ISearchStrategy<Request, Response>['search']> | | + +Returns: + +`import("rxjs").Observable` + diff --git a/src/plugins/data/common/search/index.ts b/src/plugins/data/common/search/index.ts index 01944d6e37aaf..d5939b4ec9cbf 100644 --- a/src/plugins/data/common/search/index.ts +++ b/src/plugins/data/common/search/index.ts @@ -23,5 +23,4 @@ export * from './expressions'; export * from './search_source'; export * from './tabify'; export * from './types'; -export * from './session'; export * from './utils'; diff --git a/src/plugins/data/common/search/session/index.ts b/src/plugins/data/common/search/session/index.ts deleted file mode 100644 index 0feb43f8f1d4b..0000000000000 --- a/src/plugins/data/common/search/session/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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. - */ - -export * from './status'; -export * from './types'; diff --git a/src/plugins/data/common/search/session/status.ts b/src/plugins/data/common/search/session/status.ts deleted file mode 100644 index 1f6b6eb3084bb..0000000000000 --- a/src/plugins/data/common/search/session/status.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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. - */ - -export enum BackgroundSessionStatus { - IN_PROGRESS = 'in_progress', - ERROR = 'error', - COMPLETE = 'complete', - CANCELLED = 'cancelled', - EXPIRED = 'expired', -} diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index ad1861cecea0b..cc4e32cbce157 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -81,6 +81,7 @@ import { SavedObject as SavedObject_2 } from 'src/core/server'; import { SavedObject as SavedObject_3 } from 'src/core/public'; import { SavedObjectReference } from 'src/core/types'; import { SavedObjectsClientContract } from 'src/core/public'; +import { SavedObjectsFindOptions } from 'kibana/public'; import { SavedObjectsFindResponse } from 'kibana/server'; import { Search } from '@elastic/elasticsearch/api/requestParams'; import { SearchResponse } from 'elasticsearch'; diff --git a/src/plugins/data/public/search/session/sessions_client.ts b/src/plugins/data/public/search/session/sessions_client.ts index c19c5db064094..38be647a37c7a 100644 --- a/src/plugins/data/public/search/session/sessions_client.ts +++ b/src/plugins/data/public/search/session/sessions_client.ts @@ -18,9 +18,8 @@ */ import { PublicContract } from '@kbn/utility-types'; -import { HttpSetup } from 'kibana/public'; +import { HttpSetup, SavedObjectsFindOptions } from 'kibana/public'; import type { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; -import { BackgroundSessionSavedObjectAttributes, SearchSessionFindOptions } from '../../../common'; export type ISessionsClient = PublicContract; export interface SessionsClientDeps { @@ -37,7 +36,7 @@ export class SessionsClient { this.http = deps.http; } - public get(sessionId: string): Promise> { + public get(sessionId: string): Promise { return this.http.get(`/internal/session/${encodeURIComponent(sessionId)}`); } @@ -55,7 +54,7 @@ export class SessionsClient { restoreState: Record; urlGeneratorId: string; sessionId: string; - }): Promise> { + }): Promise { return this.http.post(`/internal/session`, { body: JSON.stringify({ name, @@ -68,18 +67,13 @@ export class SessionsClient { }); } - public find( - options: SearchSessionFindOptions - ): Promise> { + public find(options: SavedObjectsFindOptions): Promise { return this.http!.post(`/internal/session`, { body: JSON.stringify(options), }); } - public update( - sessionId: string, - attributes: Partial - ): Promise> { + public update(sessionId: string, attributes: unknown): Promise { return this.http!.put(`/internal/session/${encodeURIComponent(sessionId)}`, { body: JSON.stringify(attributes), }); diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index a233447cdf438..4a4e1df2cf45f 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -242,6 +242,8 @@ export { searchUsageObserver, shimAbortSignal, SearchUsage, + SessionService, + ISessionService, } from './search'; // Search namespace diff --git a/src/plugins/data/server/saved_objects/index.ts b/src/plugins/data/server/saved_objects/index.ts index 7cd4d319e6417..077f9380823d0 100644 --- a/src/plugins/data/server/saved_objects/index.ts +++ b/src/plugins/data/server/saved_objects/index.ts @@ -20,4 +20,3 @@ export { querySavedObjectType } from './query'; export { indexPatternSavedObjectType } from './index_patterns'; export { kqlTelemetry } from './kql_telemetry'; export { searchTelemetry } from './search_telemetry'; -export { BACKGROUND_SESSION_TYPE, backgroundSessionMapping } from './background_session'; diff --git a/src/plugins/data/server/search/index.ts b/src/plugins/data/server/search/index.ts index 3001bbe3c2f38..f051e4c48223c 100644 --- a/src/plugins/data/server/search/index.ts +++ b/src/plugins/data/server/search/index.ts @@ -22,3 +22,4 @@ export * from './es_search'; export { usageProvider, SearchUsage, searchUsageObserver } from './collectors'; export * from './aggs'; export { shimHitsTotal } from './routes'; +export * from './session'; diff --git a/src/plugins/data/server/search/mocks.ts b/src/plugins/data/server/search/mocks.ts index 290e94ee7cf99..6d214008e8b17 100644 --- a/src/plugins/data/server/search/mocks.ts +++ b/src/plugins/data/server/search/mocks.ts @@ -50,13 +50,7 @@ export function createSearchRequestHandlerContext(): jest.Mocked { private defaultSearchStrategyName: string = ES_SEARCH_STRATEGY; private searchStrategies: StrategyMap = {}; private coreStart?: CoreStart; - private sessionService: BackgroundSessionService = new BackgroundSessionService(); + private sessionService: ISessionService = new SessionService(); constructor( private initializerContext: PluginInitializerContext, @@ -131,7 +126,6 @@ export class SearchService implements Plugin { }; registerSearchRoute(router); registerMsearchRoute(router, routeDependencies); - registerSessionRoutes(router); core.getStartServices().then(([coreStart]) => { this.coreStart = coreStart; @@ -143,8 +137,6 @@ export class SearchService implements Plugin { return { ...search, session }; }); - core.savedObjects.registerType(backgroundSessionMapping); - this.registerSearchStrategy( ES_SEARCH_STRATEGY, esSearchStrategyProvider( @@ -219,6 +211,7 @@ export class SearchService implements Plugin { if (this.searchStrategies.hasOwnProperty(enhancements.defaultStrategy)) { this.defaultSearchStrategyName = enhancements.defaultStrategy; } + this.sessionService = enhancements.sessionService; }, aggs, registerSearchStrategy: this.registerSearchStrategy, @@ -279,7 +272,6 @@ export class SearchService implements Plugin { public stop() { this.aggsService.stop(); - this.sessionService.stop(); } private registerSearchStrategy = < @@ -297,6 +289,7 @@ export class SearchService implements Plugin { SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse >( + session: IScopedSessionService, request: SearchStrategyRequest, options: ISearchOptions, deps: SearchStrategyDependencies @@ -304,15 +297,11 @@ export class SearchService implements Plugin { const strategy = this.getSearchStrategy( options.strategy ); - - return options.sessionId - ? this.sessionService.search(strategy, request, options, deps) - : strategy.search(request, options, deps); + return session.search(strategy, request, options, deps); }; private cancel = (id: string, options: ISearchOptions, deps: SearchStrategyDependencies) => { const strategy = this.getSearchStrategy(options.strategy); - return strategy.cancel ? strategy.cancel(id, options, deps) : Promise.resolve(); }; @@ -330,18 +319,20 @@ export class SearchService implements Plugin { return strategy; }; - private asScopedProvider = ({ elasticsearch, savedObjects, uiSettings }: CoreStart) => { + private asScopedProvider = (core: CoreStart) => { + const { elasticsearch, savedObjects, uiSettings } = core; + const getSessionAsScoped = this.sessionService.asScopedProvider(core); return (request: KibanaRequest): ISearchClient => { - const savedObjectsClient = savedObjects.getScopedClient(request, { - includedHiddenTypes: [BACKGROUND_SESSION_TYPE], - }); + const scopedSession = getSessionAsScoped(request); + const savedObjectsClient = savedObjects.getScopedClient(request); const deps = { savedObjectsClient, esClient: elasticsearch.client.asScoped(request), uiSettingsClient: uiSettings.asScopedToClient(savedObjectsClient), }; return { - search: (searchRequest, options = {}) => this.search(searchRequest, options, deps), + search: (searchRequest, options = {}) => + this.search(scopedSession, searchRequest, options, deps), cancel: (id, options = {}) => this.cancel(id, options, deps), }; }; diff --git a/src/plugins/data/server/search/session/index.ts b/src/plugins/data/server/search/session/index.ts index 11b5b16a02b56..0966a1e4a18ec 100644 --- a/src/plugins/data/server/search/session/index.ts +++ b/src/plugins/data/server/search/session/index.ts @@ -17,4 +17,5 @@ * under the License. */ -export { BackgroundSessionService, ISearchSessionClient } from './session_service'; +export * from './session_service'; +export * from './types'; diff --git a/src/plugins/data/server/search/session/session_service.test.ts b/src/plugins/data/server/search/session/session_service.test.ts index 167aa8c4099e0..3dbe01105aca4 100644 --- a/src/plugins/data/server/search/session/session_service.test.ts +++ b/src/plugins/data/server/search/session/session_service.test.ts @@ -18,297 +18,21 @@ */ import { of } from 'rxjs'; -import type { SavedObject, SavedObjectsClientContract } from 'kibana/server'; -import type { SearchStrategyDependencies } from '../types'; -import { savedObjectsClientMock } from '../../../../../core/server/mocks'; -import { BackgroundSessionStatus } from '../../../common'; -import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; -import { BackgroundSessionService } from './session_service'; -import { createRequestHash } from './utils'; +import { SearchStrategyDependencies } from '../types'; +import { SessionService } from './session_service'; -describe('BackgroundSessionService', () => { - let savedObjectsClient: jest.Mocked; - let service: BackgroundSessionService; - - const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; - const mockSavedObject: SavedObject = { - id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', - type: BACKGROUND_SESSION_TYPE, - attributes: { - name: 'my_name', - appId: 'my_app_id', - urlGeneratorId: 'my_url_generator_id', - idMapping: {}, - }, - references: [], - }; - - beforeEach(() => { - savedObjectsClient = savedObjectsClientMock.create(); - service = new BackgroundSessionService(); - }); - - it('search throws if `name` is not provided', () => { - expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot( - `[Error: Name is required]` - ); - }); - - it('save throws if `name` is not provided', () => { - expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot( - `[Error: Name is required]` - ); - }); - - it('get calls saved objects client', async () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - - const response = await service.get(sessionId, { savedObjectsClient }); - - expect(response).toBe(mockSavedObject); - expect(savedObjectsClient.get).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); - }); - - it('find calls saved objects client', async () => { - const mockFindSavedObject = { - ...mockSavedObject, - score: 1, - }; - const mockResponse = { - saved_objects: [mockFindSavedObject], - total: 1, - per_page: 1, - page: 0, - }; - savedObjectsClient.find.mockResolvedValue(mockResponse); - - const options = { page: 0, perPage: 5 }; - const response = await service.find(options, { savedObjectsClient }); - - expect(response).toBe(mockResponse); - expect(savedObjectsClient.find).toHaveBeenCalledWith({ - ...options, - type: BACKGROUND_SESSION_TYPE, - }); - }); - - it('update calls saved objects client', async () => { - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); - - const attributes = { name: 'new_name' }; - const response = await service.update(sessionId, attributes, { savedObjectsClient }); - - expect(response).toBe(mockUpdateSavedObject); - expect(savedObjectsClient.update).toHaveBeenCalledWith( - BACKGROUND_SESSION_TYPE, - sessionId, - attributes - ); - }); - - it('delete calls saved objects client', async () => { - savedObjectsClient.delete.mockResolvedValue({}); - - const response = await service.delete(sessionId, { savedObjectsClient }); - - expect(response).toEqual({}); - expect(savedObjectsClient.delete).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); - }); - - describe('search', () => { +describe('SessionService', () => { + it('search invokes `strategy.search`', async () => { + const service = new SessionService(); const mockSearch = jest.fn().mockReturnValue(of({})); const mockStrategy = { search: mockSearch }; - const mockDeps = {} as SearchStrategyDependencies; - - beforeEach(() => { - mockSearch.mockClear(); - }); - - it('searches using the original request if not restoring', async () => { - const searchRequest = { params: {} }; - const options = { sessionId, isStored: false, isRestore: false }; - - await service.search(mockStrategy, searchRequest, options, mockDeps).toPromise(); - - expect(mockSearch).toBeCalledWith(searchRequest, options, mockDeps); - }); - - it('searches using the original request if `id` is provided', async () => { - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const searchRequest = { id: searchId, params: {} }; - const options = { sessionId, isStored: true, isRestore: true }; - - await service.search(mockStrategy, searchRequest, options, mockDeps).toPromise(); - - expect(mockSearch).toBeCalledWith(searchRequest, options, mockDeps); - }); - - it('searches by looking up an `id` if restoring and `id` is not provided', async () => { - const searchRequest = { params: {} }; - const options = { sessionId, isStored: true, isRestore: true }; - const spyGetId = jest.spyOn(service, 'getId').mockResolvedValueOnce('my_id'); - - await service.search(mockStrategy, searchRequest, options, mockDeps).toPromise(); - - expect(mockSearch).toBeCalledWith({ ...searchRequest, id: 'my_id' }, options, mockDeps); - - spyGetId.mockRestore(); - }); - - it('calls `trackId` once if the response contains an `id` and not restoring', async () => { - const searchRequest = { params: {} }; - const options = { sessionId, isStored: false, isRestore: false }; - const spyTrackId = jest.spyOn(service, 'trackId').mockResolvedValue(); - mockSearch.mockReturnValueOnce(of({ id: 'my_id' }, { id: 'my_id' })); - - await service.search(mockStrategy, searchRequest, options, mockDeps).toPromise(); - - expect(spyTrackId).toBeCalledTimes(1); - expect(spyTrackId).toBeCalledWith(searchRequest, 'my_id', options, mockDeps); - - spyTrackId.mockRestore(); - }); - - it('does not call `trackId` if restoring', async () => { - const searchRequest = { params: {} }; - const options = { sessionId, isStored: true, isRestore: true }; - const spyGetId = jest.spyOn(service, 'getId').mockResolvedValueOnce('my_id'); - const spyTrackId = jest.spyOn(service, 'trackId').mockResolvedValue(); - mockSearch.mockReturnValueOnce(of({ id: 'my_id' })); - - await service.search(mockStrategy, searchRequest, options, mockDeps).toPromise(); - - expect(spyTrackId).not.toBeCalled(); - - spyGetId.mockRestore(); - spyTrackId.mockRestore(); - }); - }); - - describe('trackId', () => { - it('stores hash in memory when `isStored` is `false` for when `save` is called', async () => { - const searchRequest = { params: {} }; - const requestHash = createRequestHash(searchRequest.params); - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const isStored = false; - const name = 'my saved background search session'; - const appId = 'my_app_id'; - const urlGeneratorId = 'my_url_generator_id'; - const created = new Date().toISOString(); - const expires = new Date().toISOString(); - - await service.trackId( - searchRequest, - searchId, - { sessionId, isStored }, - { savedObjectsClient } - ); - - expect(savedObjectsClient.update).not.toHaveBeenCalled(); - - await service.save( - sessionId, - { name, created, expires, appId, urlGeneratorId }, - { savedObjectsClient } - ); - - expect(savedObjectsClient.create).toHaveBeenCalledWith( - BACKGROUND_SESSION_TYPE, - { - name, - created, - expires, - initialState: {}, - restoreState: {}, - status: BackgroundSessionStatus.IN_PROGRESS, - idMapping: { [requestHash]: searchId }, - appId, - urlGeneratorId, - }, - { id: sessionId } - ); - }); - - it('updates saved object when `isStored` is `true`', async () => { - const searchRequest = { params: {} }; - const requestHash = createRequestHash(searchRequest.params); - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const isStored = true; - - await service.trackId( - searchRequest, - searchId, - { sessionId, isStored }, - { savedObjectsClient } - ); - - expect(savedObjectsClient.update).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId, { - idMapping: { [requestHash]: searchId }, - }); - }); - }); - - describe('getId', () => { - it('throws if `sessionId` is not provided', () => { - const searchRequest = { params: {} }; - - expect(() => - service.getId(searchRequest, {}, { savedObjectsClient }) - ).rejects.toMatchInlineSnapshot(`[Error: Session ID is required]`); - }); - - it('throws if there is not a saved object', () => { - const searchRequest = { params: {} }; - - expect(() => - service.getId(searchRequest, { sessionId, isStored: false }, { savedObjectsClient }) - ).rejects.toMatchInlineSnapshot( - `[Error: Cannot get search ID from a session that is not stored]` - ); - }); - - it('throws if not restoring a saved session', () => { - const searchRequest = { params: {} }; - - expect(() => - service.getId( - searchRequest, - { sessionId, isStored: true, isRestore: false }, - { savedObjectsClient } - ) - ).rejects.toMatchInlineSnapshot( - `[Error: Get search ID is only supported when restoring a session]` - ); - }); - - it('returns the search ID from the saved object ID mapping', async () => { - const searchRequest = { params: {} }; - const requestHash = createRequestHash(searchRequest.params); - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const mockSession = { - id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', - type: BACKGROUND_SESSION_TYPE, - attributes: { - name: 'my_name', - appId: 'my_app_id', - urlGeneratorId: 'my_url_generator_id', - idMapping: { [requestHash]: searchId }, - }, - references: [], - }; - savedObjectsClient.get.mockResolvedValue(mockSession); + const mockRequest = { id: 'bar' }; + const mockOptions = { sessionId: '1234' }; + const mockDeps = { savedObjectsClient: {} } as SearchStrategyDependencies; - const id = await service.getId( - searchRequest, - { sessionId, isStored: true, isRestore: true }, - { savedObjectsClient } - ); + await service.search(mockStrategy, mockRequest, mockOptions, mockDeps); - expect(id).toBe(searchId); - }); + expect(mockSearch).toHaveBeenCalled(); + expect(mockSearch).toHaveBeenCalledWith(mockRequest, mockOptions, mockDeps); }); }); diff --git a/src/plugins/data/server/search/session/session_service.ts b/src/plugins/data/server/search/session/session_service.ts index d997af728b60c..15021436d8821 100644 --- a/src/plugins/data/server/search/session/session_service.ts +++ b/src/plugins/data/server/search/session/session_service.ts @@ -17,229 +17,27 @@ * under the License. */ -import { CoreStart, KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; -import { from } from 'rxjs'; -import { switchMap } from 'rxjs/operators'; -import { - BackgroundSessionSavedObjectAttributes, - BackgroundSessionStatus, - IKibanaSearchRequest, - IKibanaSearchResponse, - ISearchOptions, - SearchSessionFindOptions, - tapFirst, -} from '../../../common'; -import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; -import { ISearchStrategy, SearchStrategyDependencies } from '../types'; -import { createRequestHash } from './utils'; - -const DEFAULT_EXPIRATION = 7 * 24 * 60 * 60 * 1000; - -export interface BackgroundSessionDependencies { - savedObjectsClient: SavedObjectsClientContract; -} - -export type ISearchSessionClient = ReturnType< - ReturnType ->; - -export class BackgroundSessionService { - /** - * Map of sessionId to { [requestHash]: searchId } - * @private - */ - private sessionSearchMap = new Map>(); +import { CoreStart, KibanaRequest } from 'kibana/server'; +import { IKibanaSearchRequest, IKibanaSearchResponse } from '../../../common'; +import { ISearchStrategy } from '../types'; +import { ISessionService } from './types'; +/** + * The OSS session service. See data_enhanced in X-Pack for the background session service. + */ +export class SessionService implements ISessionService { constructor() {} - public setup = () => {}; - - public start = (core: CoreStart) => { - return { - asScoped: this.asScopedProvider(core), - }; - }; - - public stop = () => { - this.sessionSearchMap.clear(); - }; - - public search = ( + public search( strategy: ISearchStrategy, - searchRequest: Request, - options: ISearchOptions, - deps: SearchStrategyDependencies - ) => { - // If this is a restored background search session, look up the ID using the provided sessionId - const getSearchRequest = async () => - !options.isRestore || searchRequest.id - ? searchRequest - : { - ...searchRequest, - id: await this.getId(searchRequest, options, deps), - }; - - return from(getSearchRequest()).pipe( - switchMap((request) => strategy.search(request, options, deps)), - tapFirst((response) => { - if (searchRequest.id || !options.sessionId || !response.id || options.isRestore) return; - this.trackId(searchRequest, response.id, options, { - savedObjectsClient: deps.savedObjectsClient, - }); - }) - ); - }; - - // TODO: Generate the `userId` from the realm type/realm name/username - public save = async ( - sessionId: string, - { - name, - appId, - created = new Date().toISOString(), - expires = new Date(Date.now() + DEFAULT_EXPIRATION).toISOString(), - status = BackgroundSessionStatus.IN_PROGRESS, - urlGeneratorId, - initialState = {}, - restoreState = {}, - }: Partial, - { savedObjectsClient }: BackgroundSessionDependencies - ) => { - if (!name) throw new Error('Name is required'); - if (!appId) throw new Error('AppId is required'); - if (!urlGeneratorId) throw new Error('UrlGeneratorId is required'); - - // Get the mapping of request hash/search ID for this session - const searchMap = this.sessionSearchMap.get(sessionId) ?? new Map(); - const idMapping = Object.fromEntries(searchMap.entries()); - const attributes = { - name, - created, - expires, - status, - initialState, - restoreState, - idMapping, - urlGeneratorId, - appId, - }; - const session = await savedObjectsClient.create( - BACKGROUND_SESSION_TYPE, - attributes, - { id: sessionId } - ); - - // Clear out the entries for this session ID so they don't get saved next time - this.sessionSearchMap.delete(sessionId); - - return session; - }; - - // TODO: Throw an error if this session doesn't belong to this user - public get = (sessionId: string, { savedObjectsClient }: BackgroundSessionDependencies) => { - return savedObjectsClient.get( - BACKGROUND_SESSION_TYPE, - sessionId - ); - }; - - // TODO: Throw an error if this session doesn't belong to this user - public find = ( - options: SearchSessionFindOptions, - { savedObjectsClient }: BackgroundSessionDependencies - ) => { - return savedObjectsClient.find({ - ...options, - type: BACKGROUND_SESSION_TYPE, + ...args: Parameters['search']> + ) { + return strategy.search(...args); + } + + public asScopedProvider(core: CoreStart) { + return (request: KibanaRequest) => ({ + search: this.search, }); - }; - - // TODO: Throw an error if this session doesn't belong to this user - public update = ( - sessionId: string, - attributes: Partial, - { savedObjectsClient }: BackgroundSessionDependencies - ) => { - return savedObjectsClient.update( - BACKGROUND_SESSION_TYPE, - sessionId, - attributes - ); - }; - - // TODO: Throw an error if this session doesn't belong to this user - public delete = (sessionId: string, { savedObjectsClient }: BackgroundSessionDependencies) => { - return savedObjectsClient.delete(BACKGROUND_SESSION_TYPE, sessionId); - }; - - /** - * Tracks the given search request/search ID in the saved session (if it exists). Otherwise, just - * store it in memory until a saved session exists. - * @internal - */ - public trackId = async ( - searchRequest: IKibanaSearchRequest, - searchId: string, - { sessionId, isStored }: ISearchOptions, - deps: BackgroundSessionDependencies - ) => { - if (!sessionId || !searchId) return; - const requestHash = createRequestHash(searchRequest.params); - - // If there is already a saved object for this session, update it to include this request/ID. - // Otherwise, just update the in-memory mapping for this session for when the session is saved. - if (isStored) { - const attributes = { idMapping: { [requestHash]: searchId } }; - await this.update(sessionId, attributes, deps); - } else { - const map = this.sessionSearchMap.get(sessionId) ?? new Map(); - map.set(requestHash, searchId); - this.sessionSearchMap.set(sessionId, map); - } - }; - - /** - * Look up an existing search ID that matches the given request in the given session so that the - * request can continue rather than restart. - * @internal - */ - public getId = async ( - searchRequest: IKibanaSearchRequest, - { sessionId, isStored, isRestore }: ISearchOptions, - deps: BackgroundSessionDependencies - ) => { - if (!sessionId) { - throw new Error('Session ID is required'); - } else if (!isStored) { - throw new Error('Cannot get search ID from a session that is not stored'); - } else if (!isRestore) { - throw new Error('Get search ID is only supported when restoring a session'); - } - - const session = await this.get(sessionId, deps); - const requestHash = createRequestHash(searchRequest.params); - if (!session.attributes.idMapping.hasOwnProperty(requestHash)) { - throw new Error('No search ID in this session matching the given search request'); - } - - return session.attributes.idMapping[requestHash]; - }; - - public asScopedProvider = ({ savedObjects }: CoreStart) => { - return (request: KibanaRequest) => { - const savedObjectsClient = savedObjects.getScopedClient(request, { - includedHiddenTypes: [BACKGROUND_SESSION_TYPE], - }); - const deps = { savedObjectsClient }; - return { - save: (sessionId: string, attributes: Partial) => - this.save(sessionId, attributes, deps), - get: (sessionId: string) => this.get(sessionId, deps), - find: (options: SearchSessionFindOptions) => this.find(options, deps), - update: (sessionId: string, attributes: Partial) => - this.update(sessionId, attributes, deps), - delete: (sessionId: string) => this.delete(sessionId, deps), - }; - }; - }; + } } diff --git a/src/plugins/data/common/search/session/types.ts b/src/plugins/data/server/search/session/types.ts similarity index 55% rename from src/plugins/data/common/search/session/types.ts rename to src/plugins/data/server/search/session/types.ts index 50ca3ca390ece..5e179b99952fe 100644 --- a/src/plugins/data/common/search/session/types.ts +++ b/src/plugins/data/server/search/session/types.ts @@ -17,28 +17,19 @@ * under the License. */ -export interface BackgroundSessionSavedObjectAttributes { - /** - * User-facing session name to be displayed in session management - */ - name: string; - /** - * App that created the session. e.g 'discover' - */ - appId: string; - created: string; - expires: string; - status: string; - urlGeneratorId: string; - initialState: Record; - restoreState: Record; - idMapping: Record; +import { Observable } from 'rxjs'; +import { CoreStart, KibanaRequest } from 'kibana/server'; +import { ISearchStrategy } from '../types'; +import { IKibanaSearchRequest, IKibanaSearchResponse } from '../../../common/search'; + +export interface IScopedSessionService { + search: ( + strategy: ISearchStrategy, + ...args: Parameters['search']> + ) => Observable; + [prop: string]: any; } -export interface SearchSessionFindOptions { - page?: number; - perPage?: number; - sortField?: string; - sortOrder?: string; - filter?: string; +export interface ISessionService { + asScopedProvider: (core: CoreStart) => (request: KibanaRequest) => IScopedSessionService; } diff --git a/src/plugins/data/server/search/session/utils.test.ts b/src/plugins/data/server/search/session/utils.test.ts deleted file mode 100644 index d190f892a7f84..0000000000000 --- a/src/plugins/data/server/search/session/utils.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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 { createRequestHash } from './utils'; - -describe('data/search/session utils', () => { - describe('createRequestHash', () => { - it('ignores `preference`', () => { - const request = { - foo: 'bar', - }; - - const withPreference = { - ...request, - preference: 1234, - }; - - expect(createRequestHash(request)).toEqual(createRequestHash(withPreference)); - }); - }); -}); diff --git a/src/plugins/data/server/search/session/utils.ts b/src/plugins/data/server/search/session/utils.ts deleted file mode 100644 index c3332f80b6e3f..0000000000000 --- a/src/plugins/data/server/search/session/utils.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 { createHash } from 'crypto'; - -/** - * Generate the hash for this request so that, in the future, this hash can be used to look up - * existing search IDs for this request. Ignores the `preference` parameter since it generally won't - * match from one request to another identical request. - */ -export function createRequestHash(keys: Record) { - const { preference, ...params } = keys; - return createHash(`sha256`).update(JSON.stringify(params)).digest('hex'); -} diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index ebce02014c2a4..db8b8ac72d0e5 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -34,9 +34,11 @@ import { import { AggsSetup, AggsStart } from './aggs'; import { SearchUsage } from './collectors'; import { IEsSearchRequest, IEsSearchResponse } from './es_search'; +import { ISessionService } from './session'; export interface SearchEnhancements { defaultStrategy: string; + sessionService: ISessionService; } export interface SearchStrategyDependencies { diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 86ec784834ace..cb71e5fa0460a 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -13,8 +13,8 @@ import { BfetchServerSetup } from 'src/plugins/bfetch/server'; import { ConfigDeprecationProvider } from '@kbn/config'; import { CoreSetup } from 'src/core/server'; import { CoreSetup as CoreSetup_2 } from 'kibana/server'; -import { CoreStart } from 'src/core/server'; -import { CoreStart as CoreStart_2 } from 'kibana/server'; +import { CoreStart } from 'kibana/server'; +import { CoreStart as CoreStart_2 } from 'src/core/server'; import { Datatable } from 'src/plugins/expressions'; import { Datatable as Datatable_2 } from 'src/plugins/expressions/common'; import { DatatableColumn } from 'src/plugins/expressions'; @@ -36,7 +36,8 @@ import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public'; import { ISearchSource } from 'src/plugins/data/public'; import { IUiSettingsClient } from 'src/core/server'; import { IUiSettingsClient as IUiSettingsClient_3 } from 'kibana/server'; -import { KibanaRequest } from 'src/core/server'; +import { KibanaRequest } from 'kibana/server'; +import { KibanaRequest as KibanaRequest_2 } from 'src/core/server'; import { LegacyAPICaller } from 'src/core/server'; import { Logger } from 'src/core/server'; import { Logger as Logger_2 } from 'kibana/server'; @@ -738,7 +739,7 @@ export class IndexPatternsService implements Plugin_3 Promise; }; } @@ -781,11 +782,11 @@ export interface ISearchStart ISearchClient; + asScoped: (request: KibanaRequest_2) => ISearchClient; getSearchStrategy: (name?: string) => ISearchStrategy; // (undocumented) searchSource: { - asScoped: (request: KibanaRequest) => Promise; + asScoped: (request: KibanaRequest_2) => Promise; }; } @@ -799,6 +800,16 @@ export interface ISearchStrategy Observable; } +// Warning: (ae-missing-release-tag) "ISessionService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface ISessionService { + // Warning: (ae-forgotten-export) The symbol "IScopedSessionService" needs to be exported by the entry point index.d.ts + // + // (undocumented) + asScopedProvider: (core: CoreStart) => (request: KibanaRequest) => IScopedSessionService; +} + // @public (undocumented) export enum KBN_FIELD_TYPES { // (undocumented) @@ -957,7 +968,7 @@ export class Plugin implements Plugin_2 Promise; }; @@ -1091,6 +1102,19 @@ export function searchUsageObserver(logger: Logger_2, usage?: SearchUsage): { error(): void; }; +// Warning: (ae-missing-release-tag) "SessionService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export class SessionService implements ISessionService { + constructor(); + // (undocumented) + asScopedProvider(core: CoreStart): (request: KibanaRequest) => { + search: , Response_1 extends IKibanaSearchResponse>(strategy: ISearchStrategy, request: Request_1, options: import("../../../common").ISearchOptions, deps: import("../types").SearchStrategyDependencies) => import("rxjs").Observable; + }; + // (undocumented) + search(strategy: ISearchStrategy, ...args: Parameters['search']>): import("rxjs").Observable; +} + // @internal export const shimAbortSignal: (promise: TransportRequestPromise, signal?: AbortSignal | undefined) => TransportRequestPromise; @@ -1236,23 +1260,23 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:111:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:137:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:137:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:248:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:248:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:248:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:248:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:250:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:251:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:260:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:261:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:262:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:266:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:267:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:274:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:275:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:250:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:250:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:250:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:250:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:252:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:253:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:262:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:263:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:264:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:268:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:269:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:273:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:276:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:277:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index_patterns/index_patterns_service.ts:58:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:90:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/search/types.ts:104:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/search/types.ts:106:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 534ab0f331e87..6099a2d5f73b4 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -73,6 +73,7 @@ import { SavedObjectAttributes } from 'kibana/server'; import { SavedObjectAttributes as SavedObjectAttributes_2 } from 'src/core/public'; import { SavedObjectAttributes as SavedObjectAttributes_3 } from 'kibana/public'; import { SavedObjectsClientContract as SavedObjectsClientContract_3 } from 'src/core/public'; +import { SavedObjectsFindOptions as SavedObjectsFindOptions_3 } from 'kibana/public'; import { SavedObjectsFindResponse as SavedObjectsFindResponse_2 } from 'kibana/server'; import { Search } from '@elastic/elasticsearch/api/requestParams'; import { SearchResponse } from 'elasticsearch'; diff --git a/x-pack/plugins/data_enhanced/common/index.ts b/x-pack/plugins/data_enhanced/common/index.ts index dd1a2d39ab5d1..7b14a723d7877 100644 --- a/x-pack/plugins/data_enhanced/common/index.ts +++ b/x-pack/plugins/data_enhanced/common/index.ts @@ -12,4 +12,7 @@ export { EqlSearchStrategyResponse, IAsyncSearchOptions, pollSearch, + BackgroundSessionSavedObjectAttributes, + BackgroundSessionFindOptions, + BackgroundSessionStatus, } from './search'; diff --git a/x-pack/plugins/data_enhanced/common/search/index.ts b/x-pack/plugins/data_enhanced/common/search/index.ts index 34bb21cb91af1..5617a1780c269 100644 --- a/x-pack/plugins/data_enhanced/common/search/index.ts +++ b/x-pack/plugins/data_enhanced/common/search/index.ts @@ -6,3 +6,4 @@ export * from './types'; export * from './poll_search'; +export * from './session'; diff --git a/x-pack/plugins/data_enhanced/common/search/session/index.ts b/x-pack/plugins/data_enhanced/common/search/session/index.ts new file mode 100644 index 0000000000000..ef7f3f1c7f2c4 --- /dev/null +++ b/x-pack/plugins/data_enhanced/common/search/session/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './status'; +export * from './types'; diff --git a/x-pack/plugins/data_enhanced/common/search/session/status.ts b/x-pack/plugins/data_enhanced/common/search/session/status.ts new file mode 100644 index 0000000000000..a83dd389e4f13 --- /dev/null +++ b/x-pack/plugins/data_enhanced/common/search/session/status.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum BackgroundSessionStatus { + IN_PROGRESS = 'in_progress', + ERROR = 'error', + COMPLETE = 'complete', + CANCELLED = 'cancelled', + EXPIRED = 'expired', +} diff --git a/x-pack/plugins/data_enhanced/common/search/session/types.ts b/x-pack/plugins/data_enhanced/common/search/session/types.ts new file mode 100644 index 0000000000000..0b82c9160ea1a --- /dev/null +++ b/x-pack/plugins/data_enhanced/common/search/session/types.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface BackgroundSessionSavedObjectAttributes { + /** + * User-facing session name to be displayed in session management + */ + name: string; + /** + * App that created the session. e.g 'discover' + */ + appId: string; + created: string; + expires: string; + status: string; + urlGeneratorId: string; + initialState: Record; + restoreState: Record; + idMapping: Record; +} + +export interface BackgroundSessionFindOptions { + page?: number; + perPage?: number; + sortField?: string; + sortOrder?: string; + filter?: string; +} diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts index ad21216bb7035..956568fbe7632 100644 --- a/x-pack/plugins/data_enhanced/server/plugin.ts +++ b/x-pack/plugins/data_enhanced/server/plugin.ts @@ -4,22 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - PluginInitializerContext, - CoreSetup, - CoreStart, - Plugin, - Logger, -} from '../../../../src/core/server'; +import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; import { PluginSetup as DataPluginSetup, PluginStart as DataPluginStart, usageProvider, } from '../../../../src/plugins/data/server'; -import { enhancedEsSearchStrategyProvider, eqlSearchStrategyProvider } from './search'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; -import { getUiSettings } from './ui_settings'; import { ENHANCED_ES_SEARCH_STRATEGY, EQL_SEARCH_STRATEGY } from '../common'; +import { registerSessionRoutes } from './routes'; +import { backgroundSessionMapping } from './saved_objects'; +import { + BackgroundSessionService, + enhancedEsSearchStrategyProvider, + eqlSearchStrategyProvider, +} from './search'; +import { getUiSettings } from './ui_settings'; interface SetupDependencies { data: DataPluginSetup; @@ -37,6 +37,7 @@ export class EnhancedDataServerPlugin implements Plugin { let mockCoreSetup: MockedKeys>; diff --git a/src/plugins/data/server/search/routes/session.ts b/x-pack/plugins/data_enhanced/server/routes/session.ts similarity index 85% rename from src/plugins/data/server/search/routes/session.ts rename to x-pack/plugins/data_enhanced/server/routes/session.ts index f7dfc776565e0..b6f1187f49781 100644 --- a/src/plugins/data/server/search/routes/session.ts +++ b/x-pack/plugins/data_enhanced/server/routes/session.ts @@ -1,20 +1,7 @@ /* - * 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. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ import { schema } from '@kbn/config-schema'; diff --git a/src/plugins/data/server/saved_objects/background_session.ts b/x-pack/plugins/data_enhanced/server/saved_objects/background_session.ts similarity index 51% rename from src/plugins/data/server/saved_objects/background_session.ts rename to x-pack/plugins/data_enhanced/server/saved_objects/background_session.ts index e81272628c091..337c5f8ad974f 100644 --- a/src/plugins/data/server/saved_objects/background_session.ts +++ b/x-pack/plugins/data_enhanced/server/saved_objects/background_session.ts @@ -1,20 +1,7 @@ /* - * 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. + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. */ import { SavedObjectsType } from 'kibana/server'; diff --git a/x-pack/plugins/data_enhanced/server/saved_objects/index.ts b/x-pack/plugins/data_enhanced/server/saved_objects/index.ts new file mode 100644 index 0000000000000..4e07fe7117eaa --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/saved_objects/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './background_session'; diff --git a/x-pack/plugins/data_enhanced/server/search/index.ts b/x-pack/plugins/data_enhanced/server/search/index.ts index 64a28cea358e5..67369ef829752 100644 --- a/x-pack/plugins/data_enhanced/server/search/index.ts +++ b/x-pack/plugins/data_enhanced/server/search/index.ts @@ -6,3 +6,4 @@ export { enhancedEsSearchStrategyProvider } from './es_search_strategy'; export { eqlSearchStrategyProvider } from './eql_search_strategy'; +export * from './session'; diff --git a/x-pack/plugins/data_enhanced/server/search/session/index.ts b/x-pack/plugins/data_enhanced/server/search/session/index.ts new file mode 100644 index 0000000000000..5b75885fb31df --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './session_service'; diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts new file mode 100644 index 0000000000000..95dcb0592e23d --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts @@ -0,0 +1,312 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { of } from 'rxjs'; +import type { SavedObject, SavedObjectsClientContract } from 'kibana/server'; +import type { SearchStrategyDependencies } from '../../../../../../src/plugins/data/server'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { BackgroundSessionStatus } from '../../../common'; +import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; +import { BackgroundSessionDependencies, BackgroundSessionService } from './session_service'; +import { createRequestHash } from './utils'; + +describe('BackgroundSessionService', () => { + let savedObjectsClient: jest.Mocked; + let service: BackgroundSessionService; + + const sessionId = 'd7170a35-7e2c-48d6-8dec-9a056721b489'; + const mockSavedObject: SavedObject = { + id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', + type: BACKGROUND_SESSION_TYPE, + attributes: { + name: 'my_name', + appId: 'my_app_id', + urlGeneratorId: 'my_url_generator_id', + idMapping: {}, + }, + references: [], + }; + + beforeEach(() => { + savedObjectsClient = savedObjectsClientMock.create(); + service = new BackgroundSessionService(); + }); + + it('search throws if `name` is not provided', () => { + expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot( + `[Error: Name is required]` + ); + }); + + it('save throws if `name` is not provided', () => { + expect(() => service.save(sessionId, {}, { savedObjectsClient })).rejects.toMatchInlineSnapshot( + `[Error: Name is required]` + ); + }); + + it('get calls saved objects client', async () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + + const response = await service.get(sessionId, { savedObjectsClient }); + + expect(response).toBe(mockSavedObject); + expect(savedObjectsClient.get).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); + }); + + it('find calls saved objects client', async () => { + const mockFindSavedObject = { + ...mockSavedObject, + score: 1, + }; + const mockResponse = { + saved_objects: [mockFindSavedObject], + total: 1, + per_page: 1, + page: 0, + }; + savedObjectsClient.find.mockResolvedValue(mockResponse); + + const options = { page: 0, perPage: 5 }; + const response = await service.find(options, { savedObjectsClient }); + + expect(response).toBe(mockResponse); + expect(savedObjectsClient.find).toHaveBeenCalledWith({ + ...options, + type: BACKGROUND_SESSION_TYPE, + }); + }); + + it('update calls saved objects client', async () => { + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + + const attributes = { name: 'new_name' }; + const response = await service.update(sessionId, attributes, { savedObjectsClient }); + + expect(response).toBe(mockUpdateSavedObject); + expect(savedObjectsClient.update).toHaveBeenCalledWith( + BACKGROUND_SESSION_TYPE, + sessionId, + attributes + ); + }); + + it('delete calls saved objects client', async () => { + savedObjectsClient.delete.mockResolvedValue({}); + + const response = await service.delete(sessionId, { savedObjectsClient }); + + expect(response).toEqual({}); + expect(savedObjectsClient.delete).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId); + }); + + describe('search', () => { + const mockSearch = jest.fn().mockReturnValue(of({})); + const mockStrategy = { search: mockSearch }; + const mockSearchDeps = {} as SearchStrategyDependencies; + const mockDeps = {} as BackgroundSessionDependencies; + + beforeEach(() => { + mockSearch.mockClear(); + }); + + it('searches using the original request if not restoring', async () => { + const searchRequest = { params: {} }; + const options = { sessionId, isStored: false, isRestore: false }; + + await service + .search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) + .toPromise(); + + expect(mockSearch).toBeCalledWith(searchRequest, options, mockSearchDeps); + }); + + it('searches using the original request if `id` is provided', async () => { + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + const searchRequest = { id: searchId, params: {} }; + const options = { sessionId, isStored: true, isRestore: true }; + + await service + .search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) + .toPromise(); + + expect(mockSearch).toBeCalledWith(searchRequest, options, mockSearchDeps); + }); + + it('searches by looking up an `id` if restoring and `id` is not provided', async () => { + const searchRequest = { params: {} }; + const options = { sessionId, isStored: true, isRestore: true }; + const spyGetId = jest.spyOn(service, 'getId').mockResolvedValueOnce('my_id'); + + await service + .search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) + .toPromise(); + + expect(mockSearch).toBeCalledWith({ ...searchRequest, id: 'my_id' }, options, mockSearchDeps); + + spyGetId.mockRestore(); + }); + + it('calls `trackId` once if the response contains an `id` and not restoring', async () => { + const searchRequest = { params: {} }; + const options = { sessionId, isStored: false, isRestore: false }; + const spyTrackId = jest.spyOn(service, 'trackId').mockResolvedValue(); + mockSearch.mockReturnValueOnce(of({ id: 'my_id' }, { id: 'my_id' })); + + await service + .search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) + .toPromise(); + + expect(spyTrackId).toBeCalledTimes(1); + expect(spyTrackId).toBeCalledWith(searchRequest, 'my_id', options, {}); + + spyTrackId.mockRestore(); + }); + + it('does not call `trackId` if restoring', async () => { + const searchRequest = { params: {} }; + const options = { sessionId, isStored: true, isRestore: true }; + const spyGetId = jest.spyOn(service, 'getId').mockResolvedValueOnce('my_id'); + const spyTrackId = jest.spyOn(service, 'trackId').mockResolvedValue(); + mockSearch.mockReturnValueOnce(of({ id: 'my_id' })); + + await service + .search(mockStrategy, searchRequest, options, mockSearchDeps, mockDeps) + .toPromise(); + + expect(spyTrackId).not.toBeCalled(); + + spyGetId.mockRestore(); + spyTrackId.mockRestore(); + }); + }); + + describe('trackId', () => { + it('stores hash in memory when `isStored` is `false` for when `save` is called', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + const isStored = false; + const name = 'my saved background search session'; + const appId = 'my_app_id'; + const urlGeneratorId = 'my_url_generator_id'; + const created = new Date().toISOString(); + const expires = new Date().toISOString(); + + await service.trackId( + searchRequest, + searchId, + { sessionId, isStored }, + { savedObjectsClient } + ); + + expect(savedObjectsClient.update).not.toHaveBeenCalled(); + + await service.save( + sessionId, + { name, created, expires, appId, urlGeneratorId }, + { savedObjectsClient } + ); + + expect(savedObjectsClient.create).toHaveBeenCalledWith( + BACKGROUND_SESSION_TYPE, + { + name, + created, + expires, + initialState: {}, + restoreState: {}, + status: BackgroundSessionStatus.IN_PROGRESS, + idMapping: { [requestHash]: searchId }, + appId, + urlGeneratorId, + }, + { id: sessionId } + ); + }); + + it('updates saved object when `isStored` is `true`', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + const isStored = true; + + await service.trackId( + searchRequest, + searchId, + { sessionId, isStored }, + { savedObjectsClient } + ); + + expect(savedObjectsClient.update).toHaveBeenCalledWith(BACKGROUND_SESSION_TYPE, sessionId, { + idMapping: { [requestHash]: searchId }, + }); + }); + }); + + describe('getId', () => { + it('throws if `sessionId` is not provided', () => { + const searchRequest = { params: {} }; + + expect(() => + service.getId(searchRequest, {}, { savedObjectsClient }) + ).rejects.toMatchInlineSnapshot(`[Error: Session ID is required]`); + }); + + it('throws if there is not a saved object', () => { + const searchRequest = { params: {} }; + + expect(() => + service.getId(searchRequest, { sessionId, isStored: false }, { savedObjectsClient }) + ).rejects.toMatchInlineSnapshot( + `[Error: Cannot get search ID from a session that is not stored]` + ); + }); + + it('throws if not restoring a saved session', () => { + const searchRequest = { params: {} }; + + expect(() => + service.getId( + searchRequest, + { sessionId, isStored: true, isRestore: false }, + { savedObjectsClient } + ) + ).rejects.toMatchInlineSnapshot( + `[Error: Get search ID is only supported when restoring a session]` + ); + }); + + it('returns the search ID from the saved object ID mapping', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + const mockSession = { + id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', + type: BACKGROUND_SESSION_TYPE, + attributes: { + name: 'my_name', + appId: 'my_app_id', + urlGeneratorId: 'my_url_generator_id', + idMapping: { [requestHash]: searchId }, + }, + references: [], + }; + savedObjectsClient.get.mockResolvedValue(mockSession); + + const id = await service.getId( + searchRequest, + { sessionId, isStored: true, isRestore: true }, + { savedObjectsClient } + ); + + expect(id).toBe(searchId); + }); + }); +}); diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts new file mode 100644 index 0000000000000..65a9e0901d738 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -0,0 +1,225 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreStart, KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; +import { from, Observable } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; +import { + IKibanaSearchRequest, + IKibanaSearchResponse, + ISearchOptions, + tapFirst, +} from '../../../../../../src/plugins/data/common'; +import { + ISearchStrategy, + ISessionService, + SearchStrategyDependencies, +} from '../../../../../../src/plugins/data/server'; +import { + BackgroundSessionSavedObjectAttributes, + BackgroundSessionFindOptions, + BackgroundSessionStatus, +} from '../../../common'; +import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; +import { createRequestHash } from './utils'; + +const DEFAULT_EXPIRATION = 7 * 24 * 60 * 60 * 1000; + +export interface BackgroundSessionDependencies { + savedObjectsClient: SavedObjectsClientContract; +} + +export class BackgroundSessionService implements ISessionService { + /** + * Map of sessionId to { [requestHash]: searchId } + * @private + */ + private sessionSearchMap = new Map>(); + + constructor() {} + + public search( + strategy: ISearchStrategy, + searchRequest: Request, + options: ISearchOptions, + searchDeps: SearchStrategyDependencies, + deps: BackgroundSessionDependencies + ): Observable { + // If this is a restored background search session, look up the ID using the provided sessionId + const getSearchRequest = async () => + !options.isRestore || searchRequest.id + ? searchRequest + : { + ...searchRequest, + id: await this.getId(searchRequest, options, deps), + }; + + return from(getSearchRequest()).pipe( + switchMap((request) => strategy.search(request, options, searchDeps)), + tapFirst((response) => { + if (searchRequest.id || !options.sessionId || !response.id || options.isRestore) return; + this.trackId(searchRequest, response.id, options, deps); + }) + ); + } + + // TODO: Generate the `userId` from the realm type/realm name/username + public save = async ( + sessionId: string, + { + name, + appId, + created = new Date().toISOString(), + expires = new Date(Date.now() + DEFAULT_EXPIRATION).toISOString(), + status = BackgroundSessionStatus.IN_PROGRESS, + urlGeneratorId, + initialState = {}, + restoreState = {}, + }: Partial, + { savedObjectsClient }: BackgroundSessionDependencies + ) => { + if (!name) throw new Error('Name is required'); + if (!appId) throw new Error('AppId is required'); + if (!urlGeneratorId) throw new Error('UrlGeneratorId is required'); + + // Get the mapping of request hash/search ID for this session + const searchMap = this.sessionSearchMap.get(sessionId) ?? new Map(); + const idMapping = Object.fromEntries(searchMap.entries()); + const attributes = { + name, + created, + expires, + status, + initialState, + restoreState, + idMapping, + urlGeneratorId, + appId, + }; + const session = await savedObjectsClient.create( + BACKGROUND_SESSION_TYPE, + attributes, + { id: sessionId } + ); + + // Clear out the entries for this session ID so they don't get saved next time + this.sessionSearchMap.delete(sessionId); + + return session; + }; + + // TODO: Throw an error if this session doesn't belong to this user + public get = (sessionId: string, { savedObjectsClient }: BackgroundSessionDependencies) => { + return savedObjectsClient.get( + BACKGROUND_SESSION_TYPE, + sessionId + ); + }; + + // TODO: Throw an error if this session doesn't belong to this user + public find = ( + options: BackgroundSessionFindOptions, + { savedObjectsClient }: BackgroundSessionDependencies + ) => { + return savedObjectsClient.find({ + ...options, + type: BACKGROUND_SESSION_TYPE, + }); + }; + + // TODO: Throw an error if this session doesn't belong to this user + public update = ( + sessionId: string, + attributes: Partial, + { savedObjectsClient }: BackgroundSessionDependencies + ) => { + return savedObjectsClient.update( + BACKGROUND_SESSION_TYPE, + sessionId, + attributes + ); + }; + + // TODO: Throw an error if this session doesn't belong to this user + public delete = (sessionId: string, { savedObjectsClient }: BackgroundSessionDependencies) => { + return savedObjectsClient.delete(BACKGROUND_SESSION_TYPE, sessionId); + }; + + /** + * Tracks the given search request/search ID in the saved session (if it exists). Otherwise, just + * store it in memory until a saved session exists. + * @internal + */ + public trackId = async ( + searchRequest: IKibanaSearchRequest, + searchId: string, + { sessionId, isStored }: ISearchOptions, + deps: BackgroundSessionDependencies + ) => { + if (!sessionId || !searchId) return; + const requestHash = createRequestHash(searchRequest.params); + + // If there is already a saved object for this session, update it to include this request/ID. + // Otherwise, just update the in-memory mapping for this session for when the session is saved. + if (isStored) { + const attributes = { idMapping: { [requestHash]: searchId } }; + await this.update(sessionId, attributes, deps); + } else { + const map = this.sessionSearchMap.get(sessionId) ?? new Map(); + map.set(requestHash, searchId); + this.sessionSearchMap.set(sessionId, map); + } + }; + + /** + * Look up an existing search ID that matches the given request in the given session so that the + * request can continue rather than restart. + * @internal + */ + public getId = async ( + searchRequest: IKibanaSearchRequest, + { sessionId, isStored, isRestore }: ISearchOptions, + deps: BackgroundSessionDependencies + ) => { + if (!sessionId) { + throw new Error('Session ID is required'); + } else if (!isStored) { + throw new Error('Cannot get search ID from a session that is not stored'); + } else if (!isRestore) { + throw new Error('Get search ID is only supported when restoring a session'); + } + + const session = await this.get(sessionId, deps); + const requestHash = createRequestHash(searchRequest.params); + if (!session.attributes.idMapping.hasOwnProperty(requestHash)) { + throw new Error('No search ID in this session matching the given search request'); + } + + return session.attributes.idMapping[requestHash]; + }; + + public asScopedProvider = ({ savedObjects }: CoreStart) => { + return (request: KibanaRequest) => { + const savedObjectsClient = savedObjects.getScopedClient(request, { + includedHiddenTypes: [BACKGROUND_SESSION_TYPE], + }); + const deps = { savedObjectsClient }; + return { + search: ( + strategy: ISearchStrategy, + ...args: Parameters['search']> + ) => this.search(strategy, ...args, deps), + save: (sessionId: string, attributes: Partial) => + this.save(sessionId, attributes, deps), + get: (sessionId: string) => this.get(sessionId, deps), + find: (options: BackgroundSessionFindOptions) => this.find(options, deps), + update: (sessionId: string, attributes: Partial) => + this.update(sessionId, attributes, deps), + delete: (sessionId: string) => this.delete(sessionId, deps), + }; + }; + }; +} diff --git a/x-pack/plugins/data_enhanced/server/search/session/utils.test.ts b/x-pack/plugins/data_enhanced/server/search/session/utils.test.ts new file mode 100644 index 0000000000000..aae1819a9c3e1 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/utils.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createRequestHash } from './utils'; + +describe('data/search/session utils', () => { + describe('createRequestHash', () => { + it('ignores `preference`', () => { + const request = { + foo: 'bar', + }; + + const withPreference = { + ...request, + preference: 1234, + }; + + expect(createRequestHash(request)).toEqual(createRequestHash(withPreference)); + }); + }); +}); diff --git a/x-pack/plugins/data_enhanced/server/search/session/utils.ts b/x-pack/plugins/data_enhanced/server/search/session/utils.ts new file mode 100644 index 0000000000000..1c314c64f9be3 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/search/session/utils.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createHash } from 'crypto'; + +/** + * Generate the hash for this request so that, in the future, this hash can be used to look up + * existing search IDs for this request. Ignores the `preference` parameter since it generally won't + * match from one request to another identical request. + */ +export function createRequestHash(keys: Record) { + const { preference, ...params } = keys; + return createHash(`sha256`).update(JSON.stringify(params)).digest('hex'); +} From 93128a1d8bf19c69f53c68e205643795aa1f80cd Mon Sep 17 00:00:00 2001 From: Liza K Date: Thu, 3 Dec 2020 21:34:05 +0200 Subject: [PATCH 14/26] Better logs Save IDs only in monitoring loop --- .../server/search/session/session_service.ts | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/plugins/data/server/search/session/session_service.ts b/src/plugins/data/server/search/session/session_service.ts index a70a6dc0780aa..92defc89b41b8 100644 --- a/src/plugins/data/server/search/session/session_service.ts +++ b/src/plugins/data/server/search/session/session_service.ts @@ -69,7 +69,7 @@ export class BackgroundSessionService { private setupMonitoring = (savedObjects: SavedObjectsServiceStart) => { // TODO: setup monitoring only if BGS is enabled - const internalRepo = savedObjects.createInternalRepository(); + const internalRepo = savedObjects.createInternalRepository([BACKGROUND_SESSION_TYPE]); this.internalSavedObjectsClient = new SavedObjectsClient(internalRepo); this.monitorInterval = setInterval(this.monitorMappedIds.bind(this), INMEM_TRACKING_INTERVAL); }; @@ -102,10 +102,10 @@ export class BackgroundSessionService { moment.duration(curTime.diff(sessionInfo.insertTime)).asSeconds() > INMEM_TRACKING_TIMEOUT_SEC ) { - this.logger.debug(`Deleting expired session ${sessionId}`); + this.logger.debug(`clearSessions | Deleting expired session ${sessionId}`); this.sessionSearchMap.delete(sessionId); } else if (sessionInfo.retryCount >= MAX_UPDATE_RETRIES) { - this.logger.warn(`Deleting failed session ${sessionId}`); + this.logger.warn(`clearSessions | Deleting failed session ${sessionId}`); this.sessionSearchMap.delete(sessionId); } }); @@ -113,7 +113,7 @@ export class BackgroundSessionService { private async monitorMappedIds() { try { - this.logger.debug(`monitorMappedIds. Map contains ${this.sessionSearchMap.size} items`); + this.logger.debug(`monitorMappedIds | Map contains ${this.sessionSearchMap.size} items`); this.clearSessions(); if (!this.sessionSearchMap.size) return; @@ -135,7 +135,7 @@ export class BackgroundSessionService { } }); } catch (e) { - this.logger.error(`Error fetching sessions. ${e}`); + this.logger.error(`monitorMappedIds | Error fetching sessions. ${e}`); } } @@ -144,6 +144,7 @@ export class BackgroundSessionService { ) { if (!activeMappingObjects.length) return []; + this.logger.debug(`updateAllSavedObjects | Updating ${activeMappingObjects.length} items`); const updatedSessions = activeMappingObjects ?.filter((so) => !so.error) .map((sessionSavedObject) => { @@ -195,9 +196,8 @@ export class BackgroundSessionService { if (!appId) throw new Error('AppId is required'); if (!urlGeneratorId) throw new Error('UrlGeneratorId is required'); - // Get the mapping of request hash/search ID for this session - const searchMap = this.sessionSearchMap.get(sessionId); - const idMapping = searchMap ? Object.fromEntries(searchMap.ids.entries()) : {}; + this.logger.debug(`save. Saving ${sessionId}.`); + const attributes = { name, created, @@ -205,7 +205,7 @@ export class BackgroundSessionService { status, initialState, restoreState, - idMapping, + idMapping: {}, urlGeneratorId, appId, }; @@ -215,9 +215,6 @@ export class BackgroundSessionService { { id: sessionId } ); - // Clear out the entries for this session ID so they don't get saved next time - this.sessionSearchMap.delete(sessionId); - return session; }; @@ -270,6 +267,7 @@ export class BackgroundSessionService { deps: BackgroundSessionDependencies ) => { if (!sessionId || !searchId) return; + this.logger.debug(`trackId | Tracking ${sessionId} sid ${searchId}`); const requestHash = createRequestHash(searchRequest.params); // If there is already a saved object for this session, update it to include this request/ID. From ea14d295cec2c85dbf2a83355327aff05209a538 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Thu, 3 Dec 2020 14:23:28 -0700 Subject: [PATCH 15/26] Fix types --- src/plugins/data/server/search/mocks.ts | 15 ----------- .../data_enhanced/server/routes/mocks.ts | 26 +++++++++++++++++++ .../server/routes/session.test.ts | 6 ++--- 3 files changed, 29 insertions(+), 18 deletions(-) create mode 100644 x-pack/plugins/data_enhanced/server/routes/mocks.ts diff --git a/src/plugins/data/server/search/mocks.ts b/src/plugins/data/server/search/mocks.ts index 6d214008e8b17..4914726c85ef8 100644 --- a/src/plugins/data/server/search/mocks.ts +++ b/src/plugins/data/server/search/mocks.ts @@ -17,8 +17,6 @@ * under the License. */ -import type { RequestHandlerContext } from 'src/core/server'; -import { coreMock } from '../../../../core/server/mocks'; import { ISearchSetup, ISearchStart } from './types'; import { searchAggsSetupMock, searchAggsStartMock } from './aggs/mocks'; import { searchSourceMock } from './search_source/mocks'; @@ -42,16 +40,3 @@ export function createSearchStartMock(): jest.Mocked { searchSource: searchSourceMock.createStartContract(), }; } - -export function createSearchRequestHandlerContext(): jest.Mocked { - return { - core: coreMock.createRequestHandlerContext(), - search: { - search: jest.fn(), - cancel: jest.fn(), - session: { - search: jest.fn(), - }, - }, - }; -} diff --git a/x-pack/plugins/data_enhanced/server/routes/mocks.ts b/x-pack/plugins/data_enhanced/server/routes/mocks.ts new file mode 100644 index 0000000000000..3e7b89ed2cca6 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/routes/mocks.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RequestHandlerContext } from 'kibana/server'; +import { coreMock } from '../../../../../src/core/server/mocks'; + +export function createSearchRequestHandlerContext() { + return ({ + core: coreMock.createRequestHandlerContext(), + search: { + search: jest.fn(), + cancel: jest.fn(), + session: { + search: jest.fn(), + save: jest.fn(), + get: jest.fn(), + find: jest.fn(), + delete: jest.fn(), + update: jest.fn(), + }, + }, + } as unknown) as jest.Mocked; +} diff --git a/x-pack/plugins/data_enhanced/server/routes/session.test.ts b/x-pack/plugins/data_enhanced/server/routes/session.test.ts index 7342e5cda4e0c..313dfb1e0f1f0 100644 --- a/x-pack/plugins/data_enhanced/server/routes/session.test.ts +++ b/x-pack/plugins/data_enhanced/server/routes/session.test.ts @@ -6,10 +6,10 @@ import type { MockedKeys } from '@kbn/utility-types/jest'; import type { CoreSetup, RequestHandlerContext } from 'kibana/server'; -import { coreMock, httpServerMock } from 'src/core/server/mocks'; -import { registerSessionRoutes } from './session'; +import { coreMock, httpServerMock } from '../../../../../src/core/server/mocks'; import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server'; -import { createSearchRequestHandlerContext } from '../../../../../src/plugins/data/server/search/mocks'; +import { createSearchRequestHandlerContext } from './mocks'; +import { registerSessionRoutes } from './session'; describe('registerSessionRoutes', () => { let mockCoreSetup: MockedKeys>; From a0e2142557274b281a807c43cdf4c63096a09095 Mon Sep 17 00:00:00 2001 From: Liza K Date: Sun, 6 Dec 2020 13:15:40 +0200 Subject: [PATCH 16/26] Space aware session service --- .../saved_objects/background_session.ts | 3 + .../search/session/session_service.test.ts | 59 +++++++++++-------- .../server/search/session/session_service.ts | 27 +++++---- 3 files changed, 56 insertions(+), 33 deletions(-) diff --git a/src/plugins/data/server/saved_objects/background_session.ts b/src/plugins/data/server/saved_objects/background_session.ts index e81272628c091..82c3d754f7f71 100644 --- a/src/plugins/data/server/saved_objects/background_session.ts +++ b/src/plugins/data/server/saved_objects/background_session.ts @@ -27,6 +27,9 @@ export const backgroundSessionMapping: SavedObjectsType = { hidden: true, mappings: { properties: { + sessionId: { + type: 'keyword', + }, name: { type: 'keyword', }, diff --git a/src/plugins/data/server/search/session/session_service.test.ts b/src/plugins/data/server/search/session/session_service.test.ts index 064d37bcaf2f5..1d8e963a7c0da 100644 --- a/src/plugins/data/server/search/session/session_service.test.ts +++ b/src/plugins/data/server/search/session/session_service.test.ts @@ -41,13 +41,13 @@ describe('BackgroundSessionService', () => { const MOCK_KEY_HASH = '608de49a4600dbb5b173492759792e4a'; const createMockInternalSavedObjectClient = ( - bulkGetSpy?: jest.SpyInstance, + findSpy?: jest.SpyInstance, bulkUpdateSpy?: jest.SpyInstance ) => { Object.defineProperty(service, 'internalSavedObjectsClient', { get: () => { - const bulkGet = - bulkGetSpy || + const find = + findSpy || (() => { return { saved_objects: [ @@ -73,7 +73,7 @@ describe('BackgroundSessionService', () => { }; }); return { - bulkGet, + find, bulkUpdate, }; }, @@ -199,6 +199,11 @@ describe('BackgroundSessionService', () => { const created = new Date().toISOString(); const expires = new Date().toISOString(); + const mockIdMapping = createMockIdMapping([]); + Object.defineProperty(service, 'sessionSearchMap', { + get: () => mockIdMapping, + }); + await service.trackId( searchRequest, searchId, @@ -223,12 +228,15 @@ describe('BackgroundSessionService', () => { initialState: {}, restoreState: {}, status: BackgroundSessionStatus.IN_PROGRESS, - idMapping: { [requestHash]: searchId }, + idMapping: {}, appId, urlGeneratorId, + sessionId, }, { id: sessionId } ); + + expect(mockIdMapping.get(sessionId)!.ids.get(requestHash)).toBe(searchId); }); it('updates saved object when `isStored` is `true`', async () => { @@ -368,7 +376,7 @@ describe('BackgroundSessionService', () => { expect(deleteSpy).toHaveBeenCalledTimes(1); }); - it('should bot fetch when no IDs are mapped', async () => { + it('should not fetch when no IDs are mapped', async () => { const bulkGetSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [] }); createMockInternalSavedObjectClient(bulkGetSpy); @@ -382,24 +390,26 @@ describe('BackgroundSessionService', () => { get: () => mockIdMapping, }); - const bulkGetSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [] }); + const findSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [] }); const bulkUpdateSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [] }); - createMockInternalSavedObjectClient(bulkGetSpy, bulkUpdateSpy); + createMockInternalSavedObjectClient(findSpy, bulkUpdateSpy); jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL); - expect(bulkGetSpy).toHaveBeenCalledTimes(1); + expect(findSpy).toHaveBeenCalledTimes(1); expect(bulkUpdateSpy).not.toHaveBeenCalled(); }); - it('should update saved objects if they are found, and delete ids on success', async () => { + it('should update saved objects if they are found, and delete session on success', async () => { const mockIdMapping = createMockIdMapping([[MOCK_KEY_HASH, MOCK_ASYNC_ID]], undefined, 1); const mockMapDeleteSpy = jest.fn(); - mockIdMapping.get(MOCK_SESSION_ID)!.ids.delete = mockMapDeleteSpy; + const mockSessionDeleteSpy = jest.fn(); + mockIdMapping.delete = mockMapDeleteSpy; + mockIdMapping.get(MOCK_SESSION_ID)!.ids.delete = mockSessionDeleteSpy; Object.defineProperty(service, 'sessionSearchMap', { get: () => mockIdMapping, }); - const bulkGetSpy = jest.fn().mockResolvedValueOnce({ + const findSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [ { id: MOCK_SESSION_ID, @@ -424,7 +434,7 @@ describe('BackgroundSessionService', () => { }, ], }); - createMockInternalSavedObjectClient(bulkGetSpy, bulkUpdateSpy); + createMockInternalSavedObjectClient(findSpy, bulkUpdateSpy); jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL); @@ -432,23 +442,25 @@ describe('BackgroundSessionService', () => { jest.useRealTimers(); await new Promise((r) => setTimeout(r, 15)); - expect(bulkGetSpy).toHaveBeenCalledTimes(1); + expect(findSpy).toHaveBeenCalledTimes(1); expect(bulkUpdateSpy).toHaveBeenCalledTimes(1); - expect(mockMapDeleteSpy).toHaveBeenCalledTimes(2); - expect(mockMapDeleteSpy).toBeCalledWith('b'); - expect(mockMapDeleteSpy).toBeCalledWith(MOCK_KEY_HASH); - expect(mockIdMapping.get(MOCK_SESSION_ID)?.retryCount).toBe(0); + expect(mockSessionDeleteSpy).toHaveBeenCalledTimes(2); + expect(mockSessionDeleteSpy).toBeCalledWith('b'); + expect(mockSessionDeleteSpy).toBeCalledWith(MOCK_KEY_HASH); + expect(mockMapDeleteSpy).toBeCalledTimes(1); }); it('should update saved objects if they are found, and increase retryCount on error', async () => { const mockIdMapping = createMockIdMapping([[MOCK_KEY_HASH, MOCK_ASYNC_ID]]); const mockMapDeleteSpy = jest.fn(); - mockIdMapping.get(MOCK_SESSION_ID)!.ids.delete = mockMapDeleteSpy; + const mockSessionDeleteSpy = jest.fn(); + mockIdMapping.delete = mockMapDeleteSpy; + mockIdMapping.get(MOCK_SESSION_ID)!.ids.delete = mockSessionDeleteSpy; Object.defineProperty(service, 'sessionSearchMap', { get: () => mockIdMapping, }); - const bulkGetSpy = jest.fn().mockResolvedValueOnce({ + const findSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [ { id: MOCK_SESSION_ID, @@ -468,7 +480,7 @@ describe('BackgroundSessionService', () => { }, ], }); - createMockInternalSavedObjectClient(bulkGetSpy, bulkUpdateSpy); + createMockInternalSavedObjectClient(findSpy, bulkUpdateSpy); jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL); @@ -476,10 +488,11 @@ describe('BackgroundSessionService', () => { jest.useRealTimers(); await new Promise((r) => setTimeout(r, 15)); - expect(bulkGetSpy).toHaveBeenCalledTimes(1); + expect(findSpy).toHaveBeenCalledTimes(1); expect(bulkUpdateSpy).toHaveBeenCalledTimes(1); + expect(mockSessionDeleteSpy).not.toHaveBeenCalled(); expect(mockMapDeleteSpy).not.toHaveBeenCalled(); - expect(mockIdMapping.get(MOCK_SESSION_ID)?.retryCount).toBe(1); + expect(mockIdMapping.get(MOCK_SESSION_ID)!.retryCount).toBe(1); }); }); }); diff --git a/src/plugins/data/server/search/session/session_service.ts b/src/plugins/data/server/search/session/session_service.ts index 92defc89b41b8..27db6a4a1c2b5 100644 --- a/src/plugins/data/server/search/session/session_service.ts +++ b/src/plugins/data/server/search/session/session_service.ts @@ -83,15 +83,16 @@ export class BackgroundSessionService { * context of a user's session. */ private async getAllMappedSavedObjects() { - const activeMappingIds = Array.from(this.sessionSearchMap.keys()).map((sessionId) => { - return { - id: sessionId, - type: BACKGROUND_SESSION_TYPE, - }; + const activeMappingIds = Array.from(this.sessionSearchMap.keys()) + .map((sessionId) => `"${sessionId}"`) + .join(' | '); + const res = await this.internalSavedObjectsClient.find({ + type: BACKGROUND_SESSION_TYPE, + search: activeMappingIds, + searchFields: ['sessionId'], + namespaces: ['*'], }); - const res = await this.internalSavedObjectsClient.bulkGet( - activeMappingIds - ); + this.logger.debug(`getAllMappedSavedObjects | Got ${res.saved_objects.length} items`); return res.saved_objects; } @@ -127,11 +128,16 @@ export class BackgroundSessionService { // Retry next time sessionInfo.retryCount++; } else if (updatedSavedObject.attributes.idMapping) { - // Delete the ids that we just saved, avoiding a potential new ids being lost (?) + // Delete the ids that we just saved, avoiding a potential new ids being lost. Object.keys(updatedSavedObject.attributes.idMapping).forEach((key) => { sessionInfo.ids.delete(key); }); - sessionInfo.retryCount = 0; + // If the session object is empty, delete it as well + if (!sessionInfo.ids.entries.length) { + this.sessionSearchMap.delete(updatedSavedObject.id); + } else { + sessionInfo.retryCount = 0; + } } }); } catch (e) { @@ -199,6 +205,7 @@ export class BackgroundSessionService { this.logger.debug(`save. Saving ${sessionId}.`); const attributes = { + sessionId, name, created, expires, From 149d94e6628362936f181d5468cc6c013cb94cdd Mon Sep 17 00:00:00 2001 From: Liza K Date: Sun, 6 Dec 2020 17:11:07 +0200 Subject: [PATCH 17/26] ts --- src/plugins/kibana_usage_collection/server/plugin.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plugins/kibana_usage_collection/server/plugin.ts b/src/plugins/kibana_usage_collection/server/plugin.ts index dbd6aaa566ff9..94e13b9a43cc8 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.ts @@ -46,7 +46,6 @@ import { registerUiCounterSavedObjectType, registerUiCountersRollups, } from './collectors'; -import { ConfigSchema } from '../config'; interface KibanaUsageCollectionPluginsDepsSetup { usageCollection: UsageCollectionSetup; From 0b875ac08efc49a76c013555e13398d342bbe916 Mon Sep 17 00:00:00 2001 From: Liza K Date: Mon, 7 Dec 2020 14:22:49 +0200 Subject: [PATCH 18/26] Fix session service saving --- .../server/search/session/session_service.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index 20bf76551f094..e2869f2bdaf08 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -219,6 +219,8 @@ export class BackgroundSessionService implements ISessionService { if (!appId) throw new Error('AppId is required'); if (!urlGeneratorId) throw new Error('UrlGeneratorId is required'); + this.logger.debug(`save | ${sessionId}`); + const attributes = { name, created, @@ -237,14 +239,12 @@ export class BackgroundSessionService implements ISessionService { { id: sessionId } ); - // Clear out the entries for this session ID so they don't get saved next time - this.sessionSearchMap.delete(sessionId); - return session; }; // TODO: Throw an error if this session doesn't belong to this user public get = (sessionId: string, { savedObjectsClient }: BackgroundSessionDependencies) => { + this.logger.debug(`get | ${sessionId}`); return savedObjectsClient.get( BACKGROUND_SESSION_TYPE, sessionId @@ -268,6 +268,7 @@ export class BackgroundSessionService implements ISessionService { attributes: Partial, { savedObjectsClient }: BackgroundSessionDependencies ) => { + this.logger.debug(`update | ${sessionId}`); return savedObjectsClient.update( BACKGROUND_SESSION_TYPE, sessionId, @@ -292,6 +293,7 @@ export class BackgroundSessionService implements ISessionService { deps: BackgroundSessionDependencies ) => { if (!sessionId || !searchId) return; + this.logger.debug(`trackId | ${sessionId} | ${searchId}`); const requestHash = createRequestHash(searchRequest.params); // If there is already a saved object for this session, update it to include this request/ID. From b5b089530db9a17f100bc17f70072ac822583005 Mon Sep 17 00:00:00 2001 From: Liza K Date: Mon, 7 Dec 2020 20:13:00 +0200 Subject: [PATCH 19/26] merge fix --- .../data_enhanced/server/search/session/session_service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index a5a7bbf14046c..98f19d3557de3 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -222,8 +222,6 @@ export class BackgroundSessionService implements ISessionService { this.logger.debug(`save | ${sessionId}`); // Get the mapping of request hash/search ID for this session - const searchMap = this.sessionSearchMap.get(sessionId) ?? new Map(); - const idMapping = Object.fromEntries(searchMap.entries()); const attributes = { name, created, From f14f042e42d695d5e0b484de06cf96a38ff3c88f Mon Sep 17 00:00:00 2001 From: Liza K Date: Mon, 7 Dec 2020 20:16:02 +0200 Subject: [PATCH 20/26] stable stringify --- x-pack/plugins/data_enhanced/server/search/session/utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/data_enhanced/server/search/session/utils.ts b/x-pack/plugins/data_enhanced/server/search/session/utils.ts index 1c314c64f9be3..beaecc5a839d3 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/utils.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/utils.ts @@ -5,6 +5,7 @@ */ import { createHash } from 'crypto'; +import stringify from 'json-stable-stringify'; /** * Generate the hash for this request so that, in the future, this hash can be used to look up @@ -13,5 +14,5 @@ import { createHash } from 'crypto'; */ export function createRequestHash(keys: Record) { const { preference, ...params } = keys; - return createHash(`sha256`).update(JSON.stringify(params)).digest('hex'); + return createHash(`sha256`).update(stringify(params)).digest('hex'); } From 5b9782d444bc55531c27da73879fea85a87daefb Mon Sep 17 00:00:00 2001 From: Liza K Date: Mon, 7 Dec 2020 20:34:30 +0200 Subject: [PATCH 21/26] INMEM_MAX_SESSIONS --- .../server/search/session/session_service.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index 98f19d3557de3..b7aaca8adfbfd 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -5,6 +5,7 @@ */ import moment, { Moment } from 'moment'; +import { orderBy } from 'lodash'; import { from, Observable } from 'rxjs'; import { first, switchMap } from 'rxjs/operators'; import { @@ -35,6 +36,7 @@ import { BACKGROUND_SESSION_TYPE } from '../../saved_objects'; import { createRequestHash } from './utils'; import { ConfigSchema } from '../../../config'; +const INMEM_MAX_SESSIONS = 10000; const DEFAULT_EXPIRATION = 7 * 24 * 60 * 60 * 1000; export const INMEM_TRACKING_INTERVAL = 10 * 1000; export const INMEM_TRACKING_TIMEOUT_SEC = 60; @@ -93,6 +95,7 @@ export class BackgroundSessionService implements ISessionService { .map((sessionId) => `"${sessionId}"`) .join(' | '); const res = await this.internalSavedObjectsClient.find({ + perPage: INMEM_MAX_SESSIONS, type: BACKGROUND_SESSION_TYPE, search: activeMappingIds, searchFields: ['sessionId'], @@ -104,6 +107,27 @@ export class BackgroundSessionService implements ISessionService { private clearSessions = () => { const curTime = moment(); + + // Drop old items if map size exceeds max. + if (this.sessionSearchMap.size > INMEM_MAX_SESSIONS) { + const sortedSessionIds = orderBy( + Array.from(this.sessionSearchMap.keys()).map((sessionId) => { + return { + sessionId, + insertTime: this.sessionSearchMap.get(sessionId)!.insertTime, + }; + }), + ['insertTime'], + ['asc'] + ); + + while (this.sessionSearchMap.size > INMEM_MAX_SESSIONS) { + const { sessionId } = sortedSessionIds.shift()!; + this.logger.warn(`clearSessions | Map full | Dropping ${sessionId}`); + this.sessionSearchMap.delete(sessionId); + } + } + this.sessionSearchMap.forEach((sessionInfo, sessionId) => { if ( moment.duration(curTime.diff(sessionInfo.insertTime)).asSeconds() > From 0858b98deefaecb460392d76f8d84c3af0c306ce Mon Sep 17 00:00:00 2001 From: Liza K Date: Mon, 7 Dec 2020 20:36:03 +0200 Subject: [PATCH 22/26] INMEM_MAX_SESSIONS --- .../server/search/session/session_service.ts | 23 +------------------ 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index b7aaca8adfbfd..8ec41bd703d97 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -5,7 +5,6 @@ */ import moment, { Moment } from 'moment'; -import { orderBy } from 'lodash'; import { from, Observable } from 'rxjs'; import { first, switchMap } from 'rxjs/operators'; import { @@ -95,7 +94,7 @@ export class BackgroundSessionService implements ISessionService { .map((sessionId) => `"${sessionId}"`) .join(' | '); const res = await this.internalSavedObjectsClient.find({ - perPage: INMEM_MAX_SESSIONS, + perPage: INMEM_MAX_SESSIONS, // If there are more sessions in memory, they will be synced when some items are cleared out. type: BACKGROUND_SESSION_TYPE, search: activeMappingIds, searchFields: ['sessionId'], @@ -108,26 +107,6 @@ export class BackgroundSessionService implements ISessionService { private clearSessions = () => { const curTime = moment(); - // Drop old items if map size exceeds max. - if (this.sessionSearchMap.size > INMEM_MAX_SESSIONS) { - const sortedSessionIds = orderBy( - Array.from(this.sessionSearchMap.keys()).map((sessionId) => { - return { - sessionId, - insertTime: this.sessionSearchMap.get(sessionId)!.insertTime, - }; - }), - ['insertTime'], - ['asc'] - ); - - while (this.sessionSearchMap.size > INMEM_MAX_SESSIONS) { - const { sessionId } = sortedSessionIds.shift()!; - this.logger.warn(`clearSessions | Map full | Dropping ${sessionId}`); - this.sessionSearchMap.delete(sessionId); - } - } - this.sessionSearchMap.forEach((sessionInfo, sessionId) => { if ( moment.duration(curTime.diff(sessionInfo.insertTime)).asSeconds() > From 4876f3ded6cdb6f180315d36e87d4db59e37f1b4 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Tue, 8 Dec 2020 16:28:52 +0200 Subject: [PATCH 23/26] Update x-pack/plugins/data_enhanced/server/search/session/session_service.ts Co-authored-by: Anton Dosov --- .../data_enhanced/server/search/session/session_service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index f574d970a3334..9aadc87d1aa59 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -84,7 +84,7 @@ export class BackgroundSessionService implements ISessionService { /** * Gets all {@link SessionSavedObjectAttributes | Background Searches} that * currently being tracked by the service. - * * + * * @remarks * Uses `internalSavedObjectsClient` as this is called asynchronously, not within the * context of a user's session. From ffabaa9b114359d30fbd549b8118f7eb8b638a6a Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Tue, 8 Dec 2020 19:02:38 +0200 Subject: [PATCH 24/26] Update x-pack/plugins/data_enhanced/server/search/session/session_service.ts Co-authored-by: Anton Dosov --- .../data_enhanced/server/search/session/session_service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index 9aadc87d1aa59..9975c90318487 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -161,7 +161,7 @@ export class BackgroundSessionService implements ISessionService { this.logger.debug(`updateAllSavedObjects | Updating ${activeMappingObjects.length} items`); const updatedSessions = activeMappingObjects - ?.filter((so) => !so.error) + .filter((so) => !so.error) .map((sessionSavedObject) => { const sessionInfo = this.sessionSearchMap.get(sessionSavedObject.id); const idMapping = sessionInfo ? Object.fromEntries(sessionInfo.ids.entries()) : {}; From 63c7d6a7f0286b37041116956ed89f6cf7425812 Mon Sep 17 00:00:00 2001 From: Liza K Date: Tue, 8 Dec 2020 20:13:53 +0200 Subject: [PATCH 25/26] Use setTimeout to schedule monitoring steps --- .../search/session/session_service.test.ts | 36 ++++++++++++++----- .../server/search/session/session_service.ts | 13 ++++--- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts index 4a518ca259f1d..766de908353f5 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts @@ -415,9 +415,27 @@ describe('BackgroundSessionService', () => { service.stop(); }); + it('schedules the next iteration', async () => { + const findSpy = jest.fn().mockResolvedValue({ saved_objects: [] }); + createMockInternalSavedObjectClient(findSpy); + + const mockIdMapping = createMockIdMapping([[MOCK_KEY_HASH, MOCK_ASYNC_ID]], moment()); + + Object.defineProperty(service, 'sessionSearchMap', { + get: () => mockIdMapping, + }); + + jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL); + expect(findSpy).toHaveBeenCalledTimes(1); + await flushPromises(); + + jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL); + expect(findSpy).toHaveBeenCalledTimes(2); + }); + it('should delete expired IDs', async () => { - const bulkGetSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [] }); - createMockInternalSavedObjectClient(bulkGetSpy); + const findSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [] }); + createMockInternalSavedObjectClient(findSpy); const mockIdMapping = createMockIdMapping( [[MOCK_KEY_HASH, MOCK_ASYNC_ID]], @@ -432,13 +450,13 @@ describe('BackgroundSessionService', () => { // Get setInterval to fire jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL); - expect(bulkGetSpy).not.toHaveBeenCalled(); + expect(findSpy).not.toHaveBeenCalled(); expect(deleteSpy).toHaveBeenCalledTimes(1); }); it('should delete IDs that passed max retries', async () => { - const bulkGetSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [] }); - createMockInternalSavedObjectClient(bulkGetSpy); + const findSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [] }); + createMockInternalSavedObjectClient(findSpy); const mockIdMapping = createMockIdMapping( [[MOCK_KEY_HASH, MOCK_ASYNC_ID]], @@ -454,16 +472,16 @@ describe('BackgroundSessionService', () => { // Get setInterval to fire jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL); - expect(bulkGetSpy).not.toHaveBeenCalled(); + expect(findSpy).not.toHaveBeenCalled(); expect(deleteSpy).toHaveBeenCalledTimes(1); }); it('should not fetch when no IDs are mapped', async () => { - const bulkGetSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [] }); - createMockInternalSavedObjectClient(bulkGetSpy); + const findSpy = jest.fn().mockResolvedValueOnce({ saved_objects: [] }); + createMockInternalSavedObjectClient(findSpy); jest.advanceTimersByTime(INMEM_TRACKING_INTERVAL); - expect(bulkGetSpy).not.toHaveBeenCalled(); + expect(findSpy).not.toHaveBeenCalled(); }); it('should try to fetch saved objects if some ids are mapped', async () => { diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index 9975c90318487..c9c6f17ef8b05 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -58,7 +58,7 @@ export class BackgroundSessionService implements ISessionService { */ private sessionSearchMap = new Map(); private internalSavedObjectsClient!: SavedObjectsClientContract; - private monitorInterval!: NodeJS.Timeout; + private monitorTimer!: NodeJS.Timeout; constructor(private readonly logger: Logger) {} @@ -68,7 +68,7 @@ export class BackgroundSessionService implements ISessionService { public stop() { this.sessionSearchMap.clear(); - clearInterval(this.monitorInterval); + clearTimeout(this.monitorTimer); } private setupMonitoring = async (core: CoreStart, config$: Observable) => { @@ -77,7 +77,7 @@ export class BackgroundSessionService implements ISessionService { this.logger.debug(`setupMonitoring | Enabling monitoring`); const internalRepo = core.savedObjects.createInternalRepository([BACKGROUND_SESSION_TYPE]); this.internalSavedObjectsClient = new SavedObjectsClient(internalRepo); - this.monitorInterval = setInterval(this.monitorMappedIds.bind(this), INMEM_TRACKING_INTERVAL); + this.monitorTimer = setTimeout(this.monitorMappedIds.bind(this), INMEM_TRACKING_INTERVAL); } }; @@ -121,7 +121,7 @@ export class BackgroundSessionService implements ISessionService { }); }; - public async monitorMappedIds() { + private async monitorMappedIds() { try { this.clearSessions(); @@ -150,8 +150,11 @@ export class BackgroundSessionService implements ISessionService { } }); } catch (e) { - this.logger.error(`monitorMappedIds | Error fetching sessions. ${e}`); + this.logger.error(`monitorMappedIds | Error while updating sessions. ${e}`); } + + // Schedule the next iteration + this.monitorTimer = setTimeout(this.monitorMappedIds.bind(this), INMEM_TRACKING_INTERVAL); } private async updateAllSavedObjects( From 07fe4bf34bbb9794ee2d0fc03a932bfb515fd193 Mon Sep 17 00:00:00 2001 From: Liza K Date: Wed, 9 Dec 2020 12:18:40 +0200 Subject: [PATCH 26/26] settimeout --- .../server/search/session/session_service.ts | 67 ++++++++++--------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index c9c6f17ef8b05..96d66157c48ec 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -77,7 +77,7 @@ export class BackgroundSessionService implements ISessionService { this.logger.debug(`setupMonitoring | Enabling monitoring`); const internalRepo = core.savedObjects.createInternalRepository([BACKGROUND_SESSION_TYPE]); this.internalSavedObjectsClient = new SavedObjectsClient(internalRepo); - this.monitorTimer = setTimeout(this.monitorMappedIds.bind(this), INMEM_TRACKING_INTERVAL); + this.monitorMappedIds(); } }; @@ -122,39 +122,40 @@ export class BackgroundSessionService implements ISessionService { }; private async monitorMappedIds() { - try { - this.clearSessions(); - - if (!this.sessionSearchMap.size) return; - this.logger.debug(`monitorMappedIds | Map contains ${this.sessionSearchMap.size} items`); - - const savedSessions = await this.getAllMappedSavedObjects(); - const updatedSessions = await this.updateAllSavedObjects(savedSessions); - - updatedSessions.forEach((updatedSavedObject) => { - const sessionInfo = this.sessionSearchMap.get(updatedSavedObject.id)!; - if (updatedSavedObject.error) { - // Retry next time - sessionInfo.retryCount++; - } else if (updatedSavedObject.attributes.idMapping) { - // Delete the ids that we just saved, avoiding a potential new ids being lost. - Object.keys(updatedSavedObject.attributes.idMapping).forEach((key) => { - sessionInfo.ids.delete(key); - }); - // If the session object is empty, delete it as well - if (!sessionInfo.ids.entries.length) { - this.sessionSearchMap.delete(updatedSavedObject.id); - } else { - sessionInfo.retryCount = 0; + this.monitorTimer = setTimeout(async () => { + try { + this.clearSessions(); + + if (!this.sessionSearchMap.size) return; + this.logger.debug(`monitorMappedIds | Map contains ${this.sessionSearchMap.size} items`); + + const savedSessions = await this.getAllMappedSavedObjects(); + const updatedSessions = await this.updateAllSavedObjects(savedSessions); + + updatedSessions.forEach((updatedSavedObject) => { + const sessionInfo = this.sessionSearchMap.get(updatedSavedObject.id)!; + if (updatedSavedObject.error) { + // Retry next time + sessionInfo.retryCount++; + } else if (updatedSavedObject.attributes.idMapping) { + // Delete the ids that we just saved, avoiding a potential new ids being lost. + Object.keys(updatedSavedObject.attributes.idMapping).forEach((key) => { + sessionInfo.ids.delete(key); + }); + // If the session object is empty, delete it as well + if (!sessionInfo.ids.entries.length) { + this.sessionSearchMap.delete(updatedSavedObject.id); + } else { + sessionInfo.retryCount = 0; + } } - } - }); - } catch (e) { - this.logger.error(`monitorMappedIds | Error while updating sessions. ${e}`); - } - - // Schedule the next iteration - this.monitorTimer = setTimeout(this.monitorMappedIds.bind(this), INMEM_TRACKING_INTERVAL); + }); + } catch (e) { + this.logger.error(`monitorMappedIds | Error while updating sessions. ${e}`); + } finally { + this.monitorMappedIds(); + } + }, INMEM_TRACKING_INTERVAL); } private async updateAllSavedObjects(