/*
* 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 gobblin.metastore.testing;
import java.io.Closeable;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.URISyntaxException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Properties;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableMap;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.wix.mysql.EmbeddedMysql;
import com.wix.mysql.config.MysqldConfig;
import com.wix.mysql.distribution.Version;
import gobblin.configuration.ConfigurationKeys;
import gobblin.metastore.MetaStoreModule;
import gobblin.metastore.util.DatabaseJobHistoryStoreSchemaManager;
import gobblin.metastore.util.MySqlJdbcUrl;
class TestMetastoreDatabaseServer implements Closeable {
private static final String INFORMATION_SCHEMA = "information_schema";
private static final String ROOT_USER = "root";
private static final String DROP_DATABASE_TEMPLATE = "DROP DATABASE IF EXISTS %s;";
private static final String CREATE_DATABASE_TEMPLATE = "CREATE DATABASE %s CHARACTER SET = %s COLLATE = %s;";
private static final String ADD_USER_TEMPLATE = "GRANT ALL ON %s.* TO '%s'@'%%';";
public static final String CONFIG_PREFIX = "gobblin.metastore.testing";
public static final String EMBEDDED_MYSQL_ENABLED_KEY = "embeddedMysqlEnabled";
public static final String EMBEDDED_MYSQL_ENABLED_FULL_KEY =
CONFIG_PREFIX + "." + EMBEDDED_MYSQL_ENABLED_KEY;
public static final String DBUSER_NAME_KEY = "dbUserName";
public static final String DBUSER_NAME_FULL_KEY = CONFIG_PREFIX + "." + DBUSER_NAME_KEY;
public static final String DBUSER_PASSWORD_KEY = "dbUserPassword";
public static final String DBUSER_PASSWORD_FULL_KEY = CONFIG_PREFIX + "." + DBUSER_PASSWORD_KEY;
public static final String DBHOST_KEY = "dbHost";
public static final String DBHOST_FULL_KEY = CONFIG_PREFIX + "." + DBHOST_KEY;
public static final String DBPORT_KEY = "dbPort";
public static final String DBPORT_FULL_KEY = CONFIG_PREFIX + "." + DBPORT_KEY;
private final Logger log = LoggerFactory.getLogger(TestMetastoreDatabaseServer.class);
private final MysqldConfig config;
private final EmbeddedMysql testingMySqlServer;
private final boolean embeddedMysqlEnabled;
private final String dbUserName;
private final String dbUserPassword;
private final String dbHost;
private final int dbPort;
TestMetastoreDatabaseServer(Config dbConfig) throws Exception {
Config realConfig = dbConfig.withFallback(getDefaultConfig()).getConfig(CONFIG_PREFIX);
this.embeddedMysqlEnabled = realConfig.getBoolean(EMBEDDED_MYSQL_ENABLED_KEY);
this.dbUserName = realConfig.getString(DBUSER_NAME_KEY);
this.dbUserPassword = realConfig.getString(DBUSER_PASSWORD_KEY);
this.dbHost = this.embeddedMysqlEnabled ? "localhost" : realConfig.getString(DBHOST_KEY);
this.dbPort = this.embeddedMysqlEnabled ? chooseRandomPort() : realConfig.getInt(DBPORT_KEY);
this.log.error("Starting with config: embeddedMysqlEnabled={} dbUserName={} dbHost={} dbPort={}",
this.embeddedMysqlEnabled,
this.dbUserName,
this.dbHost,
this.dbPort);
config = MysqldConfig.aMysqldConfig(Version.v5_6_latest)
.withPort(this.dbPort)
.withUser(this.dbUserName, this.dbUserPassword)
.build();
if (this.embeddedMysqlEnabled) {
testingMySqlServer = EmbeddedMysql.anEmbeddedMysql(config).start();
}
else {
testingMySqlServer = null;
}
}
static Config getDefaultConfig() {
return ConfigFactory.parseMap(ImmutableMap.<String, Object>builder()
.put(EMBEDDED_MYSQL_ENABLED_FULL_KEY, true)
.put(DBUSER_NAME_FULL_KEY, "testUser")
.put(DBUSER_PASSWORD_FULL_KEY, "testPassword")
.put(DBHOST_FULL_KEY, "localhost")
.put(DBPORT_FULL_KEY, 3306)
.build());
}
public void drop(String database) throws SQLException, URISyntaxException {
Optional<Connection> connectionOptional = Optional.absent();
try {
connectionOptional = getConnector(getInformationSchemaJdbcUrl());
Connection connection = connectionOptional.get();
executeStatement(connection, String.format(DROP_DATABASE_TEMPLATE, database));
} finally {
if (connectionOptional.isPresent()) {
connectionOptional.get().close();
}
}
}
@Override
public void close() throws IOException {
if (testingMySqlServer != null) {
testingMySqlServer.stop();
}
}
private int chooseRandomPort() throws IOException {
ServerSocket socket = null;
try {
socket = new ServerSocket(0);
return socket.getLocalPort();
} finally {
if (socket != null) {
socket.close();
}
}
}
MySqlJdbcUrl getJdbcUrl(String database) throws URISyntaxException {
return getBaseJdbcUrl()
.setPath(database)
.setUser(this.dbUserName)
.setPassword(this.dbUserPassword)
.setParameter("useLegacyDatetimeCode", "false")
.setParameter("rewriteBatchedStatements", "true");
}
private MySqlJdbcUrl getBaseJdbcUrl() throws URISyntaxException {
return MySqlJdbcUrl.create()
.setHost(this.dbHost)
.setPort(this.dbPort);
}
private MySqlJdbcUrl getInformationSchemaJdbcUrl() throws URISyntaxException {
return getBaseJdbcUrl()
.setPath(INFORMATION_SCHEMA)
.setUser(ROOT_USER);
}
private Optional<Connection> getConnector(MySqlJdbcUrl jdbcUrl) throws SQLException {
Properties properties = new Properties();
properties.setProperty(ConfigurationKeys.JOB_HISTORY_STORE_URL_KEY, jdbcUrl.toString());
Injector injector = Guice.createInjector(new MetaStoreModule(properties));
DataSource dataSource = injector.getInstance(DataSource.class);
return Optional.of(dataSource.getConnection());
}
private void ensureDatabaseExists(String database) throws SQLException, URISyntaxException {
Optional<Connection> connectionOptional = Optional.absent();
try {
connectionOptional = getConnector(getInformationSchemaJdbcUrl());
Connection connection = connectionOptional.get();
executeStatements(connection,
String.format(DROP_DATABASE_TEMPLATE, database),
String.format(CREATE_DATABASE_TEMPLATE, database,
config.getCharset().getCharset(), config.getCharset().getCollate()),
String.format(ADD_USER_TEMPLATE, database, config.getUsername()));
} finally {
if (connectionOptional.isPresent()) {
connectionOptional.get().close();
}
}
}
void prepareDatabase(String database, String version) throws Exception {
// Drop/create the database
this.ensureDatabaseExists(database);
// Deploy the schema
DatabaseJobHistoryStoreSchemaManager schemaManager =
DatabaseJobHistoryStoreSchemaManager.builder()
.setDataSource(getJdbcUrl(database).toString(), this.dbUserName, this.dbUserPassword)
.setVersion(version)
.build();
schemaManager.migrate();
}
private void executeStatements(Connection connection, String... statements) throws SQLException {
for (String statement : statements) {
executeStatement(connection, statement);
}
}
private void executeStatement(Connection connection, String statement) throws SQLException {
try (PreparedStatement preparedStatement = connection.prepareStatement(statement)) {
preparedStatement.execute();
}
}
}