diff --git a/x-pack/plugins/data_enhanced/config.ts b/x-pack/plugins/data_enhanced/config.ts index fc1f22d50b09fa..8cbf930fe87bd1 100644 --- a/x-pack/plugins/data_enhanced/config.ts +++ b/x-pack/plugins/data_enhanced/config.ts @@ -18,7 +18,7 @@ export const configSchema = schema.object({ * pageSize controls how many search session objects we load at once while monitoring * session completion */ - pageSize: schema.number({ defaultValue: 10000 }), + pageSize: schema.number({ defaultValue: 100 }), /** * trackingInterval controls how often we track search session objects progress */ diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts index 462d1fc337ae2c..ae36b881796c46 100644 --- a/x-pack/plugins/data_enhanced/server/plugin.ts +++ b/x-pack/plugins/data_enhanced/server/plugin.ts @@ -6,13 +6,7 @@ */ import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; -import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; -import { - PluginSetup as DataPluginSetup, - PluginStart as DataPluginStart, - usageProvider, -} from '../../../../src/plugins/data/server'; -import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; +import { usageProvider } from '../../../../src/plugins/data/server'; import { ENHANCED_ES_SEARCH_STRATEGY, EQL_SEARCH_STRATEGY } from '../common'; import { registerSessionRoutes } from './routes'; import { searchSessionSavedObjectType } from './saved_objects'; @@ -22,22 +16,13 @@ import { eqlSearchStrategyProvider, } from './search'; import { getUiSettings } from './ui_settings'; -import type { DataEnhancedRequestHandlerContext } from './type'; +import type { + DataEnhancedRequestHandlerContext, + DataEnhancedSetupDependencies as SetupDependencies, + DataEnhancedStartDependencies as StartDependencies, +} from './type'; import { ConfigSchema } from '../config'; import { registerUsageCollector } from './collectors'; -import { SecurityPluginSetup } from '../../security/server'; - -interface SetupDependencies { - data: DataPluginSetup; - usageCollection?: UsageCollectionSetup; - taskManager: TaskManagerSetupContract; - security?: SecurityPluginSetup; -} - -export interface StartDependencies { - data: DataPluginStart; - taskManager: TaskManagerStartContract; -} export class EnhancedDataServerPlugin implements Plugin { @@ -50,7 +35,7 @@ export class EnhancedDataServerPlugin this.config = this.initializerContext.config.get(); } - public setup(core: CoreSetup, deps: SetupDependencies) { + public setup(core: CoreSetup, deps: SetupDependencies) { const usage = deps.usageCollection ? usageProvider(core) : undefined; core.uiSettings.register(getUiSettings()); diff --git a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts index 6e52b17f368030..60c7283320d0c2 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/check_running_sessions.ts @@ -14,7 +14,7 @@ import { } from 'kibana/server'; import moment from 'moment'; import { EMPTY, from } from 'rxjs'; -import { expand, mergeMap } from 'rxjs/operators'; +import { expand, concatMap } from 'rxjs/operators'; import { nodeBuilder } from '../../../../../../src/plugins/data/common'; import { ENHANCED_ES_SEARCH_STRATEGY, @@ -154,7 +154,7 @@ export async function checkRunningSessions( try { await getAllSavedSearchSessions$(deps, config) .pipe( - mergeMap(async (runningSearchSessionsResponse) => { + concatMap(async (runningSearchSessionsResponse) => { if (!runningSearchSessionsResponse.total) return; logger.debug(`Found ${runningSearchSessionsResponse.total} running sessions`); diff --git a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts b/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts index 8aa35def387b73..101ccb14edf67d 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts @@ -15,6 +15,7 @@ import { checkRunningSessions } from './check_running_sessions'; import { CoreSetup, SavedObjectsClient, Logger } from '../../../../../../src/core/server'; import { ConfigSchema } from '../../../config'; import { SEARCH_SESSION_TYPE } from '../../../common'; +import { DataEnhancedStartDependencies } from '../../type'; export const SEARCH_SESSIONS_TASK_TYPE = 'search_sessions_monitor'; export const SEARCH_SESSIONS_TASK_ID = `data_enhanced_${SEARCH_SESSIONS_TASK_TYPE}`; @@ -25,12 +26,19 @@ interface SearchSessionTaskDeps { config: ConfigSchema; } -function searchSessionRunner(core: CoreSetup, { logger, config }: SearchSessionTaskDeps) { +function searchSessionRunner( + core: CoreSetup, + { logger, config }: SearchSessionTaskDeps +) { return ({ taskInstance }: RunContext) => { return { async run() { const sessionConfig = config.search.sessions; const [coreStart] = await core.getStartServices(); + if (!sessionConfig.enabled) { + logger.debug('Search sessions are disabled. Skipping task.'); + return; + } const internalRepo = coreStart.savedObjects.createInternalRepository([SEARCH_SESSION_TYPE]); const internalSavedObjectsClient = new SavedObjectsClient(internalRepo); await checkRunningSessions( @@ -50,7 +58,10 @@ function searchSessionRunner(core: CoreSetup, { logger, config }: SearchSessionT }; } -export function registerSearchSessionsTask(core: CoreSetup, deps: SearchSessionTaskDeps) { +export function registerSearchSessionsTask( + core: CoreSetup, + deps: SearchSessionTaskDeps +) { deps.taskManager.registerTaskDefinitions({ [SEARCH_SESSIONS_TASK_TYPE]: { title: 'Search Sessions Monitor', @@ -59,6 +70,18 @@ export function registerSearchSessionsTask(core: CoreSetup, deps: SearchSessionT }); } +export async function unscheduleSearchSessionsTask( + taskManager: TaskManagerStartContract, + logger: Logger +) { + try { + await taskManager.removeIfExists(SEARCH_SESSIONS_TASK_ID); + logger.debug(`Search sessions cleared`); + } catch (e) { + logger.error(`Error clearing task, received ${e.message}`); + } +} + export async function scheduleSearchSessionsTasks( taskManager: TaskManagerStartContract, logger: Logger, @@ -79,6 +102,6 @@ export async function scheduleSearchSessionsTasks( logger.debug(`Search sessions task, scheduled to run`); } catch (e) { - logger.debug(`Error scheduling task, received ${e.message}`); + logger.error(`Error scheduling task, received ${e.message}`); } } 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 f61d89e2301abd..9344ab973c6366 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 @@ -15,12 +15,12 @@ import { SearchSessionStatus, SEARCH_SESSION_TYPE } from '../../../common'; import { SearchSessionService } from './session_service'; import { createRequestHash } from './utils'; import moment from 'moment'; -import { coreMock } from 'src/core/server/mocks'; +import { coreMock } from '../../../../../../src/core/server/mocks'; import { ConfigSchema } from '../../../config'; -// @ts-ignore import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { AuthenticatedUser } from '../../../../security/common/model'; import { nodeBuilder } from '../../../../../../src/plugins/data/common'; +import { TaskManagerStartContract } from '../../../../task_manager/server'; const MAX_UPDATE_RETRIES = 3; @@ -29,6 +29,7 @@ const flushPromises = () => new Promise((resolve) => setImmediate(resolve)); describe('SearchSessionService', () => { let savedObjectsClient: jest.Mocked; let service: SearchSessionService; + let mockTaskManager: jest.Mocked; const MOCK_STRATEGY = 'ese'; @@ -62,925 +63,1009 @@ describe('SearchSessionService', () => { references: [], }; - beforeEach(async () => { - savedObjectsClient = savedObjectsClientMock.create(); - const config: ConfigSchema = { - search: { - sessions: { - enabled: true, - pageSize: 10000, - notTouchedInProgressTimeout: moment.duration(1, 'm'), - notTouchedTimeout: moment.duration(2, 'm'), - maxUpdateRetries: MAX_UPDATE_RETRIES, - defaultExpiration: moment.duration(7, 'd'), - trackingInterval: moment.duration(10, 's'), - management: {} as any, + describe('Feature disabled', () => { + beforeEach(async () => { + savedObjectsClient = savedObjectsClientMock.create(); + const config: ConfigSchema = { + search: { + sessions: { + enabled: false, + pageSize: 10000, + notTouchedInProgressTimeout: moment.duration(1, 'm'), + notTouchedTimeout: moment.duration(2, 'm'), + maxUpdateRetries: MAX_UPDATE_RETRIES, + defaultExpiration: moment.duration(7, 'd'), + trackingInterval: moment.duration(10, 's'), + management: {} as any, + }, }, - }, - }; - const mockLogger: any = { - debug: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - }; - service = new SearchSessionService(mockLogger, config); - const coreStart = coreMock.createStart(); - const mockTaskManager = taskManagerMock.createStart(); - await flushPromises(); - await service.start(coreStart, { - taskManager: mockTaskManager, - }); - }); - - afterEach(() => { - service.stop(); - }); - - describe('save', () => { - it('throws if `name` is not provided', () => { - expect(() => - service.save({ savedObjectsClient }, mockUser1, sessionId, {}) - ).rejects.toMatchInlineSnapshot(`[Error: Name is required]`); + }; + const mockLogger: any = { + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + service = new SearchSessionService(mockLogger, config); + const coreStart = coreMock.createStart(); + mockTaskManager = taskManagerMock.createStart(); + await flushPromises(); + await service.start(coreStart, { + taskManager: mockTaskManager, + }); }); - it('throws if `appId` is not provided', () => { - expect( - service.save({ savedObjectsClient }, mockUser1, sessionId, { name: 'banana' }) - ).rejects.toMatchInlineSnapshot(`[Error: AppId is required]`); + afterEach(() => { + service.stop(); }); - it('throws if `generator id` is not provided', () => { - expect( - service.save({ savedObjectsClient }, mockUser1, sessionId, { - name: 'banana', - appId: 'nanana', - }) - ).rejects.toMatchInlineSnapshot(`[Error: UrlGeneratorId is required]`); + it('task is cleared, if exists', async () => { + expect(mockTaskManager.removeIfExists).toHaveBeenCalled(); }); - it('saving updates an existing saved object and persists it', async () => { - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); - - await service.save({ savedObjectsClient }, mockUser1, sessionId, { - name: 'banana', - appId: 'nanana', - urlGeneratorId: 'panama', + it('trackId ignores', async () => { + await service.trackId({ savedObjectsClient }, mockUser1, { params: {} }, '123', { + sessionId: '321', + strategy: MOCK_STRATEGY, }); - expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(savedObjectsClient.update).not.toHaveBeenCalled(); expect(savedObjectsClient.create).not.toHaveBeenCalled(); - - const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(id).toBe(sessionId); - expect(callAttributes).not.toHaveProperty('idMapping'); - expect(callAttributes).toHaveProperty('touched'); - expect(callAttributes).toHaveProperty('persisted', true); - expect(callAttributes).toHaveProperty('name', 'banana'); - expect(callAttributes).toHaveProperty('appId', 'nanana'); - expect(callAttributes).toHaveProperty('urlGeneratorId', 'panama'); - expect(callAttributes).toHaveProperty('initialState', {}); - expect(callAttributes).toHaveProperty('restoreState', {}); }); - it('saving creates a new persisted saved object, if it did not exist', async () => { - const mockCreatedSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - - savedObjectsClient.update.mockRejectedValue( - SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) - ); - savedObjectsClient.create.mockResolvedValue(mockCreatedSavedObject); - - await service.save({ savedObjectsClient }, mockUser1, sessionId, { - name: 'banana', - appId: 'nanana', - urlGeneratorId: 'panama', - }); - - expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); - - const [type, callAttributes, options] = savedObjectsClient.create.mock.calls[0]; - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(options?.id).toBe(sessionId); - expect(callAttributes).toHaveProperty('idMapping', {}); - expect(callAttributes).toHaveProperty('touched'); - expect(callAttributes).toHaveProperty('expires'); - expect(callAttributes).toHaveProperty('created'); - expect(callAttributes).toHaveProperty('persisted', true); - expect(callAttributes).toHaveProperty('name', 'banana'); - expect(callAttributes).toHaveProperty('appId', 'nanana'); - expect(callAttributes).toHaveProperty('urlGeneratorId', 'panama'); - expect(callAttributes).toHaveProperty('initialState', {}); - expect(callAttributes).toHaveProperty('restoreState', {}); - expect(callAttributes).toHaveProperty('realmType', mockUser1.authentication_realm.type); - expect(callAttributes).toHaveProperty('realmName', mockUser1.authentication_realm.name); - expect(callAttributes).toHaveProperty('username', mockUser1.username); + it('Save throws', () => { + expect(() => + service.save({ savedObjectsClient }, mockUser1, sessionId, {}) + ).rejects.toBeInstanceOf(Error); }); - it('throws error if user conflicts', () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - - expect( - service.get({ savedObjectsClient }, mockUser2, sessionId) - ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); + it('Update throws', () => { + const attributes = { name: 'new_name' }; + const response = service.update({ savedObjectsClient }, mockUser1, sessionId, attributes); + expect(response).rejects.toBeInstanceOf(Error); }); - it('works without security', async () => { - savedObjectsClient.update.mockRejectedValue( - SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) - ); - - await service.save( - { savedObjectsClient }, - - null, - sessionId, - { - name: 'my_name', - appId: 'my_app_id', - urlGeneratorId: 'my_url_generator_id', - } - ); - - expect(savedObjectsClient.create).toHaveBeenCalled(); - const [[, attributes]] = savedObjectsClient.create.mock.calls; - expect(attributes).toHaveProperty('realmType', undefined); - expect(attributes).toHaveProperty('realmName', undefined); - expect(attributes).toHaveProperty('username', undefined); + it('Cancel throws', () => { + const response = service.cancel({ savedObjectsClient }, mockUser1, sessionId); + expect(response).rejects.toBeInstanceOf(Error); }); - }); - - describe('get', () => { - it('calls saved objects client', async () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - const response = await service.get({ savedObjectsClient }, mockUser1, sessionId); - - expect(response).toBe(mockSavedObject); - expect(savedObjectsClient.get).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId); + it('getId throws', () => { + const response = service.getId({ savedObjectsClient }, mockUser1, {}, {}); + expect(response).rejects.toBeInstanceOf(Error); }); - it('works without security', async () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - - const response = await service.get({ savedObjectsClient }, null, sessionId); - - expect(response).toBe(mockSavedObject); - expect(savedObjectsClient.get).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId); + it('Delete throws', () => { + const response = service.delete({ savedObjectsClient }, mockUser1, sessionId); + expect(response).rejects.toBeInstanceOf(Error); }); }); - describe('find', () => { - it('calls saved objects client with user filter', 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({ savedObjectsClient }, mockUser1, options); - - expect(response).toBe(mockResponse); - const [[findOptions]] = savedObjectsClient.find.mock.calls; - expect(findOptions).toMatchInlineSnapshot(` - Object { - "filter": Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.realmType", - }, - Object { - "type": "literal", - "value": "my_realm_type", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.realmName", - }, - Object { - "type": "literal", - "value": "my_realm_name", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.username", - }, - Object { - "type": "literal", - "value": "my_username", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - ], - "function": "and", - "type": "function", + describe('Feature enabled', () => { + beforeEach(async () => { + savedObjectsClient = savedObjectsClientMock.create(); + const config: ConfigSchema = { + search: { + sessions: { + enabled: true, + pageSize: 10000, + notTouchedInProgressTimeout: moment.duration(1, 'm'), + notTouchedTimeout: moment.duration(2, 'm'), + maxUpdateRetries: MAX_UPDATE_RETRIES, + defaultExpiration: moment.duration(7, 'd'), + trackingInterval: moment.duration(10, 's'), + management: {} as any, }, - "page": 0, - "perPage": 5, - "type": "search-session", - } - `); - }); - - it('mixes in passed-in filter as string and KQL node', async () => { - const mockFindSavedObject = { - ...mockSavedObject, - score: 1, + }, }; - const mockResponse = { - saved_objects: [mockFindSavedObject], - total: 1, - per_page: 1, - page: 0, + const mockLogger: any = { + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), }; - savedObjectsClient.find.mockResolvedValue(mockResponse); - - const options1 = { filter: 'foobar' }; - const response1 = await service.find({ savedObjectsClient }, mockUser1, options1); - - const options2 = { filter: nodeBuilder.is('foo', 'bar') }; - const response2 = await service.find({ savedObjectsClient }, mockUser1, options2); - - expect(response1).toBe(mockResponse); - expect(response2).toBe(mockResponse); - - const [[findOptions1], [findOptions2]] = savedObjectsClient.find.mock.calls; - expect(findOptions1).toMatchInlineSnapshot(` - Object { - "filter": Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.realmType", - }, - Object { - "type": "literal", - "value": "my_realm_type", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.realmName", - }, - Object { - "type": "literal", - "value": "my_realm_name", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.username", - }, - Object { - "type": "literal", - "value": "my_username", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": null, - }, - Object { - "type": "literal", - "value": "foobar", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - ], - "function": "and", - "type": "function", - }, - "type": "search-session", - } - `); - expect(findOptions2).toMatchInlineSnapshot(` - Object { - "filter": Object { - "arguments": Array [ - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.realmType", - }, - Object { - "type": "literal", - "value": "my_realm_type", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.realmName", - }, - Object { - "type": "literal", - "value": "my_realm_name", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "search-session.attributes.username", - }, - Object { - "type": "literal", - "value": "my_username", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - Object { - "arguments": Array [ - Object { - "type": "literal", - "value": "foo", - }, - Object { - "type": "literal", - "value": "bar", - }, - Object { - "type": "literal", - "value": false, - }, - ], - "function": "is", - "type": "function", - }, - ], - "function": "and", - "type": "function", - }, - "type": "search-session", - } - `); + service = new SearchSessionService(mockLogger, config); + const coreStart = coreMock.createStart(); + mockTaskManager = taskManagerMock.createStart(); + await flushPromises(); + await service.start(coreStart, { + taskManager: mockTaskManager, + }); }); - it('has no filter without security', 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({ savedObjectsClient }, null, options); - - expect(response).toBe(mockResponse); - const [[findOptions]] = savedObjectsClient.find.mock.calls; - expect(findOptions).toMatchInlineSnapshot(` - Object { - "filter": undefined, - "page": 0, - "perPage": 5, - "type": "search-session", - } - `); + afterEach(() => { + service.stop(); }); - }); - - describe('update', () => { - it('update calls saved objects client with added touch time', async () => { - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); - const attributes = { name: 'new_name' }; - const response = await service.update( - { savedObjectsClient }, - mockUser1, - sessionId, - attributes - ); + it('task is cleared and re-created', async () => { + expect(mockTaskManager.removeIfExists).toHaveBeenCalled(); + expect(mockTaskManager.ensureScheduled).toHaveBeenCalled(); + }); - expect(response).toBe(mockUpdateSavedObject); + describe('save', () => { + it('throws if `name` is not provided', () => { + expect(() => + service.save({ savedObjectsClient }, mockUser1, sessionId, {}) + ).rejects.toMatchInlineSnapshot(`[Error: Name is required]`); + }); - const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + it('throws if `appId` is not provided', () => { + expect( + service.save({ savedObjectsClient }, mockUser1, sessionId, { name: 'banana' }) + ).rejects.toMatchInlineSnapshot(`[Error: AppId is required]`); + }); - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(id).toBe(sessionId); - expect(callAttributes).toHaveProperty('name', attributes.name); - expect(callAttributes).toHaveProperty('touched'); - }); + it('throws if `generator id` is not provided', () => { + expect( + service.save({ savedObjectsClient }, mockUser1, sessionId, { + name: 'banana', + appId: 'nanana', + }) + ).rejects.toMatchInlineSnapshot(`[Error: UrlGeneratorId is required]`); + }); - it('throws if user conflicts', () => { - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + it('saving updates an existing saved object and persists it', async () => { + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); - const attributes = { name: 'new_name' }; - expect( - service.update({ savedObjectsClient }, mockUser2, sessionId, attributes) - ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); - }); + await service.save({ savedObjectsClient }, mockUser1, sessionId, { + name: 'banana', + appId: 'nanana', + urlGeneratorId: 'panama', + }); - it('works without security', async () => { - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.get.mockResolvedValue(mockSavedObject); - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(savedObjectsClient.create).not.toHaveBeenCalled(); + + const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(id).toBe(sessionId); + expect(callAttributes).not.toHaveProperty('idMapping'); + expect(callAttributes).toHaveProperty('touched'); + expect(callAttributes).toHaveProperty('persisted', true); + expect(callAttributes).toHaveProperty('name', 'banana'); + expect(callAttributes).toHaveProperty('appId', 'nanana'); + expect(callAttributes).toHaveProperty('urlGeneratorId', 'panama'); + expect(callAttributes).toHaveProperty('initialState', {}); + expect(callAttributes).toHaveProperty('restoreState', {}); + }); - const attributes = { name: 'new_name' }; - const response = await service.update({ savedObjectsClient }, null, sessionId, attributes); - const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; - - expect(response).toBe(mockUpdateSavedObject); - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(id).toBe(sessionId); - expect(callAttributes).toHaveProperty('name', 'new_name'); - expect(callAttributes).toHaveProperty('touched'); - }); - }); + it('saving creates a new persisted saved object, if it did not exist', async () => { + const mockCreatedSavedObject = { + ...mockSavedObject, + attributes: {}, + }; - describe('cancel', () => { - it('updates object status', async () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.update.mockRejectedValue( + SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) + ); + savedObjectsClient.create.mockResolvedValue(mockCreatedSavedObject); - await service.cancel({ savedObjectsClient }, mockUser1, sessionId); - const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + await service.save({ savedObjectsClient }, mockUser1, sessionId, { + name: 'banana', + appId: 'nanana', + urlGeneratorId: 'panama', + }); - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(id).toBe(sessionId); - expect(callAttributes).toHaveProperty('status', SearchSessionStatus.CANCELLED); - expect(callAttributes).toHaveProperty('touched'); - }); + expect(savedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); + + const [type, callAttributes, options] = savedObjectsClient.create.mock.calls[0]; + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(options?.id).toBe(sessionId); + expect(callAttributes).toHaveProperty('idMapping', {}); + expect(callAttributes).toHaveProperty('touched'); + expect(callAttributes).toHaveProperty('expires'); + expect(callAttributes).toHaveProperty('created'); + expect(callAttributes).toHaveProperty('persisted', true); + expect(callAttributes).toHaveProperty('name', 'banana'); + expect(callAttributes).toHaveProperty('appId', 'nanana'); + expect(callAttributes).toHaveProperty('urlGeneratorId', 'panama'); + expect(callAttributes).toHaveProperty('initialState', {}); + expect(callAttributes).toHaveProperty('restoreState', {}); + expect(callAttributes).toHaveProperty('realmType', mockUser1.authentication_realm.type); + expect(callAttributes).toHaveProperty('realmName', mockUser1.authentication_realm.name); + expect(callAttributes).toHaveProperty('username', mockUser1.username); + }); - it('throws if user conflicts', () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); + it('throws error if user conflicts', () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); - expect( - service.cancel({ savedObjectsClient }, mockUser2, sessionId) - ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); - }); + expect( + service.get({ savedObjectsClient }, mockUser2, sessionId) + ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); + }); - it('works without security', async () => { - savedObjectsClient.get.mockResolvedValue(mockSavedObject); + it('works without security', async () => { + savedObjectsClient.update.mockRejectedValue( + SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) + ); - await service.cancel({ savedObjectsClient }, null, sessionId); + await service.save( + { savedObjectsClient }, - const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + null, + sessionId, + { + name: 'my_name', + appId: 'my_app_id', + urlGeneratorId: 'my_url_generator_id', + } + ); - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(id).toBe(sessionId); - expect(callAttributes).toHaveProperty('status', SearchSessionStatus.CANCELLED); - expect(callAttributes).toHaveProperty('touched'); + expect(savedObjectsClient.create).toHaveBeenCalled(); + const [[, attributes]] = savedObjectsClient.create.mock.calls; + expect(attributes).toHaveProperty('realmType', undefined); + expect(attributes).toHaveProperty('realmName', undefined); + expect(attributes).toHaveProperty('username', undefined); + }); }); - }); - describe('trackId', () => { - it('updates the saved object if search session already exists', async () => { - const searchRequest = { params: {} }; - const requestHash = createRequestHash(searchRequest.params); - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + describe('get', () => { + it('calls saved objects client', async () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + const response = await service.get({ savedObjectsClient }, mockUser1, sessionId); - await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { - sessionId, - strategy: MOCK_STRATEGY, + expect(response).toBe(mockSavedObject); + expect(savedObjectsClient.get).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId); }); - expect(savedObjectsClient.update).toHaveBeenCalled(); - expect(savedObjectsClient.create).not.toHaveBeenCalled(); + it('works without security', async () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); - const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(id).toBe(sessionId); - expect(callAttributes).toHaveProperty('idMapping', { - [requestHash]: { - id: searchId, - status: SearchSessionStatus.IN_PROGRESS, - strategy: MOCK_STRATEGY, - }, + const response = await service.get({ savedObjectsClient }, null, sessionId); + + expect(response).toBe(mockSavedObject); + expect(savedObjectsClient.get).toHaveBeenCalledWith(SEARCH_SESSION_TYPE, sessionId); }); - expect(callAttributes).toHaveProperty('touched'); }); - it('retries updating the saved object if there was a ES conflict 409', async () => { - const searchRequest = { params: {} }; - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - - let counter = 0; - - savedObjectsClient.update.mockImplementation(() => { - return new Promise((resolve, reject) => { - if (counter === 0) { - counter++; - reject(SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId)); - } else { - resolve(mockUpdateSavedObject); + describe('find', () => { + it('calls saved objects client with user filter', 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({ savedObjectsClient }, mockUser1, options); + + expect(response).toBe(mockResponse); + const [[findOptions]] = savedObjectsClient.find.mock.calls; + expect(findOptions).toMatchInlineSnapshot(` + Object { + "filter": Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmType", + }, + Object { + "type": "literal", + "value": "my_realm_type", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmName", + }, + Object { + "type": "literal", + "value": "my_realm_name", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.username", + }, + Object { + "type": "literal", + "value": "my_username", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "and", + "type": "function", + }, + "page": 0, + "perPage": 5, + "type": "search-session", } - }); + `); }); - await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { - sessionId, - strategy: MOCK_STRATEGY, + it('mixes in passed-in filter as string and KQL node', async () => { + const mockFindSavedObject = { + ...mockSavedObject, + score: 1, + }; + const mockResponse = { + saved_objects: [mockFindSavedObject], + total: 1, + per_page: 1, + page: 0, + }; + savedObjectsClient.find.mockResolvedValue(mockResponse); + + const options1 = { filter: 'foobar' }; + const response1 = await service.find({ savedObjectsClient }, mockUser1, options1); + + const options2 = { filter: nodeBuilder.is('foo', 'bar') }; + const response2 = await service.find({ savedObjectsClient }, mockUser1, options2); + + expect(response1).toBe(mockResponse); + expect(response2).toBe(mockResponse); + + const [[findOptions1], [findOptions2]] = savedObjectsClient.find.mock.calls; + expect(findOptions1).toMatchInlineSnapshot(` + Object { + "filter": Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmType", + }, + Object { + "type": "literal", + "value": "my_realm_type", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmName", + }, + Object { + "type": "literal", + "value": "my_realm_name", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.username", + }, + Object { + "type": "literal", + "value": "my_username", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": null, + }, + Object { + "type": "literal", + "value": "foobar", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "and", + "type": "function", + }, + "type": "search-session", + } + `); + expect(findOptions2).toMatchInlineSnapshot(` + Object { + "filter": Object { + "arguments": Array [ + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmType", + }, + Object { + "type": "literal", + "value": "my_realm_type", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.realmName", + }, + Object { + "type": "literal", + "value": "my_realm_name", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "search-session.attributes.username", + }, + Object { + "type": "literal", + "value": "my_username", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "foo", + }, + Object { + "type": "literal", + "value": "bar", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + }, + ], + "function": "and", + "type": "function", + }, + "type": "search-session", + } + `); }); - expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); - expect(savedObjectsClient.create).not.toHaveBeenCalled(); + it('has no filter without security', 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({ savedObjectsClient }, null, options); + + expect(response).toBe(mockResponse); + const [[findOptions]] = savedObjectsClient.find.mock.calls; + expect(findOptions).toMatchInlineSnapshot(` + Object { + "filter": undefined, + "page": 0, + "perPage": 5, + "type": "search-session", + } + `); + }); }); - it('retries updating the saved object if theres a ES conflict 409, but stops after MAX_RETRIES times', async () => { - const searchRequest = { params: {} }; - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + describe('update', () => { + it('update calls saved objects client with added touch time', async () => { + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + + const attributes = { name: 'new_name' }; + const response = await service.update( + { savedObjectsClient }, + mockUser1, + sessionId, + attributes + ); - savedObjectsClient.update.mockImplementation(() => { - return new Promise((resolve, reject) => { - reject(SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId)); - }); + expect(response).toBe(mockUpdateSavedObject); + + const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(id).toBe(sessionId); + expect(callAttributes).toHaveProperty('name', attributes.name); + expect(callAttributes).toHaveProperty('touched'); }); - await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { - sessionId, - strategy: MOCK_STRATEGY, + it('throws if user conflicts', () => { + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + + const attributes = { name: 'new_name' }; + expect( + service.update({ savedObjectsClient }, mockUser2, sessionId, attributes) + ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); }); - // Track ID doesn't throw errors even in cases of failure! - expect(savedObjectsClient.update).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); - expect(savedObjectsClient.create).not.toHaveBeenCalled(); + it('works without security', async () => { + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + + const attributes = { name: 'new_name' }; + const response = await service.update({ savedObjectsClient }, null, sessionId, attributes); + const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + + expect(response).toBe(mockUpdateSavedObject); + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(id).toBe(sessionId); + expect(callAttributes).toHaveProperty('name', 'new_name'); + expect(callAttributes).toHaveProperty('touched'); + }); }); - it('creates the saved object in non persisted state, if search session doesnt exists', async () => { - const searchRequest = { params: {} }; - const requestHash = createRequestHash(searchRequest.params); - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + describe('cancel', () => { + it('updates object status', async () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); - const mockCreatedSavedObject = { - ...mockSavedObject, - attributes: {}, - }; + await service.cancel({ savedObjectsClient }, mockUser1, sessionId); + const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; - savedObjectsClient.update.mockRejectedValue( - SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) - ); - savedObjectsClient.create.mockResolvedValue(mockCreatedSavedObject); + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(id).toBe(sessionId); + expect(callAttributes).toHaveProperty('status', SearchSessionStatus.CANCELLED); + expect(callAttributes).toHaveProperty('touched'); + }); - await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { - sessionId, - strategy: MOCK_STRATEGY, + it('throws if user conflicts', () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); + + expect( + service.cancel({ savedObjectsClient }, mockUser2, sessionId) + ).rejects.toMatchInlineSnapshot(`[Error: Not Found]`); }); - expect(savedObjectsClient.update).toHaveBeenCalled(); - expect(savedObjectsClient.create).toHaveBeenCalled(); + it('works without security', async () => { + savedObjectsClient.get.mockResolvedValue(mockSavedObject); - const [type, callAttributes, options] = savedObjectsClient.create.mock.calls[0]; - expect(type).toBe(SEARCH_SESSION_TYPE); - expect(options).toStrictEqual({ id: sessionId }); - expect(callAttributes).toHaveProperty('idMapping', { - [requestHash]: { - id: searchId, - status: SearchSessionStatus.IN_PROGRESS, - strategy: MOCK_STRATEGY, - }, + await service.cancel({ savedObjectsClient }, null, sessionId); + + const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(id).toBe(sessionId); + expect(callAttributes).toHaveProperty('status', SearchSessionStatus.CANCELLED); + expect(callAttributes).toHaveProperty('touched'); }); - expect(callAttributes).toHaveProperty('expires'); - expect(callAttributes).toHaveProperty('created'); - expect(callAttributes).toHaveProperty('touched'); - expect(callAttributes).toHaveProperty('sessionId', sessionId); - expect(callAttributes).toHaveProperty('persisted', false); }); - it('retries updating if update returned 404 and then update returned conflict 409 (first create race condition)', async () => { - const searchRequest = { params: {} }; - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + describe('trackId', () => { + it('updates the saved object if search session already exists', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); - let counter = 0; + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, + strategy: MOCK_STRATEGY, + }); - savedObjectsClient.update.mockImplementation(() => { - return new Promise((resolve, reject) => { - if (counter === 0) { - counter++; - reject(SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId)); - } else { - resolve(mockUpdateSavedObject); - } + expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(savedObjectsClient.create).not.toHaveBeenCalled(); + + const [type, id, callAttributes] = savedObjectsClient.update.mock.calls[0]; + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(id).toBe(sessionId); + expect(callAttributes).toHaveProperty('idMapping', { + [requestHash]: { + id: searchId, + status: SearchSessionStatus.IN_PROGRESS, + strategy: MOCK_STRATEGY, + }, }); + expect(callAttributes).toHaveProperty('touched'); }); - savedObjectsClient.create.mockRejectedValue( - SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId) - ); + it('retries updating the saved object if there was a ES conflict 409', async () => { + const searchRequest = { params: {} }; + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + + let counter = 0; + + savedObjectsClient.update.mockImplementation(() => { + return new Promise((resolve, reject) => { + if (counter === 0) { + counter++; + reject(SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId)); + } else { + resolve(mockUpdateSavedObject); + } + }); + }); - await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { - sessionId, - strategy: MOCK_STRATEGY, + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, + strategy: MOCK_STRATEGY, + }); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); + expect(savedObjectsClient.create).not.toHaveBeenCalled(); }); - expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); - expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); - }); + it('retries updating the saved object if theres a ES conflict 409, but stops after MAX_RETRIES times', async () => { + const searchRequest = { params: {} }; + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - it('retries everything at most MAX_RETRIES times', async () => { - const searchRequest = { params: {} }; - const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + savedObjectsClient.update.mockImplementation(() => { + return new Promise((resolve, reject) => { + reject(SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId)); + }); + }); - savedObjectsClient.update.mockRejectedValue( - SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) - ); - savedObjectsClient.create.mockRejectedValue( - SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId) - ); + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, + strategy: MOCK_STRATEGY, + }); - await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { - sessionId, - strategy: MOCK_STRATEGY, + // Track ID doesn't throw errors even in cases of failure! + expect(savedObjectsClient.update).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); + expect(savedObjectsClient.create).not.toHaveBeenCalled(); }); - expect(savedObjectsClient.update).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); - expect(savedObjectsClient.create).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); - }); + it('creates the saved object in non persisted state, if search session doesnt exists', async () => { + const searchRequest = { params: {} }; + const requestHash = createRequestHash(searchRequest.params); + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; - it('batches updates for the same session', async () => { - const sessionId1 = 'sessiondId1'; - const sessionId2 = 'sessiondId2'; + const mockCreatedSavedObject = { + ...mockSavedObject, + attributes: {}, + }; - const searchRequest1 = { params: { 1: '1' } }; - const requestHash1 = createRequestHash(searchRequest1.params); - const searchId1 = 'searchId1'; + savedObjectsClient.update.mockRejectedValue( + SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) + ); + savedObjectsClient.create.mockResolvedValue(mockCreatedSavedObject); - const searchRequest2 = { params: { 2: '2' } }; - const requestHash2 = createRequestHash(searchRequest2.params); - const searchId2 = 'searchId1'; + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, + strategy: MOCK_STRATEGY, + }); - const searchRequest3 = { params: { 3: '3' } }; - const requestHash3 = createRequestHash(searchRequest3.params); - const searchId3 = 'searchId3'; + expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(savedObjectsClient.create).toHaveBeenCalled(); + + const [type, callAttributes, options] = savedObjectsClient.create.mock.calls[0]; + expect(type).toBe(SEARCH_SESSION_TYPE); + expect(options).toStrictEqual({ id: sessionId }); + expect(callAttributes).toHaveProperty('idMapping', { + [requestHash]: { + id: searchId, + status: SearchSessionStatus.IN_PROGRESS, + strategy: MOCK_STRATEGY, + }, + }); + expect(callAttributes).toHaveProperty('expires'); + expect(callAttributes).toHaveProperty('created'); + expect(callAttributes).toHaveProperty('touched'); + expect(callAttributes).toHaveProperty('sessionId', sessionId); + expect(callAttributes).toHaveProperty('persisted', false); + }); - const mockUpdateSavedObject = { - ...mockSavedObject, - attributes: {}, - }; - savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + it('retries updating if update returned 404 and then update returned conflict 409 (first create race condition)', async () => { + const searchRequest = { params: {} }; + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + + let counter = 0; + + savedObjectsClient.update.mockImplementation(() => { + return new Promise((resolve, reject) => { + if (counter === 0) { + counter++; + reject(SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId)); + } else { + resolve(mockUpdateSavedObject); + } + }); + }); - await Promise.all([ - service.trackId({ savedObjectsClient }, mockUser1, searchRequest1, searchId1, { - sessionId: sessionId1, - strategy: MOCK_STRATEGY, - }), - service.trackId({ savedObjectsClient }, mockUser1, searchRequest2, searchId2, { - sessionId: sessionId1, - strategy: MOCK_STRATEGY, - }), - service.trackId({ savedObjectsClient }, mockUser1, searchRequest3, searchId3, { - sessionId: sessionId2, + savedObjectsClient.create.mockRejectedValue( + SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId) + ); + + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, strategy: MOCK_STRATEGY, - }), - ]); + }); - expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); // 3 trackIds calls batched into 2 update calls (2 different sessions) - expect(savedObjectsClient.create).not.toHaveBeenCalled(); + expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); + expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); + }); - const [type1, id1, callAttributes1] = savedObjectsClient.update.mock.calls[0]; - expect(type1).toBe(SEARCH_SESSION_TYPE); - expect(id1).toBe(sessionId1); - expect(callAttributes1).toHaveProperty('idMapping', { - [requestHash1]: { - id: searchId1, - status: SearchSessionStatus.IN_PROGRESS, - strategy: MOCK_STRATEGY, - }, - [requestHash2]: { - id: searchId2, - status: SearchSessionStatus.IN_PROGRESS, + it('retries everything at most MAX_RETRIES times', async () => { + const searchRequest = { params: {} }; + const searchId = 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0'; + + savedObjectsClient.update.mockRejectedValue( + SavedObjectsErrorHelpers.createGenericNotFoundError(sessionId) + ); + savedObjectsClient.create.mockRejectedValue( + SavedObjectsErrorHelpers.createConflictError(SEARCH_SESSION_TYPE, searchId) + ); + + await service.trackId({ savedObjectsClient }, mockUser1, searchRequest, searchId, { + sessionId, strategy: MOCK_STRATEGY, - }, + }); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); + expect(savedObjectsClient.create).toHaveBeenCalledTimes(MAX_UPDATE_RETRIES); }); - expect(callAttributes1).toHaveProperty('touched'); - const [type2, id2, callAttributes2] = savedObjectsClient.update.mock.calls[1]; - expect(type2).toBe(SEARCH_SESSION_TYPE); - expect(id2).toBe(sessionId2); - expect(callAttributes2).toHaveProperty('idMapping', { - [requestHash3]: { - id: searchId3, - status: SearchSessionStatus.IN_PROGRESS, - strategy: MOCK_STRATEGY, - }, + it('batches updates for the same session', async () => { + const sessionId1 = 'sessiondId1'; + const sessionId2 = 'sessiondId2'; + + const searchRequest1 = { params: { 1: '1' } }; + const requestHash1 = createRequestHash(searchRequest1.params); + const searchId1 = 'searchId1'; + + const searchRequest2 = { params: { 2: '2' } }; + const requestHash2 = createRequestHash(searchRequest2.params); + const searchId2 = 'searchId1'; + + const searchRequest3 = { params: { 3: '3' } }; + const requestHash3 = createRequestHash(searchRequest3.params); + const searchId3 = 'searchId3'; + + const mockUpdateSavedObject = { + ...mockSavedObject, + attributes: {}, + }; + savedObjectsClient.update.mockResolvedValue(mockUpdateSavedObject); + + await Promise.all([ + service.trackId({ savedObjectsClient }, mockUser1, searchRequest1, searchId1, { + sessionId: sessionId1, + strategy: MOCK_STRATEGY, + }), + service.trackId({ savedObjectsClient }, mockUser1, searchRequest2, searchId2, { + sessionId: sessionId1, + strategy: MOCK_STRATEGY, + }), + service.trackId({ savedObjectsClient }, mockUser1, searchRequest3, searchId3, { + sessionId: sessionId2, + strategy: MOCK_STRATEGY, + }), + ]); + + expect(savedObjectsClient.update).toHaveBeenCalledTimes(2); // 3 trackIds calls batched into 2 update calls (2 different sessions) + expect(savedObjectsClient.create).not.toHaveBeenCalled(); + + const [type1, id1, callAttributes1] = savedObjectsClient.update.mock.calls[0]; + expect(type1).toBe(SEARCH_SESSION_TYPE); + expect(id1).toBe(sessionId1); + expect(callAttributes1).toHaveProperty('idMapping', { + [requestHash1]: { + id: searchId1, + status: SearchSessionStatus.IN_PROGRESS, + strategy: MOCK_STRATEGY, + }, + [requestHash2]: { + id: searchId2, + status: SearchSessionStatus.IN_PROGRESS, + strategy: MOCK_STRATEGY, + }, + }); + expect(callAttributes1).toHaveProperty('touched'); + + const [type2, id2, callAttributes2] = savedObjectsClient.update.mock.calls[1]; + expect(type2).toBe(SEARCH_SESSION_TYPE); + expect(id2).toBe(sessionId2); + expect(callAttributes2).toHaveProperty('idMapping', { + [requestHash3]: { + id: searchId3, + status: SearchSessionStatus.IN_PROGRESS, + strategy: MOCK_STRATEGY, + }, + }); + expect(callAttributes2).toHaveProperty('touched'); }); - expect(callAttributes2).toHaveProperty('touched'); }); - }); - describe('getId', () => { - it('throws if `sessionId` is not provided', () => { - const searchRequest = { params: {} }; + describe('getId', () => { + it('throws if `sessionId` is not provided', () => { + const searchRequest = { params: {} }; - expect(() => - service.getId({ savedObjectsClient }, mockUser1, searchRequest, {}) - ).rejects.toMatchInlineSnapshot(`[Error: Session ID is required]`); - }); + expect(() => + service.getId({ savedObjectsClient }, mockUser1, searchRequest, {}) + ).rejects.toMatchInlineSnapshot(`[Error: Session ID is required]`); + }); - it('throws if there is not a saved object', () => { - const searchRequest = { params: {} }; + it('throws if there is not a saved object', () => { + const searchRequest = { params: {} }; + + expect(() => + service.getId({ savedObjectsClient }, mockUser1, searchRequest, { + sessionId, + isStored: false, + }) + ).rejects.toMatchInlineSnapshot( + `[Error: Cannot get search ID from a session that is not stored]` + ); + }); - expect(() => - service.getId({ savedObjectsClient }, mockUser1, searchRequest, { - sessionId, - isStored: false, - }) - ).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({ savedObjectsClient }, mockUser1, searchRequest, { + sessionId, + isStored: true, + isRestore: false, + }) + ).rejects.toMatchInlineSnapshot( + `[Error: Get search ID is only supported when restoring a session]` + ); + }); - it('throws if not restoring a saved session', () => { - const searchRequest = { params: {} }; + 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 = { + ...mockSavedObject, + attributes: { + ...mockSavedObject.attributes, + idMapping: { + [requestHash]: { + id: searchId, + }, + }, + }, + }; + savedObjectsClient.get.mockResolvedValue(mockSession); - expect(() => - service.getId({ savedObjectsClient }, mockUser1, searchRequest, { + const id = await service.getId({ savedObjectsClient }, mockUser1, searchRequest, { sessionId, isStored: true, - isRestore: false, - }) - ).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 = { - ...mockSavedObject, - attributes: { - ...mockSavedObject.attributes, - idMapping: { - [requestHash]: { - id: searchId, - }, - }, - }, - }; - savedObjectsClient.get.mockResolvedValue(mockSession); + isRestore: true, + }); - const id = await service.getId({ savedObjectsClient }, mockUser1, searchRequest, { - sessionId, - isStored: true, - isRestore: true, + expect(id).toBe(searchId); }); - - expect(id).toBe(searchId); }); - }); - describe('getSearchIdMapping', () => { - it('retrieves the search IDs and strategies from the saved object', async () => { - const mockSession = { - ...mockSavedObject, - attributes: { - ...mockSavedObject.attributes, - idMapping: { - foo: { - id: 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0', - strategy: MOCK_STRATEGY, + describe('getSearchIdMapping', () => { + it('retrieves the search IDs and strategies from the saved object', async () => { + const mockSession = { + ...mockSavedObject, + attributes: { + ...mockSavedObject.attributes, + idMapping: { + foo: { + id: 'FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0', + strategy: MOCK_STRATEGY, + }, }, }, - }, - }; - savedObjectsClient.get.mockResolvedValue(mockSession); - const searchIdMapping = await service.getSearchIdMapping( - { savedObjectsClient }, - mockUser1, - mockSession.id - ); - expect(searchIdMapping).toMatchInlineSnapshot(` - Map { - "FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0" => "ese", - } - `); + }; + savedObjectsClient.get.mockResolvedValue(mockSession); + const searchIdMapping = await service.getSearchIdMapping( + { savedObjectsClient }, + mockUser1, + mockSession.id + ); + expect(searchIdMapping).toMatchInlineSnapshot(` + Map { + "FnpFYlBpeXdCUTMyZXhCLTc1TWFKX0EbdDFDTzJzTE1Sck9PVTBIcW1iU05CZzo4MDA0" => "ese", + } + `); + }); }); }); }); 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 c95c58a8dc06ba..b5f7da594d53b9 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 @@ -29,6 +29,7 @@ import { TaskManagerStartContract, } from '../../../../task_manager/server'; import { + ENHANCED_ES_SEARCH_STRATEGY, SearchSessionRequestInfo, SearchSessionSavedObjectAttributes, SearchSessionStatus, @@ -36,8 +37,13 @@ import { } from '../../../common'; import { createRequestHash } from './utils'; import { ConfigSchema } from '../../../config'; -import { registerSearchSessionsTask, scheduleSearchSessionsTasks } from './monitoring_task'; +import { + registerSearchSessionsTask, + scheduleSearchSessionsTasks, + unscheduleSearchSessionsTask, +} from './monitoring_task'; import { SearchSessionsConfig, SearchStatus } from './types'; +import { DataEnhancedStartDependencies } from '../../type'; export interface SearchSessionDependencies { savedObjectsClient: SavedObjectsClientContract; @@ -78,7 +84,7 @@ export class SearchSessionService this.sessionConfig = this.config.search.sessions; } - public setup(core: CoreSetup, deps: SetupDependencies) { + public setup(core: CoreSetup, deps: SetupDependencies) { registerSearchSessionsTask(core, { config: this.config, taskManager: deps.taskManager, @@ -99,6 +105,8 @@ export class SearchSessionService this.logger, this.sessionConfig.trackingInterval ); + } else { + unscheduleSearchSessionsTask(deps.taskManager, this.logger); } }; @@ -217,6 +225,7 @@ export class SearchSessionService restoreState = {}, }: Partial ) => { + if (!this.sessionConfig.enabled) throw new Error('Search sessions are disabled'); if (!name) throw new Error('Name is required'); if (!appId) throw new Error('AppId is required'); if (!urlGeneratorId) throw new Error('UrlGeneratorId is required'); @@ -316,6 +325,7 @@ export class SearchSessionService attributes: Partial ) => { this.logger.debug(`update | ${sessionId}`); + if (!this.sessionConfig.enabled) throw new Error('Search sessions are disabled'); await this.get(deps, user, sessionId); // Verify correct user return deps.savedObjectsClient.update( SEARCH_SESSION_TYPE, @@ -353,6 +363,7 @@ export class SearchSessionService user: AuthenticatedUser | null, sessionId: string ) => { + if (!this.sessionConfig.enabled) throw new Error('Search sessions are disabled'); this.logger.debug(`delete | ${sessionId}`); await this.get(deps, user, sessionId); // Verify correct user return deps.savedObjectsClient.delete(SEARCH_SESSION_TYPE, sessionId); @@ -367,9 +378,9 @@ export class SearchSessionService user: AuthenticatedUser | null, searchRequest: IKibanaSearchRequest, searchId: string, - { sessionId, strategy }: ISearchOptions + { sessionId, strategy = ENHANCED_ES_SEARCH_STRATEGY }: ISearchOptions ) => { - if (!sessionId || !searchId) return; + if (!this.sessionConfig.enabled || !sessionId || !searchId) return; this.logger.debug(`trackId | ${sessionId} | ${searchId}`); let idMapping: Record = {}; @@ -378,7 +389,7 @@ export class SearchSessionService const requestHash = createRequestHash(searchRequest.params); const searchInfo = { id: searchId, - strategy: strategy!, + strategy, status: SearchStatus.IN_PROGRESS, }; idMapping = { [requestHash]: searchInfo }; @@ -411,7 +422,9 @@ export class SearchSessionService searchRequest: IKibanaSearchRequest, { sessionId, isStored, isRestore }: ISearchOptions ) => { - if (!sessionId) { + if (!this.sessionConfig.enabled) { + throw new Error('Search sessions are disabled'); + } else 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'); diff --git a/x-pack/plugins/data_enhanced/server/type.ts b/x-pack/plugins/data_enhanced/server/type.ts index c4a16eab1a3a7d..215700c5dcc5ce 100644 --- a/x-pack/plugins/data_enhanced/server/type.ts +++ b/x-pack/plugins/data_enhanced/server/type.ts @@ -7,6 +7,13 @@ import type { IRouter } from 'kibana/server'; import type { DataRequestHandlerContext } from '../../../../src/plugins/data/server'; +import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; +import { + PluginSetup as DataPluginSetup, + PluginStart as DataPluginStart, +} from '../../../../src/plugins/data/server'; +import { SecurityPluginSetup } from '../../security/server'; +import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; /** * @internal @@ -17,3 +24,15 @@ export type DataEnhancedRequestHandlerContext = DataRequestHandlerContext; * @internal */ export type DataEnhancedPluginRouter = IRouter; + +export interface DataEnhancedSetupDependencies { + data: DataPluginSetup; + usageCollection?: UsageCollectionSetup; + taskManager: TaskManagerSetupContract; + security?: SecurityPluginSetup; +} + +export interface DataEnhancedStartDependencies { + data: DataPluginStart; + taskManager: TaskManagerStartContract; +}