org.killbill.commons.embeddeddb.postgresql.KillBillEmbeddedPostgreSql Maven / Gradle / Ivy
/*
* Copyright 2015 Groupon, Inc
* Copyright 2015 The Billing Project, LLC
*
* The Billing Project 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.killbill.commons.embeddeddb.postgresql;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.MoreObjects;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import io.airlift.command.Command;
import io.airlift.command.CommandFailedException;
import io.airlift.units.Duration;
import static com.google.common.base.StandardSystemProperty.OS_ARCH;
import static com.google.common.base.StandardSystemProperty.OS_NAME;
import static com.google.common.collect.Lists.newArrayList;
import static io.airlift.concurrent.Threads.daemonThreadsNamed;
import static io.airlift.testing.FileUtils.deleteRecursively;
import static java.lang.String.format;
import static java.nio.file.Files.copy;
import static java.nio.file.Files.createTempDirectory;
import static java.util.concurrent.Executors.newCachedThreadPool;
// Forked from https://github.com/airlift/testing-postgresql-server (as of 0c18d5aa4e67114d5a3f4eb66e899c2397374157)
// Added Java 6 support and ability to configure the port
class KillBillEmbeddedPostgreSql implements Closeable {
private static final Logger log = LoggerFactory.getLogger(KillBillEmbeddedPostgreSql.class);
private static final String JDBC_FORMAT = "jdbc:postgresql://localhost:%s/%s?user=%s";
private static final String PG_SUPERUSER = "postgres";
private static final Duration PG_STARTUP_WAIT = new Duration(10, TimeUnit.SECONDS);
private static final Duration COMMAND_TIMEOUT = new Duration(30, TimeUnit.SECONDS);
private final ExecutorService executor = newCachedThreadPool(daemonThreadsNamed("testing-postgresql-server-%s"));
private final Path serverDirectory;
private final Path dataDirectory;
private final int port;
private final AtomicBoolean closed = new AtomicBoolean();
private final Map postgresConfig;
private final Process postmaster;
public KillBillEmbeddedPostgreSql() throws IOException {
this(randomPort());
}
public KillBillEmbeddedPostgreSql(final int port) throws IOException {
this.port = port;
serverDirectory = createTempDirectory("testing-postgresql-server");
dataDirectory = serverDirectory.resolve("data");
postgresConfig = ImmutableMap.builder()
.put("timezone", "UTC")
.put("synchronous_commit", "off")
.put("checkpoint_segments", "64")
.put("max_connections", "300")
.build();
try {
unpackPostgres(serverDirectory);
initdb();
postmaster = startPostmaster();
} catch (final IOException e) {
close();
throw e;
}
}
private static int randomPort() throws IOException {
ServerSocket socket = null;
try {
socket = new ServerSocket(0);
return socket.getLocalPort();
} finally {
if (socket != null) {
socket.close();
}
}
}
private static void checkSql(final boolean expression, final String message) throws SQLException {
if (!expression) {
throw new SQLException(message);
}
}
private static String getPlatform() {
return (OS_NAME.value() + "-" + OS_ARCH.value()).replace(' ', '_');
}
public String getJdbcUrl(final String userName, final String dbName) {
return format(JDBC_FORMAT, port, dbName, userName);
}
public int getPort() {
return port;
}
public Connection getPostgresDatabase() throws SQLException {
return DriverManager.getConnection(getJdbcUrl("postgres", "postgres"));
}
@Override
public void close() {
if (closed.getAndSet(true)) {
return;
}
try {
pgStop();
} catch (final Exception e) {
log.error("could not stop postmaster in " + serverDirectory.toString(), e);
if (postmaster != null) {
postmaster.destroy();
}
}
deleteRecursively(serverDirectory.toAbsolutePath().toFile());
executor.shutdownNow();
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("serverDirectory", serverDirectory)
.add("port", port)
.toString();
}
private void initdb() {
system(pgBin("initdb"),
"-A", "trust",
"-U", PG_SUPERUSER,
"-D", dataDirectory.toString(),
"-E", "UTF-8");
}
private Process startPostmaster()
throws IOException {
final List args = newArrayList(pgBin("postgres"),
"-D", dataDirectory.toString(),
"-p", String.valueOf(port),
"-i",
"-F");
for (final Entry config : postgresConfig.entrySet()) {
args.add("-c");
args.add(config.getKey() + "=" + config.getValue());
}
final Process process = new ProcessBuilder(args)
.redirectErrorStream(true)
.redirectOutput(ProcessBuilder.Redirect.INHERIT)
.start();
log.info("postmaster started on port {}. Waiting up to {} for startup to finish.", port, PG_STARTUP_WAIT);
waitForServerStartup(process);
return process;
}
private void waitForServerStartup(final Process process) throws IOException {
Throwable lastCause = null;
final long start = System.nanoTime();
while (Duration.nanosSince(start).compareTo(PG_STARTUP_WAIT) <= 0) {
try {
checkReady();
log.debug("postmaster startup finished");
return;
} catch (final SQLException e) {
lastCause = e;
log.debug("while waiting for postmaster startup", e);
}
try {
// check if process has exited
final int value = process.exitValue();
throw new IOException(format("postmaster exited with value %d, check stdout for more detail", value));
} catch (final IllegalThreadStateException ignored) {
// process is still running, loop and try again
}
try {
Thread.sleep(10);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
throw new IOException("postmaster failed to start after " + PG_STARTUP_WAIT, lastCause);
}
private void checkReady() throws SQLException {
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
try {
connection = getPostgresDatabase();
statement = connection.createStatement();
resultSet = statement.executeQuery("SELECT 42");
checkSql(resultSet.next(), "no rows in result set");
checkSql(resultSet.getInt(1) == 42, "wrong result");
checkSql(!resultSet.next(), "multiple rows in result set");
} finally {
if (connection != null) {
connection.close();
}
if (statement != null) {
statement.close();
}
if (resultSet != null) {
resultSet.close();
}
}
}
private void pgStop() {
system(pgBin("pg_ctl"),
"stop",
"-D", dataDirectory.toString(),
"-m", "fast",
"-t", "5",
"-w");
}
private String pgBin(final String binaryName) {
return serverDirectory.resolve("bin").resolve(binaryName).toString();
}
private String system(final String... command) {
try {
return new Command(command)
.setTimeLimit(COMMAND_TIMEOUT)
.execute(executor)
.getCommandOutput();
} catch (final CommandFailedException e) {
throw Throwables.propagate(e);
}
}
private void unpackPostgres(final Path target) throws IOException {
final String archiveName = format("/postgresql-%s.tar.gz", getPlatform());
final URL url = KillBillEmbeddedPostgreSql.class.getResource(archiveName);
if (url == null) {
throw new RuntimeException("archive not found: " + archiveName);
}
final File archive = File.createTempFile("postgresql-", null);
try {
InputStream in = null;
try {
in = url.openStream();
copy(in, archive.toPath(), StandardCopyOption.REPLACE_EXISTING);
} finally {
if (in != null) {
in.close();
}
}
system("tar", "-xzf", archive.getPath(), "-C", target.toString());
} finally {
if (!archive.delete()) {
log.warn("failed to delete {}", archive);
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy