/** * diqube: Distributed Query Base. * * Copyright (C) 2015 Bastian Gloeckle * * This file is part of diqube. * * diqube is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.diqube.itest.tests; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.StringReader; import java.util.UUID; import java.util.regex.Pattern; import org.diqube.context.Profiles; import org.diqube.execution.ExecutablePlan; import org.diqube.execution.steps.AbstractThreadedExecutablePlanStep; import org.diqube.execution.steps.ExecuteRemotePlanOnShardsStep; import org.diqube.itest.AbstractDiqubeIntegrationTest; import org.diqube.itest.annotations.NeedsProcessPid; import org.diqube.itest.annotations.NeedsServer; import org.diqube.itest.control.ServerControl; import org.diqube.itest.util.QueryResultServiceTestUtil; import org.diqube.itest.util.QueryResultServiceTestUtil.TestQueryResultService; import org.diqube.itest.util.TestDataGenerator; import org.diqube.itest.util.Waiter; import org.diqube.plan.ExecutionPlanBuilder; import org.diqube.plan.ExecutionPlanBuilderFactory; import org.diqube.remote.cluster.thrift.RExecutionPlan; import org.diqube.remote.query.thrift.QueryService; import org.diqube.server.ControlFileManager; import org.diqube.thrift.base.thrift.RUUID; import org.diqube.thrift.base.thrift.Ticket; import org.diqube.thrift.base.util.RUuidUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.testng.Assert; import org.testng.annotations.Test; /** * Tests that queries are actually cancelled on remotes, too, when * {@link QueryService.Iface#cancelQueryExecution(RUUID)} is called. * * @author Bastian Gloeckle */ public class QueryCancelIntegrationTest extends AbstractDiqubeIntegrationTest { private static final Logger logger = LoggerFactory.getLogger(QueryCancelIntegrationTest.class); private static final String BIG_TABLE = "big"; private static final String BIG_CONTROL_FILE = "/" + QueryCancelIntegrationTest.class.getSimpleName() + "/big" + ControlFileManager.CONTROL_FILE_EXTENSION; private static final String BIG2_CONTROL_FILE = "/" + QueryCancelIntegrationTest.class.getSimpleName() + "/big2" + ControlFileManager.CONTROL_FILE_EXTENSION; private static final String BIG_DATA_FILE_WORK = "big.json"; @Test @NeedsServer(servers = 2) @NeedsProcessPid public void queryGetsCancelledOnAllNodes() throws InterruptedException, FileNotFoundException, IOException { // GIVEN // a table which has enough data to make execution a bit longer. TestDataGenerator.generateJsonTestData(work(BIG_DATA_FILE_WORK), 10, 2, new String[] { "a", "b" }, 30); // WHEN serverControl.get(0).deploy(cp(BIG_CONTROL_FILE), work(BIG_DATA_FILE_WORK)); serverControl.get(1).deploy(cp(BIG2_CONTROL_FILE), work(BIG_DATA_FILE_WORK)); Ticket ticket = serverControl.get(0).loginSuperuser(); // THEN try (TestQueryResultService queryRes = QueryResultServiceTestUtil.createQueryResultService()) { String diqlQuery = "select avg(add(a[*].a[*], 1)), avg(add(a[*].b[*], 1)), avg(add(b[*].a[*], 1)) from " + BIG_TABLE; UUID queryUuid = UUID.randomUUID(); RUUID queryRUuid = RUuidUtil.toRUuid(queryUuid); logger.info("Executing query {}", RUuidUtil.toUuid(queryRUuid)); // execute a long-running query. serverControl.get(0).getSerivceTestUtil().queryService((queryService) -> queryService.asyncExecuteQuery(ticket, queryRUuid, diqlQuery, true, queryRes.getThisServicesAddr().toRNodeAddress())); new Waiter().waitUntil("Remote worker threads start showing up", 2, 300, () -> threadDumpContainsString(serverControl.get(0), "query-remote-worker-" + queryUuid.toString()) && // threadDumpContainsString(serverControl.get(1), "query-remote-worker-" + queryUuid.toString())); // now remotes are running. Cancel execution. logger.info("Canceling query {}", queryUuid); serverControl.get(0).getSerivceTestUtil() .queryService(queryService -> queryService.cancelQueryExecution(ticket, queryRUuid)); // now /all/ threads should be cancelled within a short amount of time, both the ones of query master and the ones // of query remotes. new Waiter().waitUntil("There are no more query threads", 20, 500, () -> !threadDumpContainsString(serverControl.get(0), queryUuid.toString()) && !threadDumpContainsString(serverControl.get(1), queryUuid.toString())); // now check that the remotes did not actually complete processing the plan (in which case the threads would be // gone, too). int numberOfRemoteSteps = calculateNumberOfRemoteSteps(diqlQuery); logger.info("Found that there are {} remote steps in the execution plan", numberOfRemoteSteps); String stepDoneRegex = AbstractThreadedExecutablePlanStep.STEP_IS_DONE_PROCESSING_LOG_PATTERN.replace("{}", ".*"); Pattern workerDonePattern = Pattern.compile(".*query-remote-worker-" + queryUuid.toString() + ".*" + stepDoneRegex); // validate first server cancelled. int completed = countLinesMatching(serverControl.get(0).getServerLogOutput(), workerDonePattern); logger.info("First server completed {} remote steps", completed); Assert.assertTrue(completed < numberOfRemoteSteps, "Expected that not all remote steps did complete computation. Number of remote steps: " + numberOfRemoteSteps + ", completed: " + completed); // assert that at least one step was completed -> this is mainly to ensure that the Regex is still valid. At least // the RowIdSinkStep should have completed, since that should complete at under a second. Assert.assertTrue(completed > 0, "Expected for at least one remote step to be completed, the regex might not be valid any more?"); // validate second server cancelled. completed = countLinesMatching(serverControl.get(1).getServerLogOutput(), workerDonePattern); logger.info("Second server completed {} remote steps", completed); Assert.assertTrue(completed < numberOfRemoteSteps, "Expected that not all remote steps did complete computation. Number of remote steps: " + numberOfRemoteSteps + ", completed: " + completed); logger.info("Successfully received exception and all threads of the query were shut down.", queryRes.getException()); } catch (IOException e) { throw new RuntimeException("Could not execute query", e); } } private boolean threadDumpContainsString(ServerControl serverControl, String searchString) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); serverControl.createThreadDump(baos); String s = baos.toString(); logger.info("Gathered thread dump of server process {}: {}", serverControl.getAddr(), s); return s.contains(searchString); } private int countLinesMatching(String input, Pattern pattern) { logger.trace("Starting to match log output to a pattern..."); try { BufferedReader reader = new BufferedReader(new StringReader(input)); return (int) reader.lines().parallel().filter(s -> pattern.matcher(s).matches()).count(); } finally { logger.trace("Done matching log output to a pattern."); } } private int calculateNumberOfRemoteSteps(String diql) { try (AnnotationConfigApplicationContext dataContext = new AnnotationConfigApplicationContext()) { dataContext.getEnvironment().setActiveProfiles(Profiles.UNIT_TEST); dataContext.scan("org.diqube"); dataContext.refresh(); ExecutionPlanBuilder planBuilder = dataContext.getBean(ExecutionPlanBuilderFactory.class).createExecutionPlanBuilder(); ExecutablePlan masterPlan = planBuilder.fromDiql(diql).build(); ExecuteRemotePlanOnShardsStep executeRemoteStep = (ExecuteRemotePlanOnShardsStep) masterPlan.getSteps().stream() .filter(step -> step instanceof ExecuteRemotePlanOnShardsStep).findAny().get(); RExecutionPlan remotePlan = executeRemoteStep.getRemoteExecutionPlan(); return remotePlan.getSteps().size(); } } }