blob: c0332f8021e5f0f36b2de4b1e8b06a7176536d22 [file] [log] [blame]
/*
* 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.
*/
package org.apache.phoenix.end2end;
import static org.apache.phoenix.query.BaseTest.setUpConfigForMiniCluster;
import static org.apache.phoenix.util.TestUtil.TEST_PROPERTIES;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assume.assumeFalse;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.Collection;
import java.util.List;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.HBaseTestingUtility;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Admin;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.apache.hadoop.hbase.util.VersionInfo;
import org.apache.phoenix.coprocessor.MetaDataProtocol;
import org.apache.phoenix.jdbc.PhoenixDatabaseMetaData;
import org.apache.phoenix.jdbc.PhoenixDriver;
import org.apache.phoenix.query.QueryConstants;
import org.apache.phoenix.query.QueryServices;
import org.apache.phoenix.query.QueryServicesOptions;
import org.apache.phoenix.util.PhoenixRuntime;
import org.apache.phoenix.util.PropertiesUtil;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import com.google.common.collect.Lists;
/**
* This class is meant for testing all compatible client versions
* against the current server version. It runs SQL queries with given
* client versions and compares the output against gold files
*/
@RunWith(Parameterized.class)
@Category(NeedsOwnMiniClusterTest.class)
public class BackwardCompatibilityIT {
private static final String SQL_DIR = "sql_files/";
private static final String RESULTS_AND_GOLD_FILES_DIR = "gold_files/";
private static final String COMPATIBLE_CLIENTS_JSON =
"compatible_client_versions.json";
private static final String BASH = "/bin/bash";
private static final String EXECUTE_QUERY_SH = "scripts/execute_query.sh";
private static final String QUERY_PREFIX = "query_";
private static final String RESULT_PREFIX = "result_";
private static final String GOLD_PREFIX = "gold_";
private static final String SQL_EXTENSION = ".sql";
private static final String TEXT_EXTENSION = ".txt";
private static final String CREATE_ADD = "create_add";
private static final String CREATE_DIVERGED_VIEW = "create_diverged_view";
private static final String ADD_DATA = "add_data";
private static final String ADD_DELETE = "add_delete";
private static final String QUERY_CREATE_ADD = QUERY_PREFIX + CREATE_ADD;
private static final String QUERY_ADD_DATA = QUERY_PREFIX + ADD_DATA;
private static final String QUERY_ADD_DELETE = QUERY_PREFIX + ADD_DELETE;
private static final String QUERY_CREATE_DIVERGED_VIEW = QUERY_PREFIX + CREATE_DIVERGED_VIEW;
private static final String MVN_HOME = "maven.home";
private static final String JAVA_TMP_DIR = "java.io.tmpdir";
private final String compatibleClientVersion;
private static Configuration conf;
private static HBaseTestingUtility hbaseTestUtil;
private static String zkQuorum;
private static String url;
private String tmpDir;
public BackwardCompatibilityIT(String compatibleClientVersion) {
this.compatibleClientVersion = compatibleClientVersion;
}
@Parameters(name = "BackwardCompatibilityIT_compatibleClientVersion={0}")
public static synchronized Collection<String> data() throws Exception {
return computeClientVersions();
}
@Before
public synchronized void doSetup() throws Exception {
tmpDir = System.getProperty("java.io.tmpdir");
conf = HBaseConfiguration.create();
hbaseTestUtil = new HBaseTestingUtility(conf);
setUpConfigForMiniCluster(conf);
conf.set(QueryServices.EXTRA_JDBC_ARGUMENTS_ATTRIB, QueryServicesOptions.DEFAULT_EXTRA_JDBC_ARGUMENTS);
hbaseTestUtil.startMiniCluster();
zkQuorum = "localhost:" + hbaseTestUtil.getZkCluster().getClientPort();
url = PhoenixRuntime.JDBC_PROTOCOL + PhoenixRuntime.JDBC_PROTOCOL_SEPARATOR + zkQuorum;
DriverManager.registerDriver(PhoenixDriver.INSTANCE);
checkForPreConditions();
}
@After
public void cleanUpAfterTest() throws Exception {
try {
DriverManager.deregisterDriver(PhoenixDriver.INSTANCE);
} finally {
hbaseTestUtil.shutdownMiniCluster();
}
System.setProperty("java.io.tmpdir", tmpDir);
}
private static List<String> computeClientVersions() throws Exception {
String hbaseVersion = VersionInfo.getVersion();
Pattern p = Pattern.compile("\\d+\\.\\d+");
Matcher m = p.matcher(hbaseVersion);
String hbaseProfile = null;
if (m.find()) {
hbaseProfile = m.group();
}
List<String> clientVersions = Lists.newArrayList();
ObjectMapper mapper = new ObjectMapper();
mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
try (InputStream inputStream = BackwardCompatibilityIT.class
.getClassLoader().getResourceAsStream(COMPATIBLE_CLIENTS_JSON)) {
assertNotNull(inputStream);
JsonNode jsonNode = mapper.readTree(inputStream);
JsonNode HBaseProfile = jsonNode.get(hbaseProfile);
for (final JsonNode clientVersion : HBaseProfile) {
clientVersions.add(clientVersion.textValue() + "-HBase-" + hbaseProfile);
}
}
return clientVersions;
}
/**
* Scenario:
* 1. Old Client connects to the updated server
* 2. Old Client creates tables and inserts data
* 3. New Client reads the data inserted by the old client
*
* @throws Exception thrown if any errors encountered during query execution or file IO
*/
@Test
public void testUpsertWithOldClient() throws Exception {
// Insert data with old client and read with new client
executeQueryWithClientVersion(compatibleClientVersion, CREATE_ADD);
executeQueriesWithCurrentVersion(QUERY_CREATE_ADD);
assertExpectedOutput(QUERY_CREATE_ADD);
}
@Test
public void testCreateDivergedViewWithOldClientReadFromNewClient() throws Exception {
// Create a base table, view and make it diverge from an old client
executeQueryWithClientVersion(compatibleClientVersion, CREATE_DIVERGED_VIEW);
executeQueriesWithCurrentVersion(QUERY_CREATE_DIVERGED_VIEW);
assertExpectedOutput(QUERY_CREATE_DIVERGED_VIEW);
}
@Test
public void testCreateDivergedViewWithOldClientReadFromOldClient() throws Exception {
// Create a base table, view and make it diverge from an old client
executeQueryWithClientVersion(compatibleClientVersion, CREATE_DIVERGED_VIEW);
executeQueryWithClientVersion(compatibleClientVersion, QUERY_CREATE_DIVERGED_VIEW);
assertExpectedOutput(QUERY_CREATE_DIVERGED_VIEW);
}
@Test
public void testCreateDivergedViewWithOldClientReadFromOldClientAfterUpgrade()
throws Exception {
// Create a base table, view and make it diverge from an old client
executeQueryWithClientVersion(compatibleClientVersion, CREATE_DIVERGED_VIEW);
try (Connection conn = DriverManager.getConnection(url)) {
// Just connect with a new client to cause a metadata upgrade
}
// Query with an old client again
executeQueryWithClientVersion(compatibleClientVersion, QUERY_CREATE_DIVERGED_VIEW);
assertExpectedOutput(QUERY_CREATE_DIVERGED_VIEW);
}
@Test
public void testCreateDivergedViewWithNewClientReadFromOldClient() throws Exception {
executeQueriesWithCurrentVersion(CREATE_DIVERGED_VIEW);
executeQueryWithClientVersion(compatibleClientVersion, QUERY_CREATE_DIVERGED_VIEW);
assertExpectedOutput(QUERY_CREATE_DIVERGED_VIEW);
}
@Test
public void testCreateDivergedViewWithNewClientReadFromNewClient() throws Exception {
executeQueriesWithCurrentVersion(CREATE_DIVERGED_VIEW);
executeQueriesWithCurrentVersion(QUERY_CREATE_DIVERGED_VIEW);
assertExpectedOutput(QUERY_CREATE_DIVERGED_VIEW);
}
/**
* Scenario:
* 1. New Client connects to the updated server
* 2. New Client creates tables and inserts data
* 3. Old Client reads the data inserted by the new client
*
* @throws Exception thrown if any errors encountered during query execution or file IO
*/
@Test
public void testSelectWithOldClient() throws Exception {
// Insert data with new client and read with old client
executeQueriesWithCurrentVersion(CREATE_ADD);
executeQueryWithClientVersion(compatibleClientVersion, QUERY_CREATE_ADD);
assertExpectedOutput(QUERY_CREATE_ADD);
}
/**
* Scenario:
* 1. Old Client connects to the updated server
* 2. Old Client creates tables and inserts data
* 3. New Client reads the data inserted by the old client
* 4. New Client inserts more data into the tables created by old client
* 5. Old Client reads the data inserted by new client
*
* @throws Exception thrown if any errors encountered during query execution or file IO
*/
@Test
public void testSelectUpsertWithNewClient() throws Exception {
// Insert data with old client and read with new client
executeQueryWithClientVersion(compatibleClientVersion, CREATE_ADD);
executeQueriesWithCurrentVersion(QUERY_CREATE_ADD);
assertExpectedOutput(QUERY_CREATE_ADD);
// Insert more data with new client and read with old client
executeQueriesWithCurrentVersion(ADD_DATA);
executeQueryWithClientVersion(compatibleClientVersion, QUERY_ADD_DATA);
assertExpectedOutput(QUERY_ADD_DATA);
}
/**
* Scenario:
* 1. New Client connects to the updated server
* 2. New Client creates tables and inserts data
* 3. Old Client reads the data inserted by the old client
* 4. Old Client inserts more data into the tables created by old client
* 5. New Client reads the data inserted by new client
*
* @throws Exception thrown if any errors encountered during query execution or file IO
*/
@Test
public void testSelectUpsertWithOldClient() throws Exception {
// Insert data with new client and read with old client
executeQueriesWithCurrentVersion(CREATE_ADD);
executeQueryWithClientVersion(compatibleClientVersion, QUERY_CREATE_ADD);
assertExpectedOutput(QUERY_CREATE_ADD);
// Insert more data with old client and read with new client
executeQueryWithClientVersion(compatibleClientVersion, ADD_DATA);
executeQueriesWithCurrentVersion(QUERY_ADD_DATA);
assertExpectedOutput(QUERY_ADD_DATA);
}
/**
* Scenario:
* 1. Old Client connects to the updated server
* 2. Old Client creates tables and inserts data
* 3. New Client reads the data inserted by the old client
* 4. Old Client creates and deletes the data
*
* @throws Exception thrown if any errors encountered during query execution or file IO
*/
@Test
public void testUpsertDeleteWithOldClient() throws Exception {
// Insert data with old client and read with new client
executeQueryWithClientVersion(compatibleClientVersion, CREATE_ADD);
executeQueriesWithCurrentVersion(QUERY_CREATE_ADD);
assertExpectedOutput(QUERY_CREATE_ADD);
// Deletes with the old client
executeQueryWithClientVersion(compatibleClientVersion, ADD_DELETE);
executeQueryWithClientVersion(compatibleClientVersion, QUERY_ADD_DELETE);
assertExpectedOutput(QUERY_ADD_DELETE);
}
/**
* Scenario:
* 1. New Client connects to the updated server
* 2. New Client creates tables and inserts data
* 3. Old Client reads the data inserted by the old client
* 4. New Client creates and deletes the data
*
* @throws Exception thrown if any errors encountered during query execution or file IO
*/
@Test
public void testUpsertDeleteWithNewClient() throws Exception {
// Insert data with old client and read with new client
executeQueriesWithCurrentVersion(CREATE_ADD);
executeQueryWithClientVersion(compatibleClientVersion, QUERY_CREATE_ADD);
assertExpectedOutput(QUERY_CREATE_ADD);
// Deletes with the new client
executeQueriesWithCurrentVersion(ADD_DELETE);
executeQueriesWithCurrentVersion(QUERY_ADD_DELETE);
assertExpectedOutput(QUERY_ADD_DELETE);
}
private void checkForPreConditions() throws Exception {
// For the first code cut of any major version, there wouldn't be any backward compatible
// clients. Hence the test wouldn't run and just return true when the client
// version to be tested is same as current version
assumeFalse(compatibleClientVersion.contains(MetaDataProtocol.CURRENT_CLIENT_VERSION));
// Make sure that cluster is clean before test execution with no system tables
try (org.apache.hadoop.hbase.client.Connection conn =
ConnectionFactory.createConnection(conf);
Admin admin = conn.getAdmin()) {
assertFalse(admin.tableExists(TableName.valueOf(QueryConstants.SYSTEM_SCHEMA_NAME,
PhoenixDatabaseMetaData.SYSTEM_CATALOG_TABLE)));
}
}
// Executes the queries listed in the operation file with a given client version
private void executeQueryWithClientVersion(String clientVersion, String operation)
throws Exception {
List<String> cmdParams = Lists.newArrayList();
cmdParams.add(BASH);
// Note that auto-commit is true for queries executed via SQLline
URL fileUrl = BackwardCompatibilityIT.class.getClassLoader().getResource(EXECUTE_QUERY_SH);
assertNotNull(fileUrl);
cmdParams.add(new File(fileUrl.getFile()).getAbsolutePath());
cmdParams.add(zkQuorum);
cmdParams.add(clientVersion);
fileUrl = BackwardCompatibilityIT.class.getClassLoader()
.getResource(SQL_DIR + operation + SQL_EXTENSION);
assertNotNull(fileUrl);
cmdParams.add(new File(fileUrl.getFile()).getAbsolutePath());
fileUrl = BackwardCompatibilityIT.class.getClassLoader().getResource(
RESULTS_AND_GOLD_FILES_DIR);
assertNotNull(fileUrl);
String resultFilePath = new File(fileUrl.getFile()).getAbsolutePath() + "/" +
RESULT_PREFIX + operation + TEXT_EXTENSION;
cmdParams.add(resultFilePath);
cmdParams.add(System.getProperty(JAVA_TMP_DIR));
if (System.getProperty(MVN_HOME) != null) {
cmdParams.add(System.getProperty(MVN_HOME));
}
ProcessBuilder pb = new ProcessBuilder(cmdParams);
final Process p = pb.start();
final StringBuffer sb = new StringBuffer();
//Capture the error stream if any from the execution of the script
Thread errorStreamThread = new Thread() {
@Override
public void run() {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(p.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (final Exception e) {
sb.append(e.getMessage());
}
}
};
errorStreamThread.start();
p.waitFor();
assertEquals(String.format("Executing the query failed%s. Check the result file: %s",
sb.length() > 0 ? sb.append(" with : ").toString() : "", resultFilePath),
0, p.exitValue());
}
// Executes the SQL commands listed in the given operation file from the sql_files directory
private void executeQueriesWithCurrentVersion(String operation) throws Exception {
Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
try (Connection conn = DriverManager.getConnection(url, props)) {
StringBuilder sb = new StringBuilder();
try (BufferedReader reader =
getBufferedReaderForResource(SQL_DIR + operation + SQL_EXTENSION)) {
String sqlCommand;
while ((sqlCommand = reader.readLine()) != null) {
sqlCommand = sqlCommand.trim();
if (sqlCommand.length() == 0 || sqlCommand.startsWith("/")
|| sqlCommand.startsWith("*")) {
continue;
}
sb.append(sqlCommand);
}
}
ResultSet rs;
String[] sqlCommands = sb.toString().split(";");
URL fileUrl = BackwardCompatibilityIT.class.getClassLoader().getResource(
RESULTS_AND_GOLD_FILES_DIR);
assertNotNull(fileUrl);
final String resultFile = new File(fileUrl.getFile()).getAbsolutePath() + "/" +
RESULT_PREFIX + operation + TEXT_EXTENSION;
try (BufferedWriter br = new BufferedWriter(new FileWriter(resultFile))) {
for (String command : sqlCommands) {
try (PreparedStatement stmt = conn.prepareStatement(command)) {
stmt.execute();
rs = stmt.getResultSet();
if (rs != null) {
saveResultSet(rs, br);
}
}
conn.commit();
}
}
}
}
// Saves the result set to a text file to be compared against the gold file for difference
private void saveResultSet(ResultSet rs, BufferedWriter br) throws Exception {
ResultSetMetaData rsm = rs.getMetaData();
int columnCount = rsm.getColumnCount();
StringBuilder row = new StringBuilder(formatStringWithQuotes(rsm.getColumnName(1)));
for (int i = 2; i <= columnCount; i++) {
row.append(",").append(formatStringWithQuotes(rsm.getColumnName(i)));
}
br.write(row.toString());
br.write("\n");
while (rs.next()) {
row = new StringBuilder(formatStringWithQuotes(rs.getString(1)));
for (int i = 2; i <= columnCount; i++) {
row.append(",").append(formatStringWithQuotes(rs.getString(i)));
}
br.write(row.toString());
br.write("\n");
}
}
private String formatStringWithQuotes(String str) {
return (str != null) ? String.format("\'%s\'", str) : "\'\'";
}
private BufferedReader getBufferedReaderForResource(String relativePath)
throws FileNotFoundException {
URL fileUrl = getClass().getClassLoader().getResource(relativePath);
assertNotNull(fileUrl);
return new BufferedReader(new FileReader(new File(fileUrl.getFile())));
}
// Compares the result file against the gold file to match for the expected output
// for the given operation
private void assertExpectedOutput(String result) throws Exception {
List<String> resultFile = Lists.newArrayList();
List<String> goldFile = Lists.newArrayList();
String line;
try (BufferedReader resultFileReader = getBufferedReaderForResource(
RESULTS_AND_GOLD_FILES_DIR + RESULT_PREFIX + result + TEXT_EXTENSION)) {
while ((line = resultFileReader.readLine()) != null) {
resultFile.add(line.trim());
}
}
try (BufferedReader goldFileReader = getBufferedReaderForResource(
RESULTS_AND_GOLD_FILES_DIR + GOLD_PREFIX + result + TEXT_EXTENSION)) {
while ((line = goldFileReader.readLine()) != null) {
line = line.trim();
if ( !(line.isEmpty() || line.startsWith("*") || line.startsWith("/"))) {
goldFile.add(line);
}
}
}
// We take the first line in gold file and match against the result file to exclude any
// other WARNING messages that comes as a result of the query execution
int index = resultFile.indexOf(goldFile.get(0));
assertNotEquals("Mismatch found between gold file and result file", -1, index);
resultFile = resultFile.subList(index, resultFile.size());
assertEquals(goldFile, resultFile);
}
}