From f82baf174e230c6f336096ad62fa81a2f2fa6b91 Mon Sep 17 00:00:00 2001 From: andreacyc <90633413+andreacyc@users.noreply.github.com> Date: Tue, 5 Oct 2021 13:28:49 -0400 Subject: [PATCH] Support real query cancelling for web console (#11738) * Support real query cancelling for web console * use uuid for queryId, create isSql reuse variable, and add catch for rejectionhandled promise * remove delete api promise.then() response * slove conflicts * update read me with debug * add degub code to test why CI failed * included a druid extension called druid-testing-tools and it is not build nor loaded by default * remove unuse variable * remove debug log --- web-console/README.md | 7 +++ web-console/e2e-tests/cancel-query.spec.ts | 61 +++++++++++++++++++ .../e2e-tests/component/query/overview.ts | 39 +++++++++++- web-console/e2e-tests/util/playwright.ts | 4 ++ web-console/script/druid | 5 +- .../src/views/query-view/query-view.tsx | 20 +++++- 6 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 web-console/e2e-tests/cancel-query.spec.ts diff --git a/web-console/README.md b/web-console/README.md index 9e5bd4752bb..a92908377c6 100644 --- a/web-console/README.md +++ b/web-console/README.md @@ -115,6 +115,13 @@ The environment variable `DRUID_E2E_TEST_UNIFIED_CONSOLE_PORT` can be used to ta non-default port (i.e., not port `8888`). For example, this environment variable can be used to target the development mode of the web console (started via `npm start`), which runs on port `18081`. +Like so: `DRUID_E2E_TEST_UNIFIED_CONSOLE_PORT=18081 npm run test-e2e` + +#### Running and debugging a single e2e test using Jest and Playwright + +- Run - `jest --config jest.e2e.config.js e2e-tests/tutorial-batch.spec.ts`. +- Debug - `PWDEBUG=console jest --config jest.e2e.config.js e2e-tests/tutorial-batch.spec.ts`. + ## Description of the directory structure As part of this directory: diff --git a/web-console/e2e-tests/cancel-query.spec.ts b/web-console/e2e-tests/cancel-query.spec.ts new file mode 100644 index 00000000000..fc6822e46a1 --- /dev/null +++ b/web-console/e2e-tests/cancel-query.spec.ts @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as playwright from 'playwright-chromium'; + +import { QueryOverview } from './component/query/overview'; +import { saveScreenshotIfError } from './util/debug'; +import { UNIFIED_CONSOLE_URL } from './util/druid'; +import { createBrowser, createPage } from './util/playwright'; +import { waitTillWebConsoleReady } from './util/setup'; + +jest.setTimeout(5 * 60 * 1000); + +describe('Cancel query', () => { + let browser: playwright.Browser; + let page: playwright.Page; + + beforeAll(async () => { + await waitTillWebConsoleReady(); + browser = await createBrowser(); + }); + + beforeEach(async () => { + page = await createPage(browser); + }); + + afterAll(async () => { + await browser.close(); + }); + + it('delete accepted', async () => { + const testName = 'cancel-query'; + await saveScreenshotIfError(testName, page, async () => { + await validateCancelQuery(page); + }); + }); +}); + +async function validateCancelQuery(page: playwright.Page) { + const queryOverview = new QueryOverview(page, UNIFIED_CONSOLE_URL); + const query = 'SELECT sleep(40)'; + const results = await queryOverview.cancelQuery(query); + expect(results).toBeDefined(); + expect(results).toBeGreaterThan(0); + expect(results).toStrictEqual(202); +} diff --git a/web-console/e2e-tests/component/query/overview.ts b/web-console/e2e-tests/component/query/overview.ts index d3b8986418d..6feeebfa9c0 100644 --- a/web-console/e2e-tests/component/query/overview.ts +++ b/web-console/e2e-tests/component/query/overview.ts @@ -18,7 +18,7 @@ import * as playwright from 'playwright-chromium'; -import { clickButton, setInput } from '../../util/playwright'; +import { clickButton, clickText, setInput } from '../../util/playwright'; import { extractTable } from '../../util/table'; /** @@ -44,4 +44,41 @@ export class QueryOverview { return await extractTable(this.page, 'div.query-output div.rt-tr-group', 'div.rt-td'); } + + async cancelQuery(query: string): Promise { + await this.page.goto(this.baseUrl); + await this.page.reload({ waitUntil: 'networkidle' }); + + await this.page.waitForSelector('div.query-input textarea'); + const input = await this.page.$('div.query-input textarea'); + + await setInput(input!, query); + + await Promise.all([ + this.page.waitForRequest( + request => request.url().includes('druid/v2') && request.method() === 'POST', + ), + clickButton(this.page, 'Run'), + ]); + + await this.page.waitForSelector('.cancel-label'); + + const [resp] = await Promise.all([ + this.page.waitForResponse( + response => response.url().includes('druid/v2') && response.request().method() === 'DELETE', + ), + + clickText(this.page, 'Cancel query'), + this.page.off( + 'requestfinished', + request => request.url().includes('druid/v2') && request.method() === 'POST', + ), + this.page.off( + 'requestfinished', + request => request.url().includes('druid/v2') && request.method() === 'DELETE', + ), + ]); + + return resp.status(); + } } diff --git a/web-console/e2e-tests/util/playwright.ts b/web-console/e2e-tests/util/playwright.ts index 732cae81014..c0591342b7a 100644 --- a/web-console/e2e-tests/util/playwright.ts +++ b/web-console/e2e-tests/util/playwright.ts @@ -100,6 +100,10 @@ export async function clickLabeledButton( await page.click(`//*[text()="${label}"]/following-sibling::div${buttonSelector(text)}`); } +export async function clickText(page: playwright.Page, text: string): Promise { + await page.click(`//*[text()="${text}"]`); +} + export async function selectSuggestibleInput( page: playwright.Page, label: string, diff --git a/web-console/script/druid b/web-console/script/druid index e57a90f114e..2be7f386198 100755 --- a/web-console/script/druid +++ b/web-console/script/druid @@ -59,7 +59,10 @@ function _build_distribution() { && mvn -Pdist,skip-static-checks,skip-tests -Dmaven.javadoc.skip=true -q -T1C install \ && cd distribution/target \ && tar xzf "apache-druid-$(_get_druid_version)-bin.tar.gz" \ - && echo -e "\n\ndruid.server.http.allowedHttpMethods=[\"HEAD\"]" >> apache-druid-$(_get_druid_version)/conf/druid/single-server/micro-quickstart/_common/common.runtime.properties + && cd apache-druid-$(_get_druid_version) \ + && java -classpath "lib/*" org.apache.druid.cli.Main tools pull-deps -c org.apache.druid.extensions:druid-testing-tools \ + && echo -e "\n\ndruid.extensions.loadList=[\"druid-hdfs-storage\", \"druid-kafka-indexing-service\", \"druid-datasketches\", \"druid-testing-tools\"]" >> conf/druid/single-server/micro-quickstart/_common/common.runtime.properties \ + && echo -e "\n\ndruid.server.http.allowedHttpMethods=[\"HEAD\"]" >> conf/druid/single-server/micro-quickstart/_common/common.runtime.properties \ ) } diff --git a/web-console/src/views/query-view/query-view.tsx b/web-console/src/views/query-view/query-view.tsx index 4ba6f703bef..d8ca9357e0c 100644 --- a/web-console/src/views/query-view/query-view.tsx +++ b/web-console/src/views/query-view/query-view.tsx @@ -25,6 +25,7 @@ import * as JSONBig from 'json-bigint-native'; import memoizeOne from 'memoize-one'; import React, { RefObject } from 'react'; import SplitterLayout from 'react-splitter-layout'; +import { v4 as uuidv4 } from 'uuid'; import { Loader } from '../../components'; import { EditContextDialog } from '../../dialogs/edit-context-dialog/edit-context-dialog'; @@ -203,16 +204,33 @@ export class QueryView extends React.PureComponent => { const { queryString, queryContext, wrapQueryLimit } = queryWithContext; - const query = QueryView.isJsonLike(queryString) ? Hjson.parse(queryString) : queryString; + const isSql = !QueryView.isJsonLike(queryString); + + const query = isSql ? queryString : Hjson.parse(queryString); + + const queryId = uuidv4(); let context: Record | undefined; if (!isEmptyContext(queryContext) || wrapQueryLimit || mandatoryQueryContext) { context = { ...queryContext, ...(mandatoryQueryContext || {}) }; + + if (isSql) { + context.sqlQueryId = queryId; + } else { + context.queryId = queryId; + } + if (typeof wrapQueryLimit !== 'undefined') { context.sqlOuterLimit = wrapQueryLimit + 1; } } + void cancelToken.promise + .then(() => { + return Api.instance.delete(`/druid/v2${isSql ? '/sql' : ''}/${queryId}`); + }) + .catch(() => {}); + try { return await queryRunner.runQuery({ query,