diff --git a/kibana-reports/package.json b/kibana-reports/package.json index 077fccb7..06e28237 100644 --- a/kibana-reports/package.json +++ b/kibana-reports/package.json @@ -20,6 +20,7 @@ "plugin_helpers": "node ../../scripts/plugin_helpers" }, "dependencies": { + "async-mutex": "^0.2.6", "babel-polyfill": "^6.26.0", "cheerio": "0.22.0", "cron-validator": "^1.1.1", diff --git a/kibana-reports/public/components/context_menu/context_menu.js b/kibana-reports/public/components/context_menu/context_menu.js index b1e1ba8b..b46049c5 100644 --- a/kibana-reports/public/components/context_menu/context_menu.js +++ b/kibana-reports/public/components/context_menu/context_menu.js @@ -126,6 +126,8 @@ const generateInContextReport = ( } else { if (response.status === 403) { addSuccessOrFailureToast('permissionsFailure'); + } else if (response.status === 503) { + addSuccessOrFailureToast('timeoutFailure', reportSource); } else { addSuccessOrFailureToast('failure'); } diff --git a/kibana-reports/public/components/context_menu/context_menu_helpers.js b/kibana-reports/public/components/context_menu/context_menu_helpers.js index 27535ef0..fbc47fd4 100644 --- a/kibana-reports/public/components/context_menu/context_menu_helpers.js +++ b/kibana-reports/public/components/context_menu/context_menu_helpers.js @@ -86,7 +86,7 @@ export const displayLoadingModal = () => { } }; -export const addSuccessOrFailureToast = (status) => { +export const addSuccessOrFailureToast = (status, reportSource) => { const generateToast = document.querySelectorAll('.euiGlobalToastList'); if (generateToast) { try { @@ -101,6 +101,14 @@ export const addSuccessOrFailureToast = (status) => { setTimeout(function () { document.getElementById('reportFailureToast').style.display = 'none'; }, 6000); + } else if (status === 'timeoutFailure') { + generateInProgressToast.innerHTML = reportGenerationFailure( + 'Error generating report.', + `Timed out generating on-demand report from ${reportSource}. Try again later.` + ); + setTimeout(function () { + document.getElementById('reportFailureToast').style.display = 'none'; + }, 6000); } else if (status === 'permissionsFailure') { generateInProgressToast.innerHTML = permissionsMissingOnGeneration(); setTimeout(function () { diff --git a/kibana-reports/public/components/context_menu/context_menu_ui.js b/kibana-reports/public/components/context_menu/context_menu_ui.js index a61e1976..184f7639 100644 --- a/kibana-reports/public/components/context_menu/context_menu_ui.js +++ b/kibana-reports/public/components/context_menu/context_menu_ui.js @@ -244,7 +244,10 @@ export const reportGenerationSuccess = () => { `; }; -export const reportGenerationFailure = () => { +export const reportGenerationFailure = ( + title = 'Download error', + text = 'There was an error generating this report.' +) => { return `

A new notification appears

@@ -255,7 +258,7 @@ export const reportGenerationFailure = () => { role="img" aria-hidden="true"> - Download error + ${title}
-

There was an error generating this report.

+

${text}

`; diff --git a/kibana-reports/public/components/main/__tests__/main_utils.test.tsx b/kibana-reports/public/components/main/__tests__/main_utils.test.tsx index 3386061a..27e0b7d8 100644 --- a/kibana-reports/public/components/main/__tests__/main_utils.test.tsx +++ b/kibana-reports/public/components/main/__tests__/main_utils.test.tsx @@ -129,4 +129,28 @@ describe('main_utils tests', () => { handleErrorToast ); }); + + test('test generateReportById timeout error handling', async () => { + expect.assertions(1); + const reportId = '1'; + const handleSuccessToast = jest.fn(); + const handleErrorToast = jest.fn(); + const handlePermissionsMissingToast = jest.fn(); + + httpClientMock.get.mockReturnValue( + Promise.reject({ body: { statusCode: 503 } }) + ); + + await generateReportById( + reportId, + httpClientMock, + handleSuccessToast, + handleErrorToast, + handlePermissionsMissingToast + ); + expect(handleErrorToast).toHaveBeenCalledWith( + 'Error generating report.', + 'Timed out generating report ID 1. Try again later.' + ); + }); }); diff --git a/kibana-reports/public/components/main/main.tsx b/kibana-reports/public/components/main/main.tsx index 72e8b76d..9f2c5545 100644 --- a/kibana-reports/public/components/main/main.tsx +++ b/kibana-reports/public/components/main/main.tsx @@ -103,9 +103,10 @@ export function Main(props) { addReportDefinitionsTableErrorToastHandler(errorType); }; - const addErrorOnDemandDownloadToastHandler = () => { + const addErrorOnDemandDownloadToastHandler = (title = 'Error downloading report.', text = '') => { const errorToast = { - title: 'Error downloading report.', + title, + text, color: 'danger', iconType: 'alert', id: 'onDemandDownloadErrorToast', @@ -113,8 +114,8 @@ export function Main(props) { setToasts(toasts.concat(errorToast)); }; - const handleOnDemandDownloadErrorToast = () => { - addErrorOnDemandDownloadToastHandler(); + const handleOnDemandDownloadErrorToast = (title?: string, text?: string) => { + addErrorOnDemandDownloadToastHandler(title, text); }; const addSuccessOnDemandDownloadToastHandler = () => { diff --git a/kibana-reports/public/components/main/main_utils.tsx b/kibana-reports/public/components/main/main_utils.tsx index 1405533f..2fa4f414 100644 --- a/kibana-reports/public/components/main/main_utils.tsx +++ b/kibana-reports/public/components/main/main_utils.tsx @@ -198,6 +198,11 @@ export const generateReportById = async ( console.log('error on generating report by id:', error); if (error.body.statusCode === 403) { handlePermissionsMissingToast(); + } else if (error.body.statusCode === 503) { + handleErrorToast( + 'Error generating report.', + `Timed out generating report ID ${reportId}. Try again later.` + ); } else { handleErrorToast(); } diff --git a/kibana-reports/public/components/main/report_details/report_details.tsx b/kibana-reports/public/components/main/report_details/report_details.tsx index 3f597988..158556eb 100644 --- a/kibana-reports/public/components/main/report_details/report_details.tsx +++ b/kibana-reports/public/components/main/report_details/report_details.tsx @@ -94,9 +94,10 @@ export function ReportDetails(props) { addPermissionsMissingDownloadToastHandler(); } - const addErrorToastHandler = () => { + const addErrorToastHandler = (title = 'Error loading report details.', text = '') => { const errorToast = { - title: 'Error loading report details.', + title, + text, color: 'danger', iconType: 'alert', id: 'reportDetailsErrorToast', @@ -104,8 +105,8 @@ export function ReportDetails(props) { setToasts(toasts.concat(errorToast)); }; - const handleErrorToast = () => { - addErrorToastHandler(); + const handleErrorToast = (title?: string, text?: string) => { + addErrorToastHandler(title, text); }; const addSuccessToastHandler = () => { diff --git a/kibana-reports/server/plugin.ts b/kibana-reports/server/plugin.ts index e0cd6e76..e6ddda63 100644 --- a/kibana-reports/server/plugin.ts +++ b/kibana-reports/server/plugin.ts @@ -22,6 +22,7 @@ import { ILegacyClusterClient, } from '../../../src/core/server'; import { setIntervalAsync } from 'set-interval-async/dynamic'; +import { Semaphore, SemaphoreInterface, withTimeout } from 'async-mutex'; import esReportsPlugin from './backend/opendistro-es-reports-plugin'; import notificationPlugin from './backend/opendistro-notification-plugin'; import { @@ -50,9 +51,14 @@ export class OpendistroKibanaReportsPlugin OpendistroKibanaReportsPluginStart > { private readonly logger: Logger; + private readonly semaphore: SemaphoreInterface; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); + + const timeoutError = new Error('Server busy'); + timeoutError.statusCode = 503; + this.semaphore = withTimeout(new Semaphore(1), 180000, timeoutError); } public setup(core: CoreSetup) { @@ -83,6 +89,7 @@ export class OpendistroKibanaReportsPlugin (context, request) => { return { logger: this.logger, + semaphore: this.semaphore, notificationClient, esReportsClient, }; diff --git a/kibana-reports/server/routes/lib/createReport.ts b/kibana-reports/server/routes/lib/createReport.ts index 5fed5a9c..e11f4460 100644 --- a/kibana-reports/server/routes/lib/createReport.ts +++ b/kibana-reports/server/routes/lib/createReport.ts @@ -35,6 +35,7 @@ import { SetCookie } from 'puppeteer-core'; import { deliverReport } from './deliverReport'; import { updateReportState } from './updateReportState'; import { saveReport } from './saveReport'; +import { SemaphoreInterface } from 'async-mutex'; export const createReport = async ( request: KibanaRequest, @@ -45,6 +46,8 @@ export const createReport = async ( const isScheduledTask = false; //@ts-ignore const logger: Logger = context.reporting_plugin.logger; + //@ts-ignore + const semaphore: SemaphoreInterface = context.reporting_plugin.semaphore; // @ts-ignore const notificationClient: ILegacyScopedClusterClient = context.reporting_plugin.notificationClient.asScoped( request @@ -102,13 +105,18 @@ export const createReport = async ( } }); } - createReportResult = await createVisualReport( - reportParams, - completeQueryUrl, - logger, - cookieObject, - timezone - ); + const [value, release] = await semaphore.acquire(); + try { + createReportResult = await createVisualReport( + reportParams, + completeQueryUrl, + logger, + cookieObject, + timezone + ); + } finally { + release(); + } } // update report state to "created" // TODO: temporarily remove the following diff --git a/kibana-reports/yarn.lock b/kibana-reports/yarn.lock index 46fe5d46..7ed15bdd 100644 --- a/kibana-reports/yarn.lock +++ b/kibana-reports/yarn.lock @@ -1041,6 +1041,13 @@ async-limiter@~1.0.0: resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== +async-mutex@^0.2.6: + version "0.2.6" + resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.2.6.tgz#0d7a3deb978bc2b984d5908a2038e1ae2e54ff40" + integrity sha512-Hs4R+4SPgamu6rSGW8C7cV9gaWUKEHykfzCCvIRuaVv636Ju10ZdeUbvb4TBEW0INuq2DHZqXbK4Nd3yG4RaRw== + dependencies: + tslib "^2.0.0" + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -5453,6 +5460,11 @@ tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" + integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== + tty-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"