[Reporting] Capture Kibana stopped error (#127017)

* added new error and error code

* updated error code of auth error and added a test

* added a way to observe Kibana shutdown via ReportingCore

* added a test for execute report during Kibana shutdown

* observe kibana shutdown while performing job and updated report mocks

* hook up reporting plugin to "stop" phase of reporting plugin

* rather use code from KibanaShuttingDownError

* remove done TODO

* fix jest test snapshots that are now failing
This commit is contained in:
Jean-Louis Leysens 2022-03-09 10:52:06 +01:00 committed by GitHub
parent 06e453ef78
commit 030816d5d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 116 additions and 11 deletions

View file

@ -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$/);
});
});
});

View file

@ -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

View file

@ -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",

View file

@ -82,6 +82,8 @@ export class ReportingCore {
public getContract: () => ReportingSetup;
private kibanaShuttingDown$ = new Rx.ReplaySubject<void>(1);
constructor(private logger: Logger, context: PluginInitializerContext<ReportingConfigType>) {
this.packageInfo = context.env.packageInfo;
const syncConfig = context.config.get<ReportingConfigType>();
@ -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<void> {
return this.kibanaShuttingDown$.pipe(take(1));
}
private async assertKibanaIsAvailable(): Promise<void> {
const { status } = this.getPluginSetupDeps();

View file

@ -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.",

View file

@ -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 }),
})
);
});
});

View file

@ -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<T>(): Promise<T> {
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<TaskRunResult>([
this._performJob(task, cancellationToken, stream),
this.throwIfKibanaShutsDown(),
]);
stream.end();

View file

@ -113,4 +113,8 @@ export class ReportingPlugin
return reportingCore.getContract();
}
stop() {
this.reportingCore?.pluginStop();
}
}

View file

@ -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;
};