diff --git a/x-pack/plugins/reporting/common/errors/errors.test.ts b/x-pack/plugins/reporting/common/errors/errors.test.ts index e0cc86a40d373..37210373f4d68 100644 --- a/x-pack/plugins/reporting/common/errors/errors.test.ts +++ b/x-pack/plugins/reporting/common/errors/errors.test.ts @@ -5,17 +5,23 @@ * 2.0. */ -import { AuthenticationExpiredError } from '.'; +import * as errors from '.'; describe('ReportingError', () => { it('provides error code when stringified', () => { - expect(new AuthenticationExpiredError() + '').toBe( - `ReportingError(code: authentication_expired)` + expect(new errors.AuthenticationExpiredError() + '').toBe( + `ReportingError(code: authentication_expired_error)` ); }); it('provides details if there are any and error code when stringified', () => { - expect(new AuthenticationExpiredError('some details') + '').toBe( - `ReportingError(code: authentication_expired) "some details"` + expect(new errors.AuthenticationExpiredError('some details') + '').toBe( + `ReportingError(code: authentication_expired_error) "some details"` ); }); + it('has the expected code structure', () => { + const { ReportingError: _, ...nonAbstractErrors } = errors; + Object.values(nonAbstractErrors).forEach((Ctor) => { + expect(new Ctor().code).toMatch(/^[a-z_]+_error$/); + }); + }); }); diff --git a/x-pack/plugins/reporting/common/errors/index.ts b/x-pack/plugins/reporting/common/errors/index.ts index 6064eca33ed7b..0e84fa854f386 100644 --- a/x-pack/plugins/reporting/common/errors/index.ts +++ b/x-pack/plugins/reporting/common/errors/index.ts @@ -9,6 +9,12 @@ import { i18n } from '@kbn/i18n'; export abstract class ReportingError extends Error { + /** + * A string that uniquely brands an error type. This is used to power telemetry + * about reporting failures. + * + * @note Convention for codes: lower-case, snake-case and end in `_error`. + */ public abstract code: string; constructor(public details?: string) { @@ -32,7 +38,7 @@ export abstract class ReportingError extends Error { * access token expired. */ export class AuthenticationExpiredError extends ReportingError { - code = 'authentication_expired'; + code = 'authentication_expired_error'; } export class QueueTimeoutError extends ReportingError { @@ -59,7 +65,9 @@ export class PdfWorkerOutOfMemoryError extends ReportingError { } } -// TODO: Add ReportingError for Kibana stopping unexpectedly -// TODO: Add ReportingError for missing Chromium dependencies +export class KibanaShuttingDownError extends ReportingError { + code = 'kibana_shutting_down_error'; +} + // TODO: Add ReportingError for missing Chromium dependencies // TODO: Add ReportingError for Chromium not starting for an unknown reason diff --git a/x-pack/plugins/reporting/server/config/create_config.test.ts b/x-pack/plugins/reporting/server/config/create_config.test.ts index f839d72e1a45d..498281c56424f 100644 --- a/x-pack/plugins/reporting/server/config/create_config.test.ts +++ b/x-pack/plugins/reporting/server/config/create_config.test.ts @@ -76,6 +76,16 @@ describe('Reporting server createConfig$', () => { expect(result).toMatchInlineSnapshot(` Object { + "capture": Object { + "loadDelay": 1, + "maxAttempts": 1, + "timeouts": Object { + "openUrl": 100, + "renderComplete": 100, + "waitForElements": 100, + }, + "zoom": 1, + }, "csv": Object {}, "encryptionKey": "iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii", "index": ".reporting", diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index a4e4f43f90e1e..ee50b99e5b3b7 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -82,6 +82,8 @@ export class ReportingCore { public getContract: () => ReportingSetup; + private kibanaShuttingDown$ = new Rx.ReplaySubject(1); + constructor(private logger: Logger, context: PluginInitializerContext) { this.packageInfo = context.env.packageInfo; const syncConfig = context.config.get(); @@ -129,6 +131,14 @@ export class ReportingCore { await Promise.all([executeTask.init(taskManager), monitorTask.init(taskManager)]); } + public pluginStop() { + this.kibanaShuttingDown$.next(); + } + + public getKibanaShutdown$(): Rx.Observable { + return this.kibanaShuttingDown$.pipe(take(1)); + } + private async assertKibanaIsAvailable(): Promise { const { status } = this.getPluginSetupDeps(); diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts index 66ee465ea142f..37ec0d400f473 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts @@ -847,7 +847,7 @@ describe('error codes', () => { ); const { error_code: errorCode, warnings } = await generateCsv.generateData(); - expect(errorCode).toBe('authentication_expired'); + expect(errorCode).toBe('authentication_expired_error'); expect(warnings).toMatchInlineSnapshot(` Array [ "This report contains partial CSV results because the authentication token expired. Export a smaller amount of data or increase the timeout of the authentication token.", diff --git a/x-pack/plugins/reporting/server/lib/tasks/execute_report.test.ts b/x-pack/plugins/reporting/server/lib/tasks/execute_report.test.ts index b47df99b7a0fd..9f016a6a54a10 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/execute_report.test.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/execute_report.test.ts @@ -9,6 +9,8 @@ import { loggingSystemMock } from 'src/core/server/mocks'; import { ReportingCore } from '../..'; import { RunContext } from '../../../../task_manager/server'; import { taskManagerMock } from '../../../../task_manager/server/mocks'; +import { KibanaShuttingDownError } from '../../../common/errors'; +import type { SavedReport } from '../store'; import { ReportingConfigType } from '../../config'; import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import { ExecuteReportTask } from './'; @@ -79,4 +81,45 @@ describe('Execute Report Task', () => { } `); }); + + it('throws during reporting if Kibana starts shutting down', async () => { + mockReporting.getExportTypesRegistry().register({ + id: 'noop', + name: 'Noop', + createJobFnFactory: () => async () => new Promise(() => {}), + runTaskFnFactory: () => async () => new Promise(() => {}), + jobContentExtension: 'none', + jobType: 'noop', + validLicenses: [], + }); + const store = await mockReporting.getStore(); + store.setReportFailed = jest.fn(() => Promise.resolve({} as any)); + const task = new ExecuteReportTask(mockReporting, configType, logger); + task._claimJob = jest.fn(() => + Promise.resolve({ _id: 'test', jobtype: 'noop', status: 'pending' } as SavedReport) + ); + const mockTaskManager = taskManagerMock.createStart(); + await task.init(mockTaskManager); + + const taskDef = task.getTaskDefinition(); + const taskRunner = taskDef.createTaskRunner({ + taskInstance: { + id: 'random-task-id', + params: { index: 'cool-reporting-index', id: 'noop', jobtype: 'noop', payload: {} }, + }, + } as unknown as RunContext); + + const taskPromise = taskRunner.run(); + setImmediate(() => { + mockReporting.pluginStop(); + }); + await taskPromise; + + expect(store.setReportFailed).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + output: expect.objectContaining({ error_code: new KibanaShuttingDownError().code }), + }) + ); + }); }); diff --git a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts index 4d4959eef00c4..9007f5701f6f9 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts @@ -20,7 +20,12 @@ import type { TaskRunCreatorFunction, } from '../../../../task_manager/server'; import { CancellationToken } from '../../../common/cancellation_token'; -import { QueueTimeoutError, ReportingError, UnknownError } from '../../../common/errors'; +import { + ReportingError, + UnknownError, + QueueTimeoutError, + KibanaShuttingDownError, +} from '../../../common/errors'; import { durationToNumber, numberToDuration } from '../../../common/schema_utils'; import type { ReportOutput } from '../../../common/types'; import type { ReportingConfigType } from '../../config'; @@ -288,6 +293,12 @@ export class ExecuteReportTask implements ReportingTask { return report; } + // Generic is used to let TS infer the return type at call site. + private async throwIfKibanaShutsDown(): Promise { + await this.reporting.getKibanaShutdown$().toPromise(); + throw new KibanaShuttingDownError(); + } + /* * Provides a TaskRunner for Task Manager */ @@ -361,7 +372,10 @@ export class ExecuteReportTask implements ReportingTask { eventLog.logExecutionStart(); - const output = await this._performJob(task, cancellationToken, stream); + const output = await Promise.race([ + this._performJob(task, cancellationToken, stream), + this.throwIfKibanaShutsDown(), + ]); stream.end(); diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index 37d6494f5e079..52a72e7139020 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -113,4 +113,8 @@ export class ReportingPlugin return reportingCore.getContract(); } + + stop() { + this.reportingCore?.pluginStop(); + } } diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts index e00ebd99f0420..414d3dd10f8ff 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts @@ -119,6 +119,16 @@ export const createMockConfigSchema = ( enabled: false, ...overrides.roles, }, + capture: { + maxAttempts: 1, + loadDelay: 1, + timeouts: { + openUrl: 100, + renderComplete: 100, + waitForElements: 100, + }, + zoom: 1, + }, } as ReportingConfigType; };