diff --git a/CHANGELOG.md b/CHANGELOG.md index de762706..56272d48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,48 @@ + +1.0.0 +----- + +All the RC release features and fixes plus: + +* Add network alias option +* Expose full bind mount function +* fix equals/hashcode in builder. + +1.0.0RC3 +----- +* Stop excluding junit4 from testcontainers - check the README for the sordid details. +* Pass host and port to the ConnectionInfo bean. We strongly recommend you prefer getDatasource or getUrl, these will be more portable in usage. We ran into +a few use cases where this was handy, however. +* LegacySingleInstancePostgresExtension to do the old Junit5 lifecycle behavior. +* 60 seconds default startup wait. +* Expose bind mounts (optional, none by default) in builder, currently hard coded as Read only. +* Expose network (optional, none by default) in builder. + +1.0.0RC2 +------- +* Restore Java 8 compatibility +* Update to testcontainers 1.16.3 + +1.0.0RC1 +----- +* A completely rewritten version of `otj-pg-embedded`. Uses "testcontainers" for docker, while preserving API compatibility. + +Advantages + +* multi arch (m1 etc) support +* Works the same way on every OS - Mac, Windows, Linux. Please note the maintainers only test on Mac Linux +* You need a tarball for every linux distribution as PG 10+ no longer ship a "universal binary" for linux. +* Easy to switch docker image tag to upgrade versions. +* More maintainable and secure (you can pull docker images you trust, instead of trusting our tarballs) + +Admittedly, a few disadvantages + +* Slower than running a tarball +* A few compatibility drops and options have probably disappeared. Feel free to submit PRs +* Docker in Docker can be dodgy to get running. + +=== legacy tarball versions === + 0.13.4 ------ * POM 287, Flyway 7 diff --git a/README.md b/README.md index 4c20030a..d782a245 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,50 @@ OpenTable Embedded PostgreSQL Component ======================================= -Allows embedding PostgreSQL into Java application code with -no external dependencies. Excellent for allowing you to unit -test with a "real" Postgres without requiring end users to install -and set up a database cluster. +Note: This library requires Java 8+. + +Allows embedding PostgreSQL into Java application code, using Docker containers. +Excellent for allowing you to unit +test with a "real" Postgres without requiring end users to install and set up a database cluster. + +The release of 1.0 brings major changes to the innards of this library. +Previous pre 1.x versions used an embedded tarball. This was extemely fast (a major plus(, but we switched to a docker based version +for these reasons: + +Advantages +--- + +* multi architecture support. This has become a huge issue for us with the introduction of the Mac M1 (and Windows ARM, Linux ARM)/ +* The same container works the same way on every OS - Mac, Windows, Linux. +* You need a tarball for every linux distribution as PG 10+ no longer ship a "universal binary" for linux. This means a lot of support and maintenance work. +* Easy to switch docker image tag to upgrade versions - no need for a whole new pg-embedded version. +* More maintainable and secure (you can pull docker images you trust, instead of trusting our tarballs running in your security context) +* Trivial to do a build oneself based on the official Postgres image adding extensions, setup scripts etc - see https://github.com/docker-library/docs/blob/master/postgres/README.md for details. + +Admittedly, a few disadvantages +--- + +* Slower than running a tarball (2-5x slower). +* A few API compatibility changes and options have probably disappeared. Feel free to submit PRs. +* Docker in Docker can be dodgy to get running. (See below for one thing we discovered)) + +## Before filing tickets. + +1. Before filing tickets, please test your docker environment etc. If using podman or lima instead of "true docker", state so, and realize that the +docker socket api provided by these apps is not 100% compatible, as we've found to our sadness. We'll be revisiting +testing these in the future. We've managed to get PodMan working, albeit not 100% reliably. +2. **No further PRs or tickets will be accepted for the pre 1.0.0 release, unless community support arises for the `legacy` branch.** +3. We primarily use Macs and Ubuntu Linux at OpenTable. We'll be happy to try to help out otherwise, but other platforms, such +as Windows depend primarily on community support. We simply don't have the time or hardware. Happy to merge PRs though + +See "Alternatives Considered" as well if this library doesn't appear to fit your needs. -[![Build Status](https://travis-ci.org/opentable/otj-pg-embedded.svg)](https://travis-ci.org/opentable/otj-pg-embedded) ## Basic Usage In your JUnit test just add (for JUnit 5 example see **Using JUnit5** below): -```java +``` @Rule public SingleInstancePostgresRule pg = EmbeddedPostgresRules.singleInstance(); ``` @@ -23,11 +55,24 @@ Additionally you may use the [`EmbeddedPostgres`](src/main/java/com/opentable/db Default username/password is: postgres/postgres and the default database is 'postgres' +## Sample of Embedded Postgres direct Usage + +``` +public void testDatabaseName() throws IOException,SQLException{ + EmbeddedPostgres db=EmbeddedPostgres.builder().start(); + Datasource dataSource = db.getPostgresDatabase(); + .... use the datasource then ... + db.close(); + } +``` + +The builder includes options to set the image, the tag, the database name, and various configuration options. + ## Migrators (Flyway or Liquibase) You can easily integrate Flyway or Liquibase database schema migration: ##### Flyway -```java +``` @Rule public PreparedDbRule db = EmbeddedPostgresRules.preparedDatabase( @@ -35,7 +80,7 @@ public PreparedDbRule db = ``` ##### Liquibase -```java +``` @Rule public PreparedDbRule db = EmbeddedPostgresRules.preparedDatabase( @@ -48,31 +93,38 @@ independent databases gives you. ## Postgres version -The JAR file contains bundled version of Postgres. You can pass different Postgres version by implementing [`PgBinaryResolver`](src/main/java/com/opentable/db/postgres/embedded/PgBinaryResolver.java). +The default is to use the docker hub registry and pull a tag, hardcoded in `EmbeddedPostgres`. Currently this is "13-latest", +as this fits the needs of OpenTable, however you can change this easily. This is super useful, both to use a newer version +of Postgres, or to build your own DockerFile with additional extensions. -Example: -```java -class ClasspathBinaryResolver implements PgBinaryResolver { - public InputStream getPgBinary(String system, String machineHardware) throws IOException { - ClassPathResource resource = new ClassPathResource(format("postgresql-%s-%s.txz", system, machineHardware)); - return resource.getInputStream(); - } -} +You may change this either by environmental variables or by explicit builder usage + +### Environmental Variables -EmbeddedPostgreSQL - .builder() - .setPgBinaryResolver(new ClasspathBinaryResolver()) - .start(); +1. If `PG_FULL_IMAGE` is set, then this will be used and is assumed to include the full docker image name. So for example this might be set to `docker.otenv.com/postgres:mytag` +2. Otherwise, if `TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX` is set, this is prefixed to "postgres" (adding a slash if it doesn't exist). So for example this might be set to "docker.otenv.com/" +3. Otherwise, the default is used as defined above. + +### Explicit builder + +It is possible to change postgres image and tag in the builder: +``` + EmbeddedPostgres.builder() + .setTag("10") + .start(); ``` -## Windows +or use custom image: -If you experience difficulty running `otj-pg-embedded` tests on Windows, make sure -you've installed the appropriate MFC redistributables. +``` + EmbeddedPostgres.builder() + .setImage(DockerImageName.parse("docker.otenv.com/super-postgres")) + .start(); +``` -* [Microsoft Site](https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads]) -* [Github issue discussing this](https://github.com/opentable/otj-pg-embedded/issues/65) +There are also options to set the initDB configuration parameters, or other functional params, the bind mounts, and +the network. ## Using JUnit5 @@ -130,5 +182,62 @@ class DaoTestUsingJunit5 { } ``` +## Yes, Junit4 is a compile time dependency + +This is because TestContainers has a long outstanding bug to remove this -https://github.com/testcontainers/testcontainers-java/issues/970 +If you exclude Junit4, you get nasty NoClassDefFound errors. + +If you only use Junit5 in your classpath, and bringing in Junit4 bothers you (it does us, sigh), then +you can do the following: + +* add maven exclusions to the testcontainers modules you declare dependencies on to strip out junit:junit. This by itself +would still lead to NoClassDefFound errors. +* add a dependency on io.quarkus:quarkus-junit4-mock , which imports empty interfaces of the required classes. This is +a hack and a cheat, but what can you do? + +We initially excluded junit4 ourselves, which led to confusing breakages for junit5 users... + +## Some new options and some lost from Pre 1.0 + +* You can't wire to a local postgres, since that concept doesn't make sense here. So that's gone. +* You can add bind mounts and a Network (between two containers), since those are docker concepts, and can +be very useful. +* By the way, TestContainers does support ~/.docker/config.json for setting authenticated access to Docker, but we've not tested it. + +## Docker in Docker, authentication notes + +We've been able to get this working in our CICD pipeline with the following + +`TESTCONTAINERS_HOST_OVERRIDE=localhost` +`TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX=dockerhub.otenv.com/` + +The first parameter corrects for testcontainers getting confused whether to address the hosting container or the "container inside the container". +The second parameter (which outside OpenTable would point to your private Docker Registry) avoids much of the Docker Rate Limiting issues. + + +## Alternatives considered + +We updated this library primarily for convenience of current users to allow them to make a reasonably smooth transition to a Docker based +test approach. + +* Why not just use Testcontainers directly? + +You can, and it should work well for you. The builders, the api compatibility, the wrapping around Flyway - that's the added value. +But certainly there's no real reason you can't use TestContainers directly - they have their own Junit4 and Junit5 Rules/Extensions. + +* Why not _use a maven plugin approach like fabric8-docker-maven? + +Honestly I suspect this is a better approach in that it doesn't try to maintain it's own version of the Docker API, and +runs outside the tests, reducing issues like forking and threading conflicts. However it would have been too major an overhaul +for our users. + +* "I really prefer the old embedded postgres approach. It's faster." + * We recommend those who prefer the embedded tarball use https://github.com/zonkyio/embedded-postgres which was forked a couple + years ago from the embedded branch and is kept reasonably up to date. + * Another alternative is flapdoodle's embedded postgres. + +Both libraries suffer from many of the cons that bedeviled upkeep of this library for years, but they are certainly viable options +for many. + ---- -Copyright (C) 2017 OpenTable, Inc +Copyright (C) 2017-2022 OpenTable, Inc diff --git a/pom.xml b/pom.xml index 3c35001d..590f8b9a 100644 --- a/pom.xml +++ b/pom.xml @@ -16,10 +16,11 @@ 4.0.0 + com.opentable otj-parent-spring - 287 + 308 @@ -31,11 +32,13 @@ com.opentable.components otj-pg-embedded - 0.13.5-SNAPSHOT + 1.0.0.RC4-SNAPSHOT Embedded PostgreSQL driver - 1.8 + + 1.16.3 + 1800 true false @@ -51,27 +54,6 @@ - - - - org.codehaus.mojo - exec-maven-plugin - 1.3.2 - - - generate-resources - - exec - - - - - ./repack-postgres.sh - - - - - org.slf4j @@ -83,27 +65,6 @@ commons-lang3 - - org.apache.commons - commons-compress - - - - org.tukaani - xz - 1.5 - - - - commons-io - commons-io - - - - commons-codec - commons-codec - - org.flywaydb flyway-core @@ -120,9 +81,14 @@ org.postgresql postgresql + - com.github.spotbugs - spotbugs-annotations + org.testcontainers + postgresql + + + org.testcontainers + testcontainers @@ -144,17 +110,21 @@ slf4j-simple test + - - org.objenesis - objenesis - test - + + + + org.testcontainers + postgresql + ${dep.testcontainers.version} + + + org.testcontainers + testcontainers + ${dep.testcontainers.version} + + + - - org.mockito - mockito-core - test - - diff --git a/repack-postgres.sh b/repack-postgres.sh deleted file mode 100755 index c8bfc42b..00000000 --- a/repack-postgres.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/bash -ex -# NB: This is the *server* version, which is not to be confused with the client library version. -# The important compatibility point is the *protocol* version, which hasn't changed in ages. -VERSION=10.6-1 - -RSRC_DIR=$PWD/target/generated-resources - -[ -e $RSRC_DIR/.repacked ] && echo "Already repacked, skipping..." && exit 0 - -cd `dirname $0` - -PACKDIR=$(mktemp -d -t wat.XXXXXX) -LINUX_DIST=dist/postgresql-$VERSION-linux-x64-binaries.tar.gz -OSX_DIST=dist/postgresql-$VERSION-osx-binaries.zip -WINDOWS_DIST=dist/postgresql-$VERSION-win-binaries.zip - -mkdir -p dist/ target/generated-resources/ -[ -e $LINUX_DIST ] || wget -O $LINUX_DIST "http://get.enterprisedb.com/postgresql/postgresql-$VERSION-linux-x64-binaries.tar.gz" -[ -e $OSX_DIST ] || wget -O $OSX_DIST "http://get.enterprisedb.com/postgresql/postgresql-$VERSION-osx-binaries.zip" -[ -e $WINDOWS_DIST ] || wget -O $WINDOWS_DIST "http://get.enterprisedb.com/postgresql/postgresql-$VERSION-windows-x64-binaries.zip" - -tar xzf $LINUX_DIST -C $PACKDIR -pushd $PACKDIR/pgsql -tar cJf $RSRC_DIR/postgresql-Linux-x86_64.txz \ - share/postgresql \ - lib \ - bin/initdb \ - bin/pg_ctl \ - bin/postgres -popd - -rm -fr $PACKDIR && mkdir -p $PACKDIR - -unzip -q -d $PACKDIR $OSX_DIST -pushd $PACKDIR/pgsql -tar cJf $RSRC_DIR/postgresql-Darwin-x86_64.txz \ - share/postgresql \ - lib/libicudata.57.dylib \ - lib/libicui18n.57.dylib \ - lib/libicuuc.57.dylib \ - lib/libxml2.2.dylib \ - lib/libssl.1.0.0.dylib \ - lib/libcrypto.1.0.0.dylib \ - lib/libuuid.1.1.dylib \ - lib/postgresql/*.so \ - bin/initdb \ - bin/pg_ctl \ - bin/postgres -popd - -rm -fr $PACKDIR && mkdir -p $PACKDIR - -unzip -q -d $PACKDIR $WINDOWS_DIST -pushd $PACKDIR/pgsql -tar cJf $RSRC_DIR/postgresql-Windows-x86_64.txz \ - share \ - lib/iconv.lib \ - lib/libxml2.lib \ - lib/ssleay32.lib \ - lib/ssleay32MD.lib \ - lib/*.dll \ - bin/initdb.exe \ - bin/pg_ctl.exe \ - bin/postgres.exe \ - bin/*.dll -popd - -rm -rf $PACKDIR -touch $RSRC_DIR/.repacked diff --git a/src/main/java/com/opentable/db/postgres/embedded/BindMount.java b/src/main/java/com/opentable/db/postgres/embedded/BindMount.java new file mode 100644 index 00000000..66274cc9 --- /dev/null +++ b/src/main/java/com/opentable/db/postgres/embedded/BindMount.java @@ -0,0 +1,72 @@ +/* + * Licensed 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 com.opentable.db.postgres.embedded; + +import java.util.Objects; + +import org.testcontainers.containers.BindMode; + +public final class BindMount { + public static BindMount of(String localFile, String remoteFile, BindMode bindMode) { + return new BindMount(localFile, remoteFile, bindMode); + } + + private final String localFile; + private final String remoteFile; + private final BindMode bindMode; + + private BindMount(String localFile, String remoteFile, BindMode bindMode) { + this.localFile = localFile; + this.remoteFile = remoteFile; + this.bindMode = bindMode == null ? BindMode.READ_ONLY : bindMode; + } + + public BindMode getBindMode() { + return bindMode; + } + + public String getLocalFile() { + return localFile; + } + + public String getRemoteFile() { + return remoteFile; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BindMount bindMount = (BindMount) o; + return Objects.equals(localFile, bindMount.localFile); + } + + @Override + public int hashCode() { + return Objects.hash(localFile); + } + + @Override + public String toString() { + return "BindMount{" + + "localFile='" + localFile + '\'' + + ", remoteFile='" + remoteFile + '\'' + + ", bindMode=" + bindMode + + '}'; + } +} diff --git a/src/main/java/com/opentable/db/postgres/embedded/BundledPostgresBinaryResolver.java b/src/main/java/com/opentable/db/postgres/embedded/BundledPostgresBinaryResolver.java deleted file mode 100644 index 53dd1d2b..00000000 --- a/src/main/java/com/opentable/db/postgres/embedded/BundledPostgresBinaryResolver.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Licensed 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 com.opentable.db.postgres.embedded; - -import static java.lang.String.format; - -import java.io.InputStream; - -/** - * Resolves pre-bundled binaries from within the JAR file. - */ -final class BundledPostgresBinaryResolver implements PgBinaryResolver { - - @Override - public InputStream getPgBinary(String system, String machineHardware) { - return EmbeddedPostgres.class.getResourceAsStream(format("/postgresql-%s-%s.txz", system, machineHardware)); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - return o != null && getClass() == o.getClass(); - } - - @Override - public int hashCode() { - return getClass().hashCode(); - } -} diff --git a/src/main/java/com/opentable/db/postgres/embedded/ConnectionInfo.java b/src/main/java/com/opentable/db/postgres/embedded/ConnectionInfo.java index 7d948c3d..32fa65c5 100644 --- a/src/main/java/com/opentable/db/postgres/embedded/ConnectionInfo.java +++ b/src/main/java/com/opentable/db/postgres/embedded/ConnectionInfo.java @@ -13,26 +13,52 @@ */ package com.opentable.db.postgres.embedded; +/** + * Basic data holding class to hold the connection information - the url, user, and password + */ public class ConnectionInfo { - private final String dbName; - private final int port; + private final String url; private final String user; + private final String password; + private final String host; + private final int port; - public ConnectionInfo(final String dbName, final int port, final String user) { - this.dbName = dbName; - this.port = port; + public ConnectionInfo(final String url, final String user, final String password, final String host, final int port) { + this.url = url; this.user = user; + this.password = password; + this.host = host; + this.port = port; } public String getUser() { return user; } - public String getDbName() { - return dbName; + public String getUrl() { + return url; } + public String getPassword() { + return password; + } + + /** + * Use sparingly! + * Prefer getUrl as a general rule over composition using getHost and getPort + * @return the host. could be a hostname or an ip address + */ + public String getHost() { + return host; + } + + /** + * Use sparingly! + * Prefer getUrl as a general rule over composition using getHost and getPort + * @return the port + */ public int getPort() { return port; } + } diff --git a/src/main/java/com/opentable/db/postgres/embedded/DatabaseConnectionPreparer.java b/src/main/java/com/opentable/db/postgres/embedded/DatabaseConnectionPreparer.java index a7242417..2792d7e0 100644 --- a/src/main/java/com/opentable/db/postgres/embedded/DatabaseConnectionPreparer.java +++ b/src/main/java/com/opentable/db/postgres/embedded/DatabaseConnectionPreparer.java @@ -18,6 +18,10 @@ import javax.sql.DataSource; +/** + * Provides a default implementation of the DatabasePreparer, and adds an additional + * method + */ public interface DatabaseConnectionPreparer extends DatabasePreparer { @Override diff --git a/src/main/java/com/opentable/db/postgres/embedded/EmbeddedPostgres.java b/src/main/java/com/opentable/db/postgres/embedded/EmbeddedPostgres.java index 0fcae06e..e7da11ad 100644 --- a/src/main/java/com/opentable/db/postgres/embedded/EmbeddedPostgres.java +++ b/src/main/java/com/opentable/db/postgres/embedded/EmbeddedPostgres.java @@ -14,152 +14,141 @@ package com.opentable.db.postgres.embedded; -import static com.opentable.db.postgres.embedded.EmbeddedUtil.getWorkingDirectory; -import static com.opentable.db.postgres.embedded.EmbeddedUtil.mkdirs; +import static org.testcontainers.containers.PostgreSQLContainer.POSTGRESQL_PORT; import java.io.Closeable; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.ServerSocket; -import java.net.Socket; -import java.nio.channels.FileLock; -import java.nio.channels.OverlappingFileLockException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.sql.Connection; -import java.sql.ResultSet; +import java.net.URISyntaxException; import java.sql.SQLException; -import java.sql.Statement; import java.time.Duration; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; import java.util.UUID; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Collectors; import javax.sql.DataSource; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.SystemUtils; -import org.apache.commons.lang3.time.StopWatch; import org.postgresql.ds.PGSimpleDataSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.utility.DockerImageName; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -@SuppressWarnings("PMD.AvoidDuplicateLiterals") // "postgres" -@SuppressFBWarnings({"RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", "RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE"}) // java 11 triggers: https://github.com/spotbugs/spotbugs/issues/756 -public class EmbeddedPostgres implements Closeable -{ +/** + * Core class of the library, providing a builder (with reasonable defaults) to wrap + * testcontainers and launch postgres container. + */ +public class EmbeddedPostgres implements Closeable { private static final Logger LOG = LoggerFactory.getLogger(EmbeddedPostgres.class); - private static final String LOG_PREFIX = EmbeddedPostgres.class.getName() + "."; - private static final String JDBC_FORMAT = "jdbc:postgresql://localhost:%s/%s?user=%s"; - private static final String PG_STOP_MODE = "fast"; - private static final String PG_STOP_WAIT_S = "5"; - private static final String PG_SUPERUSER = "postgres"; - private static final Duration DEFAULT_PG_STARTUP_WAIT = Duration.ofSeconds(10); - private static final String LOCK_FILE_NAME = "epg-lock"; + static final Duration DEFAULT_PG_STARTUP_WAIT = Duration.ofSeconds(60); + static final String POSTGRES = "postgres"; + + // There are 3 defaults. + // 1) If this is defined, then it's assumed this contains the full image and tag... + static final String ENV_DOCKER_IMAGE="PG_FULL_IMAGE"; + // 2)Otherwise if this is defined, we'll use this as the prefix, and combine with the DOCKER_DEFAULT_TAG below + // This is already used in TestContainers as a env var, so it's useful to reuse for consistency. + static final String ENV_DOCKER_PREFIX = "TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX"; + // 3) Otherwise we'll just pull from docker hub with the DOCKER_DEFAULT_TAG + static final DockerImageName DOCKER_DEFAULT_IMAGE_NAME = DockerImageName.parse(POSTGRES); + static final String DOCKER_DEFAULT_TAG = "13-alpine"; + // Note you can override any of these defaults explicitly in the builder. - private final File pgDir; + private final PostgreSQLContainer postgreDBContainer; - private final Duration pgStartupWait; - private final File dataDirectory; - private final File lockFile; private final UUID instanceId = UUID.randomUUID(); - private final int port; - private final AtomicBoolean started = new AtomicBoolean(); - private final AtomicBoolean closed = new AtomicBoolean(); - - private final Map postgresConfig; - private final Map localeConfig; - - private volatile FileOutputStream lockStream; - private volatile FileLock lock; - private final boolean cleanDataDirectory; - - private final ProcessBuilder.Redirect errorRedirector; - private final ProcessBuilder.Redirect outputRedirector; - - EmbeddedPostgres(File parentDirectory, File dataDirectory, boolean cleanDataDirectory, - Map postgresConfig, Map localeConfig, int port, Map connectConfig, - PgDirectoryResolver pgDirectoryResolver, ProcessBuilder.Redirect errorRedirector, ProcessBuilder.Redirect outputRedirector) throws IOException - { - this(parentDirectory, dataDirectory, cleanDataDirectory, postgresConfig, localeConfig, port, connectConfig, - pgDirectoryResolver, errorRedirector, outputRedirector, DEFAULT_PG_STARTUP_WAIT, Optional.empty()); + + + EmbeddedPostgres(Map postgresConfig, + Map localeConfig, + Map bindMounts, + Optional network, + Optional networkAlias, + DockerImageName image, + String databaseName + ) throws IOException { + this(postgresConfig, localeConfig, bindMounts, network, networkAlias, image, DEFAULT_PG_STARTUP_WAIT, databaseName); } - EmbeddedPostgres(File parentDirectory, File dataDirectory, boolean cleanDataDirectory, - Map postgresConfig, Map localeConfig, int port, Map connectConfig, - PgDirectoryResolver pgDirectoryResolver, ProcessBuilder.Redirect errorRedirector, - ProcessBuilder.Redirect outputRedirector, Duration pgStartupWait, - Optional overrideWorkingDirectory) throws IOException - { - - this.cleanDataDirectory = cleanDataDirectory; - this.postgresConfig = new HashMap<>(postgresConfig); - this.localeConfig = new HashMap<>(localeConfig); - this.port = port; - this.pgDir = pgDirectoryResolver.getDirectory(overrideWorkingDirectory); - this.errorRedirector = errorRedirector; - this.outputRedirector = outputRedirector; - this.pgStartupWait = Objects.requireNonNull(pgStartupWait, "Wait time cannot be null"); - if (parentDirectory != null) { - mkdirs(parentDirectory); - cleanOldDataDirectories(parentDirectory); - if (dataDirectory != null) { - this.dataDirectory = dataDirectory; - } else { - this.dataDirectory = new File(parentDirectory, instanceId.toString()); - } - } else { - this.dataDirectory = dataDirectory; - } - if (this.dataDirectory == null) { - throw new IllegalArgumentException("no data directory"); - } - LOG.debug("{} postgres: data directory is {}, postgres directory is {}", instanceId, this.dataDirectory, this.pgDir); - mkdirs(this.dataDirectory); + EmbeddedPostgres(Map postgresConfig, + Map localeConfig, + Map bindMounts, + Optional network, + Optional networkAlias, + DockerImageName image, + Duration pgStartupWait, + String databaseName + ) throws IOException { + LOG.trace("Starting containers with image {}, pgConfig {}, localeConfig {}, bindMounts {}, pgStartupWait {}, dbName {} ", image, + postgresConfig, localeConfig, bindMounts, pgStartupWait, databaseName); + image = image.asCompatibleSubstituteFor(POSTGRES); + this.postgreDBContainer = new PostgreSQLContainer<>(image) + .withDatabaseName(databaseName) + .withUsername(POSTGRES) + .withPassword(POSTGRES) + .withStartupTimeout(pgStartupWait) + .withLogConsumer(new Slf4jLogConsumer(LOG)) + // https://github.com/docker-library/docs/blob/master/postgres/README.md#postgres_initdb_args + .withEnv("POSTGRES_INITDB_ARGS", String.join(" ", createInitOptions(localeConfig))) + .withEnv("POSTGRES_HOST_AUTH_METHOD", "trust"); + final List cmd = new ArrayList<>(Collections.singletonList(POSTGRES)); + cmd.addAll(createConfigOptions(postgresConfig)); + postgreDBContainer.setCommand(cmd.toArray(new String[0])); + processBindMounts(postgreDBContainer, bindMounts); + network.ifPresent(postgreDBContainer::withNetwork); + networkAlias.ifPresent(postgreDBContainer::withNetworkAliases); + postgreDBContainer.start(); + } - lockFile = new File(this.dataDirectory, LOCK_FILE_NAME); + private void processBindMounts(PostgreSQLContainer postgreDBContainer, Map bindMounts) { + bindMounts.values().stream() + .filter(f -> new File(f.getLocalFile()).exists()) + .forEach(f -> postgreDBContainer.addFileSystemBind(f.getLocalFile(), + f.getRemoteFile(), f.getBindMode())); + } - if (cleanDataDirectory || !new File(dataDirectory, "postgresql.conf").exists()) { - initdb(); + private List createConfigOptions(final Map postgresConfig) { + final List configOptions = new ArrayList<>(); + for (final Map.Entry config : postgresConfig.entrySet()) { + configOptions.add("-c"); + configOptions.add(config.getKey() + "=" + config.getValue()); } + return configOptions; + } - lock(); - startPostmaster(connectConfig); + private List createInitOptions(final Map localeConfig) { + final List localeOptions = new ArrayList<>(); + for (final Map.Entry config : localeConfig.entrySet()) { + localeOptions.add("--" + config.getKey()); + localeOptions.add(config.getValue()); + } + return localeOptions; } public DataSource getTemplateDatabase() { - return getDatabase("postgres", "template1"); + return getDatabase(postgreDBContainer.getUsername(), "template1"); } public DataSource getTemplateDatabase(Map properties) { - return getDatabase("postgres", "template1", properties); + return getDatabase(postgreDBContainer.getUsername(), "template1", properties); } public DataSource getPostgresDatabase() { - return getDatabase("postgres", "postgres"); + return getDatabase(postgreDBContainer.getUsername(), postgreDBContainer.getDatabaseName()); } public DataSource getPostgresDatabase(Map properties) { - return getDatabase("postgres", "postgres", properties); + return getDatabase(postgreDBContainer.getUsername(), postgreDBContainer.getDatabaseName(), properties); } public DataSource getDatabase(String userName, String dbName) { @@ -168,10 +157,11 @@ public DataSource getDatabase(String userName, String dbName) { public DataSource getDatabase(String userName, String dbName, Map properties) { final PGSimpleDataSource ds = new PGSimpleDataSource(); - ds.setServerName("localhost"); - ds.setPortNumber(port); + + ds.setURL(postgreDBContainer.getJdbcUrl()); ds.setDatabaseName(dbName); ds.setUser(userName); + ds.setPassword(postgreDBContainer.getPassword()); properties.forEach((propertyKey, propertyValue) -> { try { @@ -183,286 +173,93 @@ public DataSource getDatabase(String userName, String dbName, Map command = new ArrayList<>(); - command.addAll(Arrays.asList( - pgBin("initdb"), "-A", "trust", "-U", PG_SUPERUSER, - "-D", dataDirectory.getPath(), "-E", "UTF-8")); - command.addAll(createLocaleOptions()); - system(command.toArray(new String[command.size()])); - LOG.info("{} initdb completed in {}", instanceId, watch); + public static EmbeddedPostgres start() throws IOException { + return builder().start(); } - private void startPostmaster(Map connectConfig) throws IOException { - final StopWatch watch = new StopWatch(); - watch.start(); - if (started.getAndSet(true)) { - throw new IllegalStateException("Postmaster already started"); - } - - final List args = new ArrayList<>(); - args.addAll(Arrays.asList( - pgBin("pg_ctl"), - "-D", dataDirectory.getPath(), - "-o", createInitOptions().stream().collect(Collectors.joining(" ")), - "start" - )); - - final ProcessBuilder builder = new ProcessBuilder(args); - - builder.redirectErrorStream(true); - builder.redirectError(errorRedirector); - builder.redirectOutput(outputRedirector); - final Process postmaster = builder.start(); - - if (outputRedirector.type() == ProcessBuilder.Redirect.Type.PIPE) { - ProcessOutputLogger.logOutput(LoggerFactory.getLogger("pg-" + instanceId), postmaster); - } else if(outputRedirector.type() == ProcessBuilder.Redirect.Type.APPEND) { - ProcessOutputLogger.logOutput(LoggerFactory.getLogger(LOG_PREFIX + "pg-" + instanceId), postmaster); - } - - LOG.info("{} postmaster started as {} on port {}. Waiting up to {} for server startup to finish.", instanceId, postmaster.toString(), port, pgStartupWait); - - Runtime.getRuntime().addShutdownHook(newCloserThread()); - - waitForServerStartup(watch, connectConfig); + public static EmbeddedPostgres.Builder builder() { + return new Builder(); } - private List createInitOptions() { - final List initOptions = new ArrayList<>(); - initOptions.addAll(Arrays.asList( - "-p", Integer.toString(port), - "-F")); - - for (final Entry config : postgresConfig.entrySet()) { - initOptions.add("-c"); - initOptions.add(config.getKey() + "=" + config.getValue()); - } - - return initOptions; + public String getUserName() { + return postgreDBContainer.getUsername(); } - private List createLocaleOptions() { - final List localeOptions = new ArrayList<>(); - for (final Entry config : localeConfig.entrySet()) { - if (SystemUtils.IS_OS_WINDOWS) { - localeOptions.add(String.format("--%s=%s", config.getKey(), config.getValue())); - } else { - localeOptions.add("--" + config.getKey()); - localeOptions.add(config.getValue()); - } - } - return localeOptions; + public String getPassword() { + return postgreDBContainer.getPassword(); } - private void waitForServerStartup(StopWatch watch, Map connectConfig) throws IOException { - Throwable lastCause = null; - final long start = System.nanoTime(); - final long maxWaitNs = TimeUnit.NANOSECONDS.convert(pgStartupWait.toMillis(), TimeUnit.MILLISECONDS); - while (System.nanoTime() - start < maxWaitNs) { - try { - verifyReady(connectConfig); - LOG.info("{} postmaster startup finished in {}", instanceId, watch); - return; - } catch (final SQLException e) { - lastCause = e; - LOG.trace("While waiting for server startup", e); - } - - try { - Thread.sleep(100); - } catch (final InterruptedException e) { - Thread.currentThread().interrupt(); - return; - } - } - throw new IOException("Gave up waiting for server to start after " + pgStartupWait.toMillis() + "ms", lastCause); - } + public static class Builder { + private final Map config = new HashMap<>(); + private final Map localeConfig = new HashMap<>(); + private final Map bindMounts = new HashMap<>(); + private Optional network = Optional.empty(); - @SuppressFBWarnings("OBL_UNSATISFIED_OBLIGATION") - private void verifyReady(Map connectConfig) throws SQLException - { - final InetAddress localhost = InetAddress.getLoopbackAddress(); - try (Socket sock = new Socket()) { + private Duration pgStartupWait = DEFAULT_PG_STARTUP_WAIT; - sock.setSoTimeout((int) Duration.ofMillis(500).toMillis()); - sock.connect(new InetSocketAddress(localhost, port), (int) Duration.ofMillis(500).toMillis()); - } catch (final IOException e) { - throw new SQLException("connect failed", e); - } - try (Connection c = getPostgresDatabase(connectConfig).getConnection(); - Statement s = c.createStatement(); - ResultSet rs = s.executeQuery("SELECT 1")) { - if (!rs.next()) { - throw new IllegalStateException("expecting single row"); - } - if (1 != rs.getInt(1)) { - throw new IllegalStateException("expecting 1"); - } - if (rs.next()) { - throw new IllegalStateException("expecting single row"); - } - } - } + private DockerImageName image = getDefaultImage(); + private String databaseName = POSTGRES; + private Optional networkAlias = Optional.empty(); - private Thread newCloserThread() { - final Thread closeThread = new Thread(new Runnable() { - @Override - public void run() { - try { - EmbeddedPostgres.this.close(); - } catch (IOException ex) { - LOG.error("Unexpected IOException from Closeables.close", ex); - } + // See comments at top for the logic. + DockerImageName getDefaultImage() { + if (getEnvOrProperty(ENV_DOCKER_IMAGE) != null) { + return DockerImageName.parse(getEnvOrProperty(ENV_DOCKER_IMAGE)); } - }); - closeThread.setName("postgres-" + instanceId + "-closer"); - return closeThread; - } - - @Override - public void close() throws IOException { - if (closed.getAndSet(true)) { - return; - } - final StopWatch watch = new StopWatch(); - watch.start(); - try { - pgCtl(dataDirectory, "stop"); - LOG.info("{} shut down postmaster in {}", instanceId, watch); - } catch (final Exception e) { - LOG.error("Could not stop postmaster " + instanceId, e); - } - if (lock != null) { - lock.release(); - } - try { - lockStream.close(); - } catch (IOException e) { - LOG.error("while closing lockStream", e); - } - - if (cleanDataDirectory && System.getProperty("ot.epg.no-cleanup") == null) { - try { - FileUtils.deleteDirectory(dataDirectory); - } catch (IOException e) { - LOG.error("Could not clean up directory {}", dataDirectory.getAbsolutePath()); + if (getEnvOrProperty(ENV_DOCKER_PREFIX) != null) { + return DockerImageName.parse(insertSlashIfNeeded(getEnvOrProperty(ENV_DOCKER_PREFIX),POSTGRES)).withTag(DOCKER_DEFAULT_TAG); } - } else { - LOG.info("Did not clean up directory {}", dataDirectory.getAbsolutePath()); + return DOCKER_DEFAULT_IMAGE_NAME.withTag(DOCKER_DEFAULT_TAG); } - } - private void pgCtl(File dir, String action) { - system(pgBin("pg_ctl"), "-D", dir.getPath(), action, "-m", PG_STOP_MODE, "-t", PG_STOP_WAIT_S, "-w"); - } - - private void cleanOldDataDirectories(File parentDirectory) { - final File[] children = parentDirectory.listFiles(); - if (children == null) { - return; + String getEnvOrProperty(String key) { + return Optional.ofNullable(System.getenv(key)).orElse(System.getProperty(key)); } - for (final File dir : children) { - if (!dir.isDirectory()) { - continue; - } - final File lockFile = new File(dir, LOCK_FILE_NAME); - final boolean isTooNew = System.currentTimeMillis() - lockFile.lastModified() < 10 * 60 * 1000; - if (!lockFile.exists() || isTooNew) { - continue; - } - try (FileOutputStream fos = new FileOutputStream(lockFile); - FileLock lock = fos.getChannel().tryLock()) { - if (lock != null) { - LOG.info("Found stale data directory {}", dir); - if (new File(dir, "postmaster.pid").exists()) { - try { - pgCtl(dir, "stop"); - LOG.info("Shut down orphaned postmaster!"); - } catch (Exception e) { - if (LOG.isDebugEnabled()) { - LOG.warn("Failed to stop postmaster " + dir, e); - } else { - LOG.warn("Failed to stop postmaster " + dir + ": " + e.getMessage()); - } - } - } - FileUtils.deleteDirectory(dir); - } - } catch (final OverlappingFileLockException e) { - // The directory belongs to another instance in this VM. - LOG.trace("While cleaning old data directories", e); - } catch (final Exception e) { - LOG.warn("While cleaning old data directories", e); + String insertSlashIfNeeded(String prefix, String repo) { + if ((prefix.endsWith("/")) || (repo.startsWith("/"))) { + return prefix + repo; } + return prefix + "/" + repo; } - } - - private String pgBin(String binaryName) { - final String extension = SystemUtils.IS_OS_WINDOWS ? ".exe" : ""; - return new File(pgDir, "bin/" + binaryName + extension).getPath(); - } - - - public static EmbeddedPostgres start() throws IOException { - return builder().start(); - } - - public static EmbeddedPostgres.Builder builder() { - return new Builder(); - } - - public static class Builder { - private final File parentDirectory = getWorkingDirectory(); - private Optional overrideWorkingDirectory = Optional.empty(); // use tmpdir - private File builderDataDirectory; - private final Map config = new HashMap<>(); - private final Map localeConfig = new HashMap<>(); - private boolean builderCleanDataDirectory = true; - private int builderPort = 0; - private final Map connectConfig = new HashMap<>(); - private PgDirectoryResolver pgDirectoryResolver; - private Duration pgStartupWait = DEFAULT_PG_STARTUP_WAIT; - - private ProcessBuilder.Redirect errRedirector = ProcessBuilder.Redirect.PIPE; - private ProcessBuilder.Redirect outRedirector = ProcessBuilder.Redirect.PIPE; Builder() { config.put("timezone", "UTC"); config.put("synchronous_commit", "off"); config.put("max_connections", "300"); + config.put("fsync", "off"); } + /** + * Override the default startup wait for the container to start and be ready + * @param pgStartupWait time to wait + * @return builder + */ public Builder setPGStartupWait(Duration pgStartupWait) { Objects.requireNonNull(pgStartupWait); if (pgStartupWait.isNegative()) { @@ -473,138 +270,114 @@ public Builder setPGStartupWait(Duration pgStartupWait) { return this; } - public Builder setCleanDataDirectory(boolean cleanDataDirectory) { - builderCleanDataDirectory = cleanDataDirectory; - return this; - } - - public Builder setDataDirectory(Path path) { - return setDataDirectory(path.toFile()); - } - - public Builder setDataDirectory(File directory) { - builderDataDirectory = directory; - return this; - } - - public Builder setDataDirectory(String path) { - return setDataDirectory(new File(path)); - } - + /** + * Arguments passed to the postgres process itself + * @param key key + * @param value value + * @return builder + */ public Builder setServerConfig(String key, String value) { config.put(key, value); return this; } - public Builder setLocaleConfig(String key, String value) { - localeConfig.put(key, value); - return this; + /** + * Set up a readonly bind mount. + * @param localFile local file system reference + * @param remoteFile remote file system reference + * @return builder + */ + public Builder setBindMount(String localFile, String remoteFile) { + return setBindMount(BindMount.of(localFile, remoteFile, BindMode.READ_ONLY)); } - public Builder setConnectConfig(String key, String value) { - connectConfig.put(key, value); + /** + * Set up a bind mount between the local file system and the remote + * @param bindMount object representing this bind + * @return builder + */ + public Builder setBindMount(BindMount bindMount) { + bindMounts.put(bindMount.getLocalFile(), bindMount); return this; } - public Builder setOverrideWorkingDirectory(File workingDirectory) { - overrideWorkingDirectory = Optional.ofNullable(workingDirectory); + /** + * Set up a shared network and the alias. This is useful if you have multiple containers + * and they need to communicate with each other. + * @param network The Network. Usually Network.Shared. + * @param networkAlias an alias by which other containers in the network can refer to this container + * @return builder + */ + public Builder setNetwork(Network network, String networkAlias) { + this.network = Optional.ofNullable(network); + this.networkAlias = Optional.ofNullable(networkAlias); return this; } - public Builder setPort(int port) { - builderPort = port; + /** + * Override the default databaseName of postgres + * @param databaseName the name + * @return builder + */ + public Builder setDatabaseName(String databaseName) { + this.databaseName = databaseName; return this; } - public Builder setErrorRedirector(ProcessBuilder.Redirect errRedirector) { - this.errRedirector = errRedirector; + /** + * Set up arguments to initDB process + * @param key key + * @param value value + * @return builder + */ + public Builder setLocaleConfig(String key, String value) { + localeConfig.put(key, value); return this; } - public Builder setOutputRedirector(ProcessBuilder.Redirect outRedirector) { - this.outRedirector = outRedirector; + /** + * Set a default image. This may be with or without a tag + * @param image Docker image + * @return builder + */ + public Builder setImage(DockerImageName image) { + this.image = image; return this; } - @Deprecated - public Builder setPgBinaryResolver(PgBinaryResolver pgBinaryResolver) { - return setPgDirectoryResolver(new UncompressBundleDirectoryResolver(pgBinaryResolver)); - } - - public Builder setPgDirectoryResolver(PgDirectoryResolver pgDirectoryResolver) { - this.pgDirectoryResolver = pgDirectoryResolver; + /** + * Add the tag to an existing image + * @param tag Tag + * @return builder + */ + public Builder setTag(String tag) { + this.image = this.image.withTag(tag); return this; } - public Builder setPostgresBinaryDirectory(File directory) { - return setPgDirectoryResolver((x) -> directory); + DockerImageName getImage() { + return image; } public EmbeddedPostgres start() throws IOException { - if (builderPort == 0) { - builderPort = detectPort(); - } - if (builderDataDirectory == null) { - builderDataDirectory = Files.createTempDirectory("epg").toFile(); - } - if (pgDirectoryResolver == null) { - LOG.trace("pgDirectoryResolver not overriden, using default (UncompressBundleDirectoryResolver)"); - pgDirectoryResolver = UncompressBundleDirectoryResolver.getDefault(); - } - return new EmbeddedPostgres(parentDirectory, builderDataDirectory, builderCleanDataDirectory, config, - localeConfig, builderPort, connectConfig, pgDirectoryResolver, errRedirector, outRedirector, - pgStartupWait, overrideWorkingDirectory); + return new EmbeddedPostgres(config, localeConfig, bindMounts, network, networkAlias, image, pgStartupWait, databaseName); } @Override public boolean equals(Object o) { - if (this == o) { + if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Builder builder = (Builder) o; - return builderCleanDataDirectory == builder.builderCleanDataDirectory && - builderPort == builder.builderPort && - Objects.equals(parentDirectory, builder.parentDirectory) && - Objects.equals(builderDataDirectory, builder.builderDataDirectory) && - Objects.equals(config, builder.config) && - Objects.equals(localeConfig, builder.localeConfig) && - Objects.equals(connectConfig, builder.connectConfig) && - Objects.equals(pgDirectoryResolver, builder.pgDirectoryResolver) && - Objects.equals(pgStartupWait, builder.pgStartupWait) && - Objects.equals(errRedirector, builder.errRedirector) && - Objects.equals(outRedirector, builder.outRedirector); + return Objects.equals(config, builder.config) && Objects.equals(localeConfig, builder.localeConfig) && Objects.equals(bindMounts, builder.bindMounts) && Objects.equals(network, builder.network) && Objects.equals(pgStartupWait, builder.pgStartupWait) && Objects.equals(image, builder.image) && Objects.equals(databaseName, builder.databaseName) && Objects.equals(networkAlias, builder.networkAlias); } @Override public int hashCode() { - return Objects.hash(parentDirectory, builderDataDirectory, config, localeConfig, builderCleanDataDirectory, builderPort, connectConfig, pgDirectoryResolver, pgStartupWait, errRedirector, outRedirector); - } - } - - private void system(String... command) - { - try { - final ProcessBuilder builder = new ProcessBuilder(command); - builder.redirectErrorStream(true); - builder.redirectError(errorRedirector); - builder.redirectOutput(outputRedirector); - final Process process = builder.start(); - - if (outputRedirector.type() == ProcessBuilder.Redirect.Type.PIPE) { - ProcessOutputLogger.logOutput(LoggerFactory.getLogger("init-" + instanceId + ":" + FilenameUtils.getName(command[0])), process); - } else if(outputRedirector.type() == ProcessBuilder.Redirect.Type.APPEND) { - ProcessOutputLogger.logOutput(LoggerFactory.getLogger(LOG_PREFIX + "init-" + instanceId + ":" + FilenameUtils.getName(command[0])), process); - } - if (0 != process.waitFor()) { - throw new IllegalStateException(String.format("Process %s failed%n%s", Arrays.asList(command), IOUtils.toString(process.getErrorStream(), StandardCharsets.UTF_8))); - } - } catch (final RuntimeException e) { // NOPMD - throw e; - } catch (final Exception e) { - throw new RuntimeException(e); + return Objects.hash(config, localeConfig, bindMounts, network, pgStartupWait, image, databaseName, networkAlias); } } diff --git a/src/main/java/com/opentable/db/postgres/embedded/EmbeddedUtil.java b/src/main/java/com/opentable/db/postgres/embedded/EmbeddedUtil.java deleted file mode 100644 index ab7e76cc..00000000 --- a/src/main/java/com/opentable/db/postgres/embedded/EmbeddedUtil.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Licensed 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 com.opentable.db.postgres.embedded; - -import static java.nio.file.StandardOpenOption.CREATE; -import static java.nio.file.StandardOpenOption.WRITE; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.channels.AsynchronousFileChannel; -import java.nio.channels.Channel; -import java.nio.channels.CompletionHandler; -import java.nio.file.FileSystems; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.Duration; -import java.util.concurrent.Phaser; - -import org.apache.commons.compress.archivers.tar.TarArchiveEntry; -import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; -import org.apache.commons.lang3.SystemUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.tukaani.xz.XZInputStream; - -final class EmbeddedUtil { - static final Logger LOG = LoggerFactory.getLogger(EmbeddedPostgres.class); - static final String JDBC_FORMAT = "jdbc:postgresql://localhost:%s/%s?user=%s"; - static final String PG_STOP_MODE = "fast"; - static final String PG_STOP_WAIT_S = "5"; - static final String PG_SUPERUSER = "postgres"; - static final Duration DEFAULT_PG_STARTUP_WAIT = Duration.ofSeconds(10); - static final String LOCK_FILE_NAME = "epg-lock"; - - private EmbeddedUtil() {} - - static File getWorkingDirectory() { - final File tempWorkingDirectory = new File(System.getProperty("java.io.tmpdir"), "embedded-pg"); - return new File(System.getProperty("ot.epg.working-dir", tempWorkingDirectory.getPath())); - } - - - static void mkdirs(File dir) { - if (!dir.mkdirs() && !(dir.isDirectory() && dir.exists())) { - throw new IllegalStateException("could not create " + dir); - } - } - - /** - * Get current operating system string. The string is used in the appropriate - * postgres binary name. - * - * @return Current operating system string. - */ - static String getOS() { - if (SystemUtils.IS_OS_WINDOWS) { - return "Windows"; - } - if (SystemUtils.IS_OS_MAC_OSX) { - return "Darwin"; - } - if (SystemUtils.IS_OS_LINUX) { - return "Linux"; - } - throw new UnsupportedOperationException("Unknown OS " + SystemUtils.OS_NAME); - } - - /** - * Get the machine architecture string. The string is used in the appropriate - * postgres binary name. - * - * @return Current machine architecture string. - */ - static String getArchitecture() { - return "amd64".equals(SystemUtils.OS_ARCH) ? "x86_64" : SystemUtils.OS_ARCH; - } - - /** - * Unpack archive compressed by tar with xz compression. By default system tar is used (faster). If not found, then the - * java implementation takes place. - * - * @param stream A stream with the postgres binaries. - * @param targetDir The directory to extract the content to. - */ - static void extractTxz(InputStream stream, String targetDir) throws IOException { - try ( - XZInputStream xzIn = new XZInputStream(stream); - TarArchiveInputStream tarIn = new TarArchiveInputStream(xzIn) - ) { - final Phaser phaser = new Phaser(1); - TarArchiveEntry entry; - - while ((entry = tarIn.getNextTarEntry()) != null) { //NOPMD - final String individualFile = entry.getName(); - final File fsObject = new File(targetDir, individualFile); - - if (entry.isSymbolicLink() || entry.isLink()) { - Path target = FileSystems.getDefault().getPath(entry.getLinkName()); - Files.createSymbolicLink(fsObject.toPath(), target); - } else if (entry.isFile()) { - byte[] content = new byte[(int) entry.getSize()]; - int read = tarIn.read(content, 0, content.length); - if (read == -1) { - throw new IllegalStateException("could not read " + individualFile); - } - mkdirs(fsObject.getParentFile()); - - final AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(fsObject.toPath(), CREATE, WRITE); //NOPMD - final ByteBuffer buffer = ByteBuffer.wrap(content); //NOPMD - - phaser.register(); - fileChannel.write(buffer, 0, fileChannel, new CompletionHandler() { - @Override - public void completed(Integer written, Channel channel) { - closeChannel(channel); - } - - @Override - public void failed(Throwable error, Channel channel) { - LOG.error("Could not write file {}", fsObject.getAbsolutePath(), error); - closeChannel(channel); - } - - private void closeChannel(Channel channel) { - try { - channel.close(); - } catch (IOException e) { - LOG.error("Unexpected error while closing the channel", e); - } finally { - phaser.arriveAndDeregister(); - } - } - }); - } else if (entry.isDirectory()) { - mkdirs(fsObject); - } else { - throw new UnsupportedOperationException( - String.format("Unsupported entry found: %s", individualFile) - ); - } - - if (individualFile.startsWith("bin/") || individualFile.startsWith("./bin/")) { - fsObject.setExecutable(true); - } - } - - phaser.arriveAndAwaitAdvance(); - } - } -} diff --git a/src/main/java/com/opentable/db/postgres/embedded/FlywayPreparer.java b/src/main/java/com/opentable/db/postgres/embedded/FlywayPreparer.java index b6944cb4..d7e720e7 100644 --- a/src/main/java/com/opentable/db/postgres/embedded/FlywayPreparer.java +++ b/src/main/java/com/opentable/db/postgres/embedded/FlywayPreparer.java @@ -25,7 +25,11 @@ // TODO: Detect missing migration files. // cf. https://github.com/flyway/flyway/issues/1496 // There is also a related @Ignored test in otj-sql. - +// MJB: This is finally fixed in Flyway 8.41 onwards +// failOnMissingLocations = true, not willing to force that update yet. +/** + * Support for integrating Flyway and performing a DB migration as part of the setup process. + */ public final class FlywayPreparer implements DatabasePreparer { private final List locations; diff --git a/src/main/java/com/opentable/db/postgres/embedded/JdbcUrlUtils.java b/src/main/java/com/opentable/db/postgres/embedded/JdbcUrlUtils.java new file mode 100644 index 00000000..346ec670 --- /dev/null +++ b/src/main/java/com/opentable/db/postgres/embedded/JdbcUrlUtils.java @@ -0,0 +1,92 @@ +/* + * Licensed 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 com.opentable.db.postgres.embedded; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +final class JdbcUrlUtils { + + static final String JDBC_URL_PREFIX = "jdbc:"; + + private JdbcUrlUtils() { + } + + /** + * Extracts port from JDBC url + * + * @param url JDBC url + * @return + * The port component of this URI, or -1 if the port is undefined + */ + static int getPort(final String url) { + return URI.create(url.substring(JDBC_URL_PREFIX.length())).getPort(); + } + + /** + * Adds Username/Password to the JDBC url (in postgres format) + * + * @param url JDBC url + * @param userName User name + * @param password Password + * @return Modified Url + * @throws URISyntaxException If Url violates RFC 2396 + */ + static String addUsernamePassword(final String url, final String userName, final String password) throws URISyntaxException, UnsupportedEncodingException { + final URI uri = URI.create(url.substring(JDBC_URL_PREFIX.length())); + final Map params = new HashMap<>( + Optional.ofNullable(uri.getQuery()) + .map(Stream::of).orElse(Stream.empty())// Hack around the fact Optional.Stream requires Java 9+. + .flatMap(par -> Arrays.stream(par.split("&"))) + .map(part -> part.split("=")) + .filter(part -> part.length > 1) + .collect(Collectors.toMap(part -> part[0], part -> part[1]))); + params.put("user", URLEncoder.encode(userName, "UTF-8")); // Use the old form for Java 8 compatibility. + params.put("password", URLEncoder.encode(password, "UTF-8")); + return JDBC_URL_PREFIX + new URI(uri.getScheme(), + uri.getUserInfo(), + uri.getHost(), + uri.getPort(), + uri.getPath(), + params.entrySet().stream().map(i -> i.getKey() + "=" + i.getValue()).collect(Collectors.joining("&")), + uri.getFragment()); + } + + /** + * Replaces database name in the JDBC url + * + * @param url JDBC url + * @param dbName Database name + * @return Modified Url + * @throws URISyntaxException If Url violates RFC 2396 + */ + static String replaceDatabase(final String url, final String dbName) throws URISyntaxException { + final URI uri = URI.create(url.substring(JDBC_URL_PREFIX.length())); + return JDBC_URL_PREFIX + new URI(uri.getScheme(), + uri.getUserInfo(), + uri.getHost(), + uri.getPort(), + "/" + dbName, + uri.getQuery(), + uri.getFragment()); + } +} diff --git a/src/main/java/com/opentable/db/postgres/embedded/LiquibasePreparer.java b/src/main/java/com/opentable/db/postgres/embedded/LiquibasePreparer.java index d497ef34..2a33abbe 100644 --- a/src/main/java/com/opentable/db/postgres/embedded/LiquibasePreparer.java +++ b/src/main/java/com/opentable/db/postgres/embedded/LiquibasePreparer.java @@ -27,6 +27,11 @@ import static liquibase.database.DatabaseFactory.getInstance; +/** + * Support for integrating Liquibase and performing a DB migration as part of the setup process. + * + * NB: OpenTable doesn't use Liquibase, so this is currently community supported code. + */ public final class LiquibasePreparer implements DatabasePreparer { private final String location; @@ -46,19 +51,13 @@ private LiquibasePreparer(String location, Contexts contexts) { @Override public void prepare(DataSource ds) throws SQLException { - Connection connection = null; - try { - connection = ds.getConnection(); + try (Connection connection = ds.getConnection();){ + Database database = getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection)); Liquibase liquibase = new Liquibase(location, new ClassLoaderResourceAccessor(), database); //NOPMD liquibase.update(contexts); } catch (LiquibaseException e) { throw new SQLException(e); - } finally { - if (connection != null) { - connection.rollback(); - connection.close(); - } } } diff --git a/src/main/java/com/opentable/db/postgres/embedded/PgBinaryResolver.java b/src/main/java/com/opentable/db/postgres/embedded/PgBinaryResolver.java deleted file mode 100644 index d586399a..00000000 --- a/src/main/java/com/opentable/db/postgres/embedded/PgBinaryResolver.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Licensed 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 com.opentable.db.postgres.embedded; - -import java.io.IOException; -import java.io.InputStream; - -/** - * A strategy for resolving PostgreSQL binaries. - * - * @see BundledPostgresBinaryResolver - */ -public interface PgBinaryResolver { - - /** - * Returns an input stream with the postgress binary for the given - * systen and hardware architecture. - * @param system a system identification (Darwin, Linux...) - * @param machineHardware a machine hardware architecture (x86_64...) - * @return the binary - */ - InputStream getPgBinary(String system, String machineHardware) throws IOException; -} diff --git a/src/main/java/com/opentable/db/postgres/embedded/PgDirectoryResolver.java b/src/main/java/com/opentable/db/postgres/embedded/PgDirectoryResolver.java deleted file mode 100644 index 449f9e50..00000000 --- a/src/main/java/com/opentable/db/postgres/embedded/PgDirectoryResolver.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed 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 com.opentable.db.postgres.embedded; - -import java.io.File; -import java.util.Optional; - -@FunctionalInterface -public interface PgDirectoryResolver { - File getDirectory(Optional overrideWorkingDirectory); -} diff --git a/src/main/java/com/opentable/db/postgres/embedded/PreparedDbProvider.java b/src/main/java/com/opentable/db/postgres/embedded/PreparedDbProvider.java index b5492495..f9559064 100644 --- a/src/main/java/com/opentable/db/postgres/embedded/PreparedDbProvider.java +++ b/src/main/java/com/opentable/db/postgres/embedded/PreparedDbProvider.java @@ -14,6 +14,8 @@ package com.opentable.db.postgres.embedded; import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URISyntaxException; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; @@ -31,14 +33,13 @@ import org.apache.commons.lang3.RandomStringUtils; import org.postgresql.ds.PGSimpleDataSource; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.opentable.db.postgres.embedded.EmbeddedPostgres.Builder; -public class PreparedDbProvider -{ - private static final String JDBC_FORMAT = "jdbc:postgresql://localhost:%d/%s?user=%s"; +public class PreparedDbProvider { + private static final Logger LOG = LoggerFactory.getLogger(PreparedDbProvider.class); /** * Each database cluster's template1 database has a unique set of schema @@ -69,8 +70,7 @@ private PreparedDbProvider(DatabasePreparer preparer, Iterable * Each schema set has its own database cluster. The template1 database has the schema preloaded so that * each test case need only create a new database and not re-invoke your preparer. */ - private static synchronized PrepPipeline createOrFindPreparer(DatabasePreparer preparer, Iterable> customizers) throws IOException, SQLException - { + private static synchronized PrepPipeline createOrFindPreparer(DatabasePreparer preparer, Iterable> customizers) throws IOException, SQLException { final ClusterKey key = new ClusterKey(preparer, customizers); PrepPipeline result = CLUSTERS.get(key); if (result != null) { @@ -90,10 +90,20 @@ private static synchronized PrepPipeline createOrFindPreparer(DatabasePreparer p /** * Create a new database, and return it as a JDBC connection string. * NB: No two invocations will return the same database. + * + * @return JDBC connection string. + * @throws SQLException SQLException if any */ - public String createDatabase() throws SQLException - { - return getJdbcUri(createNewDB()); + public String createDatabase() throws SQLException { + final DbInfo info = createNewDB(); + if (!info.isSuccess()) { + return null; + } + try { + return JdbcUrlUtils.addUsernamePassword(info.getUrl(), info.getUser(), info.getPassword()); + } catch (URISyntaxException | UnsupportedEncodingException e) { + throw new SQLException(e); + } } /** @@ -103,53 +113,56 @@ public String createDatabase() throws SQLException * get the JDBC connection string. * NB: No two invocations will return the same database. */ - private DbInfo createNewDB() throws SQLException - { - return dbPreparer.getNextDb(); + private DbInfo createNewDB() throws SQLException { + return dbPreparer.getNextDb(); } - public ConnectionInfo createNewDatabase() throws SQLException - { + public ConnectionInfo createNewDatabase() throws SQLException { final DbInfo dbInfo = createNewDB(); - return dbInfo == null || !dbInfo.isSuccess() ? null : new ConnectionInfo(dbInfo.getDbName(), dbInfo.getPort(), dbInfo.getUser()); + return !dbInfo.isSuccess() ? null : new ConnectionInfo(dbInfo.getUrl(), dbInfo.getUser(), dbInfo.getPassword(), dbInfo.getHost(), dbInfo.getPort()); } /** * Create a new Datasource given DBInfo. * More common usage is to call createDatasource(). + * + * @param connectionInfo connection information + * @return Datasource */ - public DataSource createDataSourceFromConnectionInfo(final ConnectionInfo connectionInfo) throws SQLException - { + public DataSource createDataSourceFromConnectionInfo(final ConnectionInfo connectionInfo) { final PGSimpleDataSource ds = new PGSimpleDataSource(); - ds.setPortNumber(connectionInfo.getPort()); - ds.setDatabaseName(connectionInfo.getDbName()); + ds.setUrl(connectionInfo.getUrl()); ds.setUser(connectionInfo.getUser()); + ds.setPassword(connectionInfo.getPassword()); return ds; } /** * Create a new database, and return it as a DataSource. * No two invocations will return the same database. + * + * @return Datasource the datasource + * @throws SQLException SQLException if any */ - public DataSource createDataSource() throws SQLException - { + public DataSource createDataSource() throws SQLException { return createDataSourceFromConnectionInfo(createNewDatabase()); } - String getJdbcUri(DbInfo db) - { - return String.format(JDBC_FORMAT, db.port, db.dbName, db.user); - } + /** * Return configuration tweaks in a format appropriate for otj-jdbc DatabaseModule. + * + * @param dbModuleName Name of the module + * @return Configuration properties + * @throws SQLException SQLException if any */ - public Map getConfigurationTweak(String dbModuleName) throws SQLException - { + public Map getConfigurationTweak(String dbModuleName) throws SQLException { final DbInfo db = dbPreparer.getNextDb(); final Map result = new HashMap<>(); - result.put("ot.db." + dbModuleName + ".uri", getJdbcUri(db)); + result.put("ot.db." + dbModuleName + ".uri", db.getUrl()); result.put("ot.db." + dbModuleName + ".ds.user", db.user); + result.put("ot.db." + dbModuleName + ".ds.password", db.password); return result; } @@ -157,18 +170,15 @@ public Map getConfigurationTweak(String dbModuleName) throws SQL * Spawns a background thread that prepares databases ahead of time for speed, and then uses a * synchronous queue to hand the prepared databases off to test cases. */ - private static class PrepPipeline implements Runnable - { + private static class PrepPipeline implements Runnable { private final EmbeddedPostgres pg; private final SynchronousQueue nextDatabase = new SynchronousQueue<>(); - PrepPipeline(EmbeddedPostgres pg) - { + PrepPipeline(EmbeddedPostgres pg) { this.pg = pg; } - PrepPipeline start() - { + PrepPipeline start() { final ExecutorService service = Executors.newSingleThreadExecutor(r -> { final Thread t = new Thread(r); t.setDaemon(true); @@ -180,12 +190,11 @@ PrepPipeline start() return this; } - DbInfo getNextDb() throws SQLException - { + DbInfo getNextDb() throws SQLException { try { final DbInfo next = nextDatabase.take(); if (next.ex != null) { - throw next.ex; + throw new SQLException(next.ex); } return next; } catch (final InterruptedException e) { @@ -195,19 +204,18 @@ DbInfo getNextDb() throws SQLException } @Override - public void run() - { + public void run() { while (true) { - final String newDbName = RandomStringUtils.randomAlphabetic(12).toLowerCase(Locale.ENGLISH); + final String newDbName = "pge_" + RandomStringUtils.randomAlphabetic(12).toLowerCase(Locale.ENGLISH); SQLException failure = null; try { - create(pg.getPostgresDatabase(), newDbName, "postgres"); + create(pg.getPostgresDatabase(), newDbName, pg.getUserName()); } catch (SQLException e) { failure = e; } try { if (failure == null) { - nextDatabase.put(DbInfo.ok(newDbName, pg.getPort(), "postgres")); + nextDatabase.put(DbInfo.ok(pg.getJdbcUrl(newDbName), pg.getUserName(), pg.getPassword(), pg.getHost(), pg.getPort())); } else { nextDatabase.put(DbInfo.error(failure)); } @@ -219,9 +227,7 @@ public void run() } } - @SuppressFBWarnings({"OBL_UNSATISFIED_OBLIGATION", "RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE"}) - private static void create(final DataSource connectDb, final String dbName, final String userName) throws SQLException - { + private static void create(final DataSource connectDb, final String dbName, final String userName) throws SQLException { if (dbName == null) { throw new IllegalStateException("the database name must not be null!"); } @@ -230,7 +236,8 @@ private static void create(final DataSource connectDb, final String dbName, fina } try (Connection c = connectDb.getConnection(); - PreparedStatement stmt = c.prepareStatement(String.format("CREATE DATABASE %s OWNER %s ENCODING = 'utf8'", dbName, userName))) { + PreparedStatement stmt = c.prepareStatement(String.format("CREATE DATABASE %s OWNER %s ENCODING = 'utf8'", dbName, userName))) { + LOG.debug("Statement: {}", stmt); stmt.execute(); } } @@ -265,34 +272,40 @@ public int hashCode() { } } - public static class DbInfo - { - public static DbInfo ok(final String dbName, final int port, final String user) { - return new DbInfo(dbName, port, user, null); + public static class DbInfo { + public static DbInfo ok(final String url, final String user, final String password, final String host, final int port) { + return new DbInfo(url, user, password, null, host, port); } public static DbInfo error(SQLException e) { - return new DbInfo(null, -1, null, e); + return new DbInfo(null, null, null, e, null, -1); } - private final String dbName; - private final int port; + private final String url; private final String user; + private final String password; private final SQLException ex; + private final String host; + private final int port; - private DbInfo(final String dbName, final int port, final String user, final SQLException e) { - this.dbName = dbName; - this.port = port; + private DbInfo(final String url, final String user, final String password, final SQLException e, final String host, final int port) { + this.url = url; this.user = user; - this.ex = null; + this.password = password; + this.ex = e; + this.host = host; + this.port = port; + } + + public String getHost() { + return host; } public int getPort() { return port; } - - public String getDbName() { - return dbName; + public String getUrl() { + return url; } public String getUser() { @@ -306,5 +319,9 @@ public SQLException getException() { public boolean isSuccess() { return ex == null; } + + public String getPassword() { + return password; + } } } diff --git a/src/main/java/com/opentable/db/postgres/embedded/ProcessOutputLogger.java b/src/main/java/com/opentable/db/postgres/embedded/ProcessOutputLogger.java deleted file mode 100644 index b1c989ae..00000000 --- a/src/main/java/com/opentable/db/postgres/embedded/ProcessOutputLogger.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Licensed 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 com.opentable.db.postgres.embedded; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.lang.reflect.Field; -import java.nio.charset.StandardCharsets; -import java.util.Optional; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import org.slf4j.Logger; - -/** - * Read standard output of process and write lines to given {@link Logger} as INFO; - * depends on {@link ProcessBuilder#redirectErrorStream(boolean)} being set to {@code true} (since only stdout is - * read). - * - *

- * The use of the input stream is threadsafe since it's used only in a single thread—the one launched by this - * code. - */ -final class ProcessOutputLogger implements Runnable { - @SuppressWarnings("PMD.LoggerIsNotStaticFinal") - private final Logger logger; - private final Process process; - private final BufferedReader reader; - - private ProcessOutputLogger(final Logger logger, final Process process) { - this.logger = logger; - this.process = process; - reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)); - } - - @Override - public void run() { - try { - while (process.isAlive()) { - try { - Optional.ofNullable(reader.readLine()).ifPresent(logger::info); - } catch (final IOException e) { - logger.error("while reading output", e); - return; - } - } - } finally { - try { - reader.close(); - } catch (final IOException e) { - logger.error("caught i/o exception closing reader", e); - } - } - } - - static void logOutput(final Logger logger, final Process process) { - final Thread t = new Thread(new ProcessOutputLogger(logger, process)); - t.setName("log:" + describe(process)); - t.setDaemon(true); - t.start(); - } - - @SuppressFBWarnings("REC_CATCH_EXCEPTION") // expected and ignored - private static String describe(Process process) { - try { // java 9+ - return String.format("pid(%s)", MethodHandles.lookup().findVirtual(Process.class, "pid", MethodType.methodType(long.class)).invoke(process)); - } catch (Throwable ignored) {} // NOPMD since MethodHandles.invoke throws Throwable - try { // openjdk / oraclejdk 8 - final Field pid = process.getClass().getDeclaredField("pid"); - pid.setAccessible(true); - return String.format("pid(%s)", pid.getInt(process)); - } catch (Exception ignored) {} - return process.toString(); // anything goes wrong - } -} diff --git a/src/main/java/com/opentable/db/postgres/embedded/UncompressBundleDirectoryResolver.java b/src/main/java/com/opentable/db/postgres/embedded/UncompressBundleDirectoryResolver.java deleted file mode 100644 index 58e64c80..00000000 --- a/src/main/java/com/opentable/db/postgres/embedded/UncompressBundleDirectoryResolver.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Licensed 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 com.opentable.db.postgres.embedded; - -import static com.opentable.db.postgres.embedded.EmbeddedUtil.LOCK_FILE_NAME; -import static com.opentable.db.postgres.embedded.EmbeddedUtil.extractTxz; -import static com.opentable.db.postgres.embedded.EmbeddedUtil.getArchitecture; -import static com.opentable.db.postgres.embedded.EmbeddedUtil.getOS; -import static com.opentable.db.postgres.embedded.EmbeddedUtil.getWorkingDirectory; -import static com.opentable.db.postgres.embedded.EmbeddedUtil.mkdirs; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.channels.FileLock; -import java.security.DigestInputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -import org.apache.commons.codec.binary.Hex; -import org.apache.commons.io.IOUtils; -import org.apache.commons.io.output.ByteArrayOutputStream; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class UncompressBundleDirectoryResolver implements PgDirectoryResolver { - - private static volatile UncompressBundleDirectoryResolver DEFAULT_INSTANCE; - - public static synchronized UncompressBundleDirectoryResolver getDefault() { - if (DEFAULT_INSTANCE == null) { - DEFAULT_INSTANCE = new UncompressBundleDirectoryResolver(new BundledPostgresBinaryResolver()); - } - return DEFAULT_INSTANCE; - } - - private static final Logger LOG = LoggerFactory.getLogger(EmbeddedPostgres.class); - private final Lock prepareBinariesLock = new ReentrantLock(); - - private final PgBinaryResolver pgBinaryResolver; - - public UncompressBundleDirectoryResolver(PgBinaryResolver pgBinaryResolver) { - this.pgBinaryResolver = pgBinaryResolver; - } - - private final Map prepareBinaries = new HashMap<>(); - - @Override - public File getDirectory(Optional overrideWorkingDirectory) { - prepareBinariesLock.lock(); - try { - if (prepareBinaries.containsKey(pgBinaryResolver) && prepareBinaries.get(pgBinaryResolver).exists()) { - return prepareBinaries.get(pgBinaryResolver); - } - - final String system = getOS(); - final String machineHardware = getArchitecture(); - - LOG.info("Detected a {} {} system", system, machineHardware); - File pgDir; - final InputStream pgBinary; // NOPMD - try { - pgBinary = pgBinaryResolver.getPgBinary(system, machineHardware); - } catch (final IOException e) { - throw new ExceptionInInitializerError(e); - } - - if (pgBinary == null) { - throw new IllegalStateException("No Postgres binary found for " + system + " / " + machineHardware); - } - - try (DigestInputStream pgArchiveData = new DigestInputStream(pgBinary, MessageDigest.getInstance("MD5")); - ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - IOUtils.copy(pgArchiveData, baos); - pgArchiveData.close(); - - String pgDigest = Hex.encodeHexString(pgArchiveData.getMessageDigest().digest()); - File workingDirectory = overrideWorkingDirectory.isPresent() ? overrideWorkingDirectory.get() - : getWorkingDirectory(); - pgDir = new File(workingDirectory, String.format("PG-%s", pgDigest)); - - mkdirs(pgDir); - final File unpackLockFile = new File(pgDir, LOCK_FILE_NAME); - final File pgDirExists = new File(pgDir, ".exists"); - - if (!pgDirExists.exists()) { - try (FileOutputStream lockStream = new FileOutputStream(unpackLockFile); - FileLock unpackLock = lockStream.getChannel().tryLock()) { - if (unpackLock != null) { - try { - if (pgDirExists.exists()) { - throw new IllegalStateException( - "unpack lock acquired but .exists file is present " + pgDirExists); - } - LOG.info("Extracting Postgres..."); - try (ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray())) { - extractTxz(bais, pgDir.getPath()); - } - if (!pgDirExists.createNewFile()) { - throw new IllegalStateException("couldn't make .exists file " + pgDirExists); - } - } catch (Exception e) { - LOG.error("while unpacking Postgres", e); - } - } else { - // the other guy is unpacking for us. - int maxAttempts = 60; - while (!pgDirExists.exists() && --maxAttempts > 0) { // NOPMD - Thread.sleep(1000L); - } - if (!pgDirExists.exists()) { - throw new IllegalStateException( - "Waited 60 seconds for postgres to be unpacked but it never finished!"); - } - } - } finally { - if (unpackLockFile.exists() && !unpackLockFile.delete()) { - LOG.error("could not remove lock file {}", unpackLockFile.getAbsolutePath()); - } - } - } - } catch (final IOException | NoSuchAlgorithmException e) { - throw new ExceptionInInitializerError(e); - } catch (final InterruptedException ie) { - Thread.currentThread().interrupt(); - throw new ExceptionInInitializerError(ie); - } - prepareBinaries.put(pgBinaryResolver, pgDir); - LOG.info("Postgres binaries at {}", pgDir); - return pgDir; - } finally { - prepareBinariesLock.unlock(); - } - } -} diff --git a/src/main/java/com/opentable/db/postgres/junit/EmbeddedPostgresRules.java b/src/main/java/com/opentable/db/postgres/junit/EmbeddedPostgresRules.java index a590fc5b..89e8294f 100644 --- a/src/main/java/com/opentable/db/postgres/junit/EmbeddedPostgresRules.java +++ b/src/main/java/com/opentable/db/postgres/junit/EmbeddedPostgresRules.java @@ -13,7 +13,6 @@ */ package com.opentable.db.postgres.junit; -import org.junit.rules.TestRule; import com.opentable.db.postgres.embedded.DatabasePreparer; @@ -23,14 +22,17 @@ private EmbeddedPostgresRules() { /** * Create a vanilla Postgres cluster -- just initialized, no customizations applied. + * @return SingleInstancePostgresRule */ public static SingleInstancePostgresRule singleInstance() { return new SingleInstancePostgresRule(); } /** - * Returns a {@link TestRule} to create a Postgres cluster, shared amongst all test cases in this JVM. + * Returns a {@link PreparedDbRule} to create a Postgres cluster, shared amongst all test cases in this JVM. * The rule contributes Config switches to configure each test case to get its own database. + * @param preparer DatabasePreparer + * @return SingleInstancePostgresRule */ public static PreparedDbRule preparedDatabase(DatabasePreparer preparer) { diff --git a/src/main/java/com/opentable/db/postgres/junit5/EmbeddedPostgresExtension.java b/src/main/java/com/opentable/db/postgres/junit5/EmbeddedPostgresExtension.java index 7c446aab..9165a1bc 100644 --- a/src/main/java/com/opentable/db/postgres/junit5/EmbeddedPostgresExtension.java +++ b/src/main/java/com/opentable/db/postgres/junit5/EmbeddedPostgresExtension.java @@ -14,7 +14,6 @@ package com.opentable.db.postgres.junit5; import com.opentable.db.postgres.embedded.DatabasePreparer; -import org.junit.rules.TestRule; public final class EmbeddedPostgresExtension { @@ -22,14 +21,17 @@ private EmbeddedPostgresExtension() {} /** * Create a vanilla Postgres cluster -- just initialized, no customizations applied. + * @return SingleInstancePostgresExtension */ public static SingleInstancePostgresExtension singleInstance() { return new SingleInstancePostgresExtension(); } /** - * Returns a {@link TestRule} to create a Postgres cluster, shared amongst all test cases in this JVM. + * Returns a {@link PreparedDbExtension} to create a Postgres cluster, shared amongst all test cases in this JVM. * The rule contributes Config switches to configure each test case to get its own database. + * @return PreparedDBExtension + * @param preparer DatabasePreparer */ public static PreparedDbExtension preparedDatabase(DatabasePreparer preparer) { diff --git a/src/main/java/com/opentable/db/postgres/junit5/LegacySingleInstancePostgresExtension.java b/src/main/java/com/opentable/db/postgres/junit5/LegacySingleInstancePostgresExtension.java new file mode 100644 index 00000000..dc8c9dcd --- /dev/null +++ b/src/main/java/com/opentable/db/postgres/junit5/LegacySingleInstancePostgresExtension.java @@ -0,0 +1,87 @@ +/* + * Licensed 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 com.opentable.db.postgres.junit5; + +import java.io.IOException; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; + +import org.junit.jupiter.api.extension.AfterTestExecutionCallback; +import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +import com.opentable.db.postgres.embedded.EmbeddedPostgres; + +/* + This is a legacy version of this class that implements the old lifecycle behavior. + Late in our testing for 1.0, we ran into a test case where the new EmbeddedPostgresExtension + failed to work properly. + + This is DEPRECATED until we figure out what's going on ;) and can be removed + in a minor version. Until then this is a workaround should anyone else run into this. + + */ +@Deprecated +public class LegacySingleInstancePostgresExtension implements AfterTestExecutionCallback, BeforeTestExecutionCallback { + + private volatile EmbeddedPostgres epg; + private volatile Connection postgresConnection; + private final List> builderCustomizers = new CopyOnWriteArrayList<>(); + + @Override + public void beforeTestExecution(ExtensionContext context) throws Exception { + epg = pg(); + postgresConnection = epg.getPostgresDatabase().getConnection(); + } + + private EmbeddedPostgres pg() throws IOException { + final EmbeddedPostgres.Builder builder = EmbeddedPostgres.builder(); + builderCustomizers.forEach(c -> c.accept(builder)); + return builder.start(); + } + + public LegacySingleInstancePostgresExtension customize(Consumer customizer) { + if (epg != null) { + throw new AssertionError("already started"); + } + builderCustomizers.add(customizer); + return this; + } + + public EmbeddedPostgres getEmbeddedPostgres() + { + EmbeddedPostgres epg = this.epg; + if (epg == null) { + throw new AssertionError("JUnit test not started yet!"); + } + return epg; + } + + @Override + public void afterTestExecution(ExtensionContext context) { + try { + postgresConnection.close(); + } catch (SQLException e) { + throw new AssertionError(e); + } + try { + epg.close(); + } catch (IOException e) { + throw new AssertionError(e); + } + } +} diff --git a/src/main/java/com/opentable/db/postgres/junit5/SingleInstancePostgresExtension.java b/src/main/java/com/opentable/db/postgres/junit5/SingleInstancePostgresExtension.java index 6797d418..9a5caf04 100644 --- a/src/main/java/com/opentable/db/postgres/junit5/SingleInstancePostgresExtension.java +++ b/src/main/java/com/opentable/db/postgres/junit5/SingleInstancePostgresExtension.java @@ -13,11 +13,6 @@ */ package com.opentable.db.postgres.junit5; -import com.opentable.db.postgres.embedded.EmbeddedPostgres; -import org.junit.jupiter.api.extension.AfterTestExecutionCallback; -import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; -import org.junit.jupiter.api.extension.ExtensionContext; - import java.io.IOException; import java.sql.Connection; import java.sql.SQLException; @@ -25,7 +20,34 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Consumer; -public class SingleInstancePostgresExtension implements AfterTestExecutionCallback, BeforeTestExecutionCallback { +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +import com.opentable.db.postgres.embedded.EmbeddedPostgres; + +/* + Implementing AfterTestExecutionCallback and BeforeTestExecutionCallback does not work if you want to use the EmbeddedPostgres in a @BeforeEach + or @BeforeAll method because it isn't instantiated then. + + The order in which the methods are called with BeforeTestExecutionCallback is: + @BeforeAll method of the test class + @BeforeEach method of the test class + beforeTestExecution(ExtensionContext) method of + SingleInstancePostgresExtension + Actual test method of the test class + + And using BeforeAllCallback instead it will be: + beforeAll(ExtensionContext) method of SingleInstancePostgresExtension + @BeforeAll method of the test class + @BeforeEach method of the test class + Actual test method of the test class + + See: https://github.com/opentable/otj-pg-embedded/pull/138. + Credits: https://github.com/qutax + + */ +public class SingleInstancePostgresExtension implements AfterAllCallback, BeforeAllCallback { private volatile EmbeddedPostgres epg; private volatile Connection postgresConnection; @@ -34,7 +56,7 @@ public class SingleInstancePostgresExtension implements AfterTestExecutionCallba SingleInstancePostgresExtension() { } @Override - public void beforeTestExecution(ExtensionContext extensionContext) throws Exception { + public void beforeAll(ExtensionContext context) throws Exception { epg = pg(); postgresConnection = epg.getPostgresDatabase().getConnection(); } @@ -63,7 +85,7 @@ public EmbeddedPostgres getEmbeddedPostgres() } @Override - public void afterTestExecution(ExtensionContext extensionContext) { + public void afterAll(ExtensionContext context) { try { postgresConnection.close(); } catch (SQLException e) { diff --git a/src/test/java/com/opentable/db/postgres/embedded/EmbeddedPostgresTest.java b/src/test/java/com/opentable/db/postgres/embedded/EmbeddedPostgresTest.java index f448a36d..5c91c1dd 100644 --- a/src/test/java/com/opentable/db/postgres/embedded/EmbeddedPostgresTest.java +++ b/src/test/java/com/opentable/db/postgres/embedded/EmbeddedPostgresTest.java @@ -20,13 +20,19 @@ import java.io.IOException; import java.sql.Connection; +import java.sql.DatabaseMetaData; import java.sql.ResultSet; +import java.sql.SQLException; import java.sql.Statement; +import javax.sql.DataSource; + import org.apache.commons.lang3.SystemUtils; +import org.junit.Assume; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import org.testcontainers.utility.DockerImageName; public class EmbeddedPostgresTest { @@ -46,15 +52,6 @@ public void testEmbeddedPg() throws Exception } } - @Test - public void testEmbeddedPgCreationWithNestedDataDirectory() throws Exception - { - try (EmbeddedPostgres pg = EmbeddedPostgres.builder() - .setDataDirectory(tf.newFolder("data-dir-parent") + "/data-dir") - .start()) { - // nothing to do - } - } @Test public void testValidLocaleSettingsPassthrough() throws IOException { @@ -81,4 +78,71 @@ public void testValidLocaleSettingsPassthrough() throws IOException { fail("Failed to set locale settings: " + e.getLocalizedMessage()); } } + + @Test + public void testImageOptions() { + // Ugly hack, since OT already has this defined as an ENV VAR, which can't really be cleared + Assume.assumeTrue(System.getenv(EmbeddedPostgres.ENV_DOCKER_PREFIX) == null); + System.clearProperty(EmbeddedPostgres.ENV_DOCKER_PREFIX); + System.clearProperty(EmbeddedPostgres.ENV_DOCKER_IMAGE); + + DockerImageName defaultImage = EmbeddedPostgres.builder().getDefaultImage(); + assertEquals(EmbeddedPostgres.DOCKER_DEFAULT_IMAGE_NAME.withTag(EmbeddedPostgres.DOCKER_DEFAULT_TAG).toString(), defaultImage.toString()); + + System.setProperty(EmbeddedPostgres.ENV_DOCKER_PREFIX, "dockerhub.otenv.com/"); + defaultImage = EmbeddedPostgres.builder().getDefaultImage(); + assertEquals("dockerhub.otenv.com/" + EmbeddedPostgres.DOCKER_DEFAULT_IMAGE_NAME.getUnversionedPart() + ":" + EmbeddedPostgres.DOCKER_DEFAULT_TAG, defaultImage.toString()); + + System.clearProperty(EmbeddedPostgres.ENV_DOCKER_PREFIX); + System.setProperty(EmbeddedPostgres.ENV_DOCKER_IMAGE, "dockerhub.otenv.com/ot-pg:14-latest"); + + EmbeddedPostgres.Builder b = EmbeddedPostgres.builder(); + defaultImage = b.getDefaultImage(); + assertEquals("dockerhub.otenv.com/ot-pg:14-latest", defaultImage.toString()); + assertEquals("dockerhub.otenv.com/ot-pg:14-latest", b.getImage().toString()); + b.setImage(DockerImageName.parse("foo").withTag("15-latest")); + assertEquals("foo:15-latest", b.getImage().toString()); + + System.clearProperty(EmbeddedPostgres.ENV_DOCKER_IMAGE); + } + + @Test + public void testDatabaseName() throws IOException, SQLException { + EmbeddedPostgres db = EmbeddedPostgres.builder().start(); + try { + testSpecificDatabaseName(db, EmbeddedPostgres.POSTGRES); + } finally { + db.close(); + } + db = EmbeddedPostgres.builder().setDatabaseName("mike").start(); + try { + testSpecificDatabaseName(db, "mike"); + } finally { + db.close(); + } + + } + + @Test + public void testTemplateDatabase() throws IOException, SQLException { + EmbeddedPostgres db = EmbeddedPostgres.builder().start(); + try { + testSpecificDatabaseName(db.getTemplateDatabase(), db, "template1"); + } finally { + db.close(); + } + } + + private void testSpecificDatabaseName(EmbeddedPostgres db, String expectedName) throws SQLException, IOException { + testSpecificDatabaseName(db.getPostgresDatabase(), db,expectedName); + } + private void testSpecificDatabaseName(DataSource dataSource, EmbeddedPostgres db, String expectedName) throws IOException, SQLException { + try (Connection c = dataSource.getConnection()) { + try (Statement statement = c.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT current_database()")) { + resultSet.next(); + assertEquals(expectedName, resultSet.getString(1)); + } + } + } } diff --git a/src/test/java/com/opentable/db/postgres/embedded/LocalDirectoryPostgresTest.java b/src/test/java/com/opentable/db/postgres/embedded/LocalDirectoryPostgresTest.java index 7f4d80bc..54a9cfc3 100644 --- a/src/test/java/com/opentable/db/postgres/embedded/LocalDirectoryPostgresTest.java +++ b/src/test/java/com/opentable/db/postgres/embedded/LocalDirectoryPostgresTest.java @@ -27,13 +27,12 @@ public class LocalDirectoryPostgresTest { - private static final File USR_LOCAL = new File("/usr/local"); private static final File USR_LOCAL_BIN_POSTGRES = new File("/usr/local/bin/postgres"); @Test public void testEmbeddedPg() throws Exception { Assume.assumeTrue("PostgreSQL binary must exist", USR_LOCAL_BIN_POSTGRES.exists()); - try (EmbeddedPostgres pg = EmbeddedPostgres.builder().setPostgresBinaryDirectory(USR_LOCAL).start(); + try (EmbeddedPostgres pg = EmbeddedPostgres.builder().start(); Connection c = pg.getPostgresDatabase().getConnection()) { Statement s = c.createStatement(); ResultSet rs = s.executeQuery("SELECT 1"); diff --git a/src/test/java/com/opentable/db/postgres/embedded/PreparedDbCustomizerTest.java b/src/test/java/com/opentable/db/postgres/embedded/PreparedDbCustomizerTest.java index 3f2a1045..6f89ffdc 100644 --- a/src/test/java/com/opentable/db/postgres/embedded/PreparedDbCustomizerTest.java +++ b/src/test/java/com/opentable/db/postgres/embedded/PreparedDbCustomizerTest.java @@ -13,15 +13,17 @@ */ package com.opentable.db.postgres.embedded; -import com.opentable.db.postgres.junit.EmbeddedPostgresRules; -import com.opentable.db.postgres.junit.PreparedDbRule; -import org.junit.Rule; -import org.junit.Test; +import static com.opentable.db.postgres.embedded.EmbeddedPostgres.DEFAULT_PG_STARTUP_WAIT; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import java.time.Duration; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; +import org.junit.Rule; +import org.junit.Test; + +import com.opentable.db.postgres.junit.EmbeddedPostgresRules; +import com.opentable.db.postgres.junit.PreparedDbRule; public class PreparedDbCustomizerTest { @@ -32,23 +34,23 @@ public class PreparedDbCustomizerTest { @Rule public PreparedDbRule dbA2 = EmbeddedPostgresRules.preparedDatabase(EMPTY_PREPARER).customize(builder -> {}); @Rule - public PreparedDbRule dbA3 = EmbeddedPostgresRules.preparedDatabase(EMPTY_PREPARER).customize(builder -> builder.setPGStartupWait(Duration.ofSeconds(10))); + public PreparedDbRule dbA3 = EmbeddedPostgresRules.preparedDatabase(EMPTY_PREPARER).customize(builder -> builder.setPGStartupWait(DEFAULT_PG_STARTUP_WAIT)); @Rule - public PreparedDbRule dbB1 = EmbeddedPostgresRules.preparedDatabase(EMPTY_PREPARER).customize(builder -> builder.setPGStartupWait(Duration.ofSeconds(11))); + public PreparedDbRule dbB1 = EmbeddedPostgresRules.preparedDatabase(EMPTY_PREPARER).customize(builder -> builder.setPGStartupWait(Duration.ofSeconds(DEFAULT_PG_STARTUP_WAIT.getSeconds() + 1))); @Rule - public PreparedDbRule dbB2 = EmbeddedPostgresRules.preparedDatabase(EMPTY_PREPARER).customize(builder -> builder.setPGStartupWait(Duration.ofSeconds(11))); + public PreparedDbRule dbB2 = EmbeddedPostgresRules.preparedDatabase(EMPTY_PREPARER).customize(builder -> builder.setPGStartupWait(Duration.ofSeconds(DEFAULT_PG_STARTUP_WAIT.getSeconds() + 1))); @Test public void testCustomizers() { - int dbA1Port = dbA1.getConnectionInfo().getPort(); - int dbA2Port = dbA2.getConnectionInfo().getPort(); - int dbA3Port = dbA3.getConnectionInfo().getPort(); + int dbA1Port = JdbcUrlUtils.getPort(dbA1.getConnectionInfo().getUrl()); + int dbA2Port = JdbcUrlUtils.getPort(dbA2.getConnectionInfo().getUrl()); + int dbA3Port = JdbcUrlUtils.getPort(dbA3.getConnectionInfo().getUrl()); assertEquals(dbA1Port, dbA2Port); assertEquals(dbA1Port, dbA3Port); - int dbB1Port = dbB1.getConnectionInfo().getPort(); - int dbB2Port = dbB2.getConnectionInfo().getPort(); + int dbB1Port = JdbcUrlUtils.getPort(dbB1.getConnectionInfo().getUrl()); + int dbB2Port = JdbcUrlUtils.getPort(dbB2.getConnectionInfo().getUrl()); assertEquals(dbB1Port, dbB2Port);