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
This commit is contained in:
andreacyc 2021-10-05 13:28:49 -04:00 committed by GitHub
parent bc3b038712
commit f82baf174e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 133 additions and 3 deletions

View File

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

View File

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

View File

@ -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<number> {
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();
}
}

View File

@ -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<void> {
await page.click(`//*[text()="${text}"]`);
}
export async function selectSuggestibleInput(
page: playwright.Page,
label: string,

View File

@ -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 \
)
}

View File

@ -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<QueryViewProps, QueryViewStat
): Promise<QueryResult> => {
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<string, any> | 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,