package com.tesora.dve.sql; /* * #%L * Tesora Inc. * Database Virtualization Engine * %% * Copyright (C) 2011 - 2014 Tesora Inc. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * 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/>. * #L% */ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.sql.SQLException; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import org.junit.*; import com.tesora.dve.resultset.ResultColumn; import com.tesora.dve.resultset.ResultRow; import com.tesora.dve.server.bootstrap.BootstrapHost; import com.tesora.dve.sql.util.DBHelperConnectionResource; import com.tesora.dve.sql.util.PEDDL; import com.tesora.dve.sql.util.PortalDBHelperConnectionResource; import com.tesora.dve.sql.util.ProjectDDL; import com.tesora.dve.sql.util.StorageGroupDDL; import com.tesora.dve.standalone.PETest; public class KillTest extends SchemaTest { private static final ProjectDDL checkDDL = new PEDDL("checkdb", new StorageGroupDDL("check", 2, "checkg"), "schema"); private static final String MSG_QUERY_INTERRUPTED = "Query execution was interrupted"; @BeforeClass public static void setup() throws Exception { PETest.projectSetup(checkDDL); PETest.bootHost = BootstrapHost.startServices(PETest.class); } private DBHelperConnectionResource nativeConn; private DBHelperConnectionResource clientConn; private ExecutorService executor; @Before public void connect() throws Throwable { nativeConn = new DBHelperConnectionResource(); clientConn = new PortalDBHelperConnectionResource(); checkDDL.create(clientConn); executor = Executors.newSingleThreadExecutor(); } @After public void disconnect() throws Throwable { nativeConn.disconnect(); nativeConn = null; checkDDL.destroy(clientConn); clientConn.disconnect(); clientConn = null; executor.shutdownNow(); executor.awaitTermination(10, TimeUnit.SECONDS); executor = null; } @Test public void testInvalidConnectionId() throws Throwable { try { clientConn.execute("KILL QUERY 12345"); } catch (SQLException e) { assertTrue(e.getMessage().contains("Unknown thread id: 12345")); } } @Test public void testKillQuery() throws Throwable { if (Boolean.getBoolean("disableTestKillQuery")) return; clientConn.execute("CREATE TABLE `killtest` (`id` int NOT NULL)"); clientConn.execute("INSERT INTO checkdb.killtest (`id`) VALUES (123)"); try (final DBHelperConnectionResource client2 = new PortalDBHelperConnectionResource()) { ExpectedFailureTask blockedQuery = new ExpectedFailureTask() { @Override protected void execute() throws Throwable { try { client2.execute("SELECT id FROM checkdb.killtest"); } catch (java.sql.SQLException e) { assertTrue("Wrong exception, expected '" + MSG_QUERY_INTERRUPTED + "', got: " + e.getMessage(), e.getMessage().contains(MSG_QUERY_INTERRUPTED)); } } }; try { // lock table to hang SELECT nativeConn.execute("LOCK TABLES check0_checkdb.killtest WRITE"); Future<Void> future = executor.submit(blockedQuery); waitForQueryToBlock(); // kill the hung query clientConn.execute("KILL QUERY " + getLastThreadId(clientConn)); future.get(20, TimeUnit.SECONDS); // release locks, query again -- confirm connection is still live nativeConn.execute("UNLOCK TABLES"); future = executor.submit(new ExpectedFailureTask() { @Override protected void execute() throws Throwable { assertEquals(client2.execute("SELECT id FROM checkdb.killtest").getResults().size(), 1); } }); future.get(20, TimeUnit.SECONDS); } finally { nativeConn.execute("UNLOCK TABLES"); } } } @Test public void testKillIdleConnection() throws Throwable { try (DBHelperConnectionResource client2 = new PortalDBHelperConnectionResource()) { client2.execute("SELECT version()"); clientConn.execute("KILL CONNECTION " + getLastThreadId(clientConn)); try { // confirm connection was killed client2.execute("SELECT version()"); } catch (Exception e) { assertTrue("Wrong exception", findExceptionsOfType(java.io.EOFException.class, e).size() > 0); } } } @Test public void testKillBlockedConnection() throws Throwable { clientConn.execute("CREATE TABLE `killtest` (`id` int NOT NULL)"); clientConn.execute("INSERT INTO checkdb.killtest (`id`) VALUES (123)"); try (final DBHelperConnectionResource client2 = new PortalDBHelperConnectionResource()) { ExpectedFailureTask blockedQuery = new ExpectedFailureTask() { @Override protected void execute() throws Throwable { try { client2.execute("SELECT id FROM checkdb.killtest"); } catch (java.sql.SQLException e) { assertTrue(e.getMessage().contains(MSG_QUERY_INTERRUPTED)); } } }; try { nativeConn.execute("LOCK TABLES check0_checkdb.killtest WRITE"); Future<Void> future = executor.submit(blockedQuery); waitForQueryToBlock(); clientConn.execute("KILL " /* CONNECTION is optional here */+ getLastThreadId(clientConn)); future.get(20, TimeUnit.SECONDS); } finally { nativeConn.execute("UNLOCK TABLES"); } try { // confirm connection was killed client2.execute("SELECT id FROM checkdb.killtest"); } catch (Exception e) { assertTrue("Not the expected exception", findExceptionsOfType(java.io.EOFException.class, e).size() > 0); } } } private long getLastThreadId(DBHelperConnectionResource conn) throws Throwable { List<ResultRow> results = conn.execute("show processlist").getResults(); ResultRow lastRow = results.get(results.size() - 1); ResultColumn col = lastRow.getResultColumn(1); return ((Long) col.getColumnValue()).intValue(); } private void waitForQueryToBlock() throws Throwable { boolean blocked; do { blocked = checkProcessState(nativeConn, "Waiting for table metadata lock") || checkProcessState(nativeConn, "Waiting for schema metadata lock"); } while (!blocked); } private boolean checkProcessState(DBHelperConnectionResource conn, String state) throws Throwable { List<ResultRow> results = conn.execute("show processlist").getResults(); for (ResultRow row : results) { ResultColumn col = row.getResultColumn(7); if (state.equals(col.getColumnValue())) { return true; } } return false; } private static abstract class ExpectedFailureTask implements Callable<Void> { @Override public Void call() throws Exception { try { execute(); } catch (Exception | Error e) { throw e; } catch (Throwable t) { throw new Exception(t); } return null; } // do something and check for expected exception protected abstract void execute() throws Throwable; } }