Skip to content

Commit 964f408

Browse files
authored
Merge pull request #37 from zonkyio/docker-databases
#22 implement support for database providers
2 parents 55c2e3c + 0fe5d69 commit 964f408

File tree

50 files changed

+4011
-207
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+4011
-207
lines changed

README.md

Lines changed: 245 additions & 44 deletions
Large diffs are not rendered by default.

build.gradle

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import java.util.regex.Matcher
22

33
plugins {
44
id 'net.researchgate.release' version '2.6.0'
5+
id 'nebula.optional-base' version '3.3.0'
56
}
67

78
ext {
@@ -42,6 +43,7 @@ subprojects {
4243
apply plugin: 'java'
4344
apply plugin: 'maven'
4445
apply plugin: 'signing'
46+
apply plugin: 'nebula.optional-base'
4547

4648
sourceCompatibility = 1.8
4749

@@ -149,16 +151,18 @@ project(':embedded-database-spring-test') {
149151
compile project(':embedded-database-spring-test-autoconfigure')
150152

151153
compile 'io.zonky.test:embedded-postgres:1.2.1'
152-
compile 'org.springframework:spring-context:4.3.21.RELEASE'
153-
compile 'org.springframework:spring-test:4.3.21.RELEASE'
154+
compile 'org.testcontainers:postgresql:1.10.3', optional
155+
compile 'com.opentable.components:otj-pg-embedded:0.13.1', optional
156+
compile 'ru.yandex.qatools.embed:postgresql-embedded:2.10', optional
157+
158+
compile 'org.springframework:spring-context:4.3.22.RELEASE'
159+
compile 'org.springframework:spring-test:4.3.22.RELEASE'
154160
compile "org.flywaydb:flyway-core:$flywayCoreVersion"
155161
compile "org.flywaydb.flyway-test-extensions:flyway-spring-test:$flywayTestVersion"
156162
compile 'com.google.guava:guava:23.0'
157163
compile 'org.tukaani:xz:1.8'
158-
159-
testCompile 'junit:junit:4.12'
160-
testCompile 'org.mockito:mockito-all:1.9.5'
161-
testCompile 'org.assertj:assertj-core:3.6.1'
164+
165+
testCompile 'org.springframework.boot:spring-boot-starter-test:1.5.19.RELEASE'
162166
testCompile 'ch.qos.logback:logback-classic:1.2.3'
163167
}
164168

embedded-database-spring-test/src/main/java/io/zonky/test/db/AutoConfigureEmbeddedDatabase.java

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,8 @@
2525
import java.lang.annotation.Target;
2626

2727
/**
28-
* Annotation that can be applied to a test class to configure a embedded test database to use
28+
* Annotation that can be applied to a test class to configure an embedded database to use
2929
* instead of any application defined {@link DataSource}.
30-
* </p>
31-
* This annotation is handled and processed by {@link io.zonky.test.db.postgres.EmbeddedPostgresContextCustomizerFactory}.
3230
*
3331
* @see io.zonky.test.db.postgres.EmbeddedPostgresContextCustomizerFactory
3432
* @see io.zonky.test.db.flyway.OptimizedFlywayTestExecutionListener
@@ -40,10 +38,11 @@
4038
public @interface AutoConfigureEmbeddedDatabase {
4139

4240
/**
43-
* If this attribute is set then a new data source
44-
* with the specified name is created instead of overriding an existing one.
41+
* The bean name to be used to identify the data source that will be replaced.
42+
* It is only necessary if there is no existing DataSource
43+
* or the context contains multiple DataSource beans.
4544
*
46-
* @return the name of a new data source bean to be created
45+
* @return the name to identify the DataSource bean
4746
*/
4847
String beanName() default "";
4948

@@ -62,6 +61,16 @@
6261
*/
6362
EmbeddedDatabaseType type() default EmbeddedDatabaseType.POSTGRES;
6463

64+
/**
65+
* Provider used to create the underlying embedded database,
66+
* see the documentation for the comparision matrix.
67+
* Note that the provider can also be configured
68+
* through {@code embedded-database.provider} property.
69+
*
70+
* @return the provider of an embedded database
71+
*/
72+
DatabaseProvider provider() default DatabaseProvider.DEFAULT;
73+
6574
/**
6675
* What the test database can replace.
6776
*/
@@ -80,14 +89,47 @@ enum Replace {
8089
}
8190

8291
/**
83-
* A supported embedded database type.
92+
* The supported types of embedded databases.
8493
*/
8594
enum EmbeddedDatabaseType {
8695

8796
/**
88-
* The Embedded PostgreSQL Database
97+
* PostgreSQL Database
8998
*/
9099
POSTGRES
91100

92101
}
102+
103+
/**
104+
* The supported providers of embedded databases.
105+
*/
106+
enum DatabaseProvider {
107+
108+
/**
109+
* Default typically equals to {@link #ZONKY} provider,
110+
* unless a different default has been configured by externalized configuration.
111+
*/
112+
DEFAULT,
113+
114+
/**
115+
* Run the embedded database in a Docker container.
116+
*/
117+
DOCKER,
118+
119+
/**
120+
* Use Zonky's fork of OpenTable Embedded PostgreSQL Component to create the embedded database (https://github.com/zonkyio/embedded-postgres).
121+
*/
122+
ZONKY,
123+
124+
/**
125+
* Use OpenTable Embedded PostgreSQL Component to create the embedded database (https://github.com/opentable/otj-pg-embedded).
126+
*/
127+
OPENTABLE,
128+
129+
/**
130+
* Use Yandex's Embedded PostgreSQL Server to create the embedded database (https://github.com/yandex-qatools/postgresql-embedded).
131+
*/
132+
YANDEX,
133+
134+
}
93135
}

embedded-database-spring-test/src/main/java/io/zonky/test/db/flyway/DefaultFlywayDataSourceContext.java

Lines changed: 17 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -16,41 +16,33 @@
1616

1717
package io.zonky.test.db.flyway;
1818

19-
import com.google.common.cache.CacheBuilder;
20-
import com.google.common.cache.CacheLoader;
21-
import com.google.common.cache.LoadingCache;
22-
import com.google.common.collect.ImmutableList;
23-
import io.zonky.test.db.postgres.embedded.DatabasePreparer;
24-
import io.zonky.test.db.postgres.embedded.EmbeddedPostgres.Builder;
25-
import io.zonky.test.db.postgres.embedded.PreparedDbProvider;
19+
import io.zonky.test.db.provider.DatabaseDescriptor;
20+
import io.zonky.test.db.provider.DatabasePreparer;
21+
import io.zonky.test.db.provider.DatabaseType;
22+
import io.zonky.test.db.provider.GenericDatabaseProvider;
23+
import io.zonky.test.db.provider.ProviderType;
2624
import org.apache.commons.lang3.exception.ExceptionUtils;
2725
import org.flywaydb.core.Flyway;
28-
import org.postgresql.ds.PGSimpleDataSource;
2926
import org.springframework.beans.factory.annotation.Autowired;
27+
import org.springframework.core.env.Environment;
3028
import org.springframework.core.task.TaskExecutor;
3129
import org.springframework.util.concurrent.CompletableToListenableFutureAdapter;
3230
import org.springframework.util.concurrent.ListenableFuture;
3331

3432
import javax.sql.DataSource;
3533
import java.io.IOException;
36-
import java.sql.SQLException;
37-
import java.time.Duration;
38-
import java.util.ArrayList;
39-
import java.util.List;
4034
import java.util.Objects;
4135
import java.util.concurrent.CompletableFuture;
4236
import java.util.concurrent.CompletionException;
4337
import java.util.concurrent.Executor;
44-
import java.util.concurrent.Semaphore;
45-
import java.util.function.Consumer;
4638

4739
import static com.google.common.base.Preconditions.checkState;
4840

4941
/**
5042
* Default implementation of {@link FlywayDataSourceContext} that is used for deferred initialization of the embedded database.
5143
* Note that this target source is dynamic and supports hot reloading while the application is running.
5244
* <p/>
53-
* For the reloading of the underlying data source is used cacheable {@link io.zonky.test.db.postgres.embedded.DatabasePreparer},
45+
* For the reloading of the underlying data source is used cacheable {@link DatabasePreparer},
5446
* which can utilize a special template database to effective copy data into multiple independent databases.
5547
*
5648
* @see io.zonky.test.db.postgres.FlywayEmbeddedPostgresDataSourceFactoryBean
@@ -59,29 +51,17 @@
5951
*/
6052
public class DefaultFlywayDataSourceContext implements FlywayDataSourceContext {
6153

62-
protected static final int MAX_DATABASE_CONNECTIONS = 300;
6354
protected static final int DEFAULT_MAX_RETRY_ATTEMPTS = 2;
6455

65-
protected static final Consumer<Builder> DEFAULT_DATABASE_CONFIGURATION = builder -> {
66-
builder.setPGStartupWait(Duration.ofSeconds(20L));
67-
};
68-
69-
protected static final Consumer<Builder> FORCED_DATABASE_CONFIGURATION =
70-
builder -> builder.setServerConfig("max_connections", String.valueOf(MAX_DATABASE_CONNECTIONS));
71-
72-
protected static final LoadingCache<Integer, Semaphore> CONNECTION_SEMAPHORES = CacheBuilder.newBuilder()
73-
.build(new CacheLoader<Integer, Semaphore>() {
74-
public Semaphore load(Integer key) {
75-
return new Semaphore(MAX_DATABASE_CONNECTIONS);
76-
}
77-
});
78-
7956
protected static final ThreadLocal<DataSource> preparerDataSourceHolder = new ThreadLocal<>();
8057

8158
protected volatile CompletableFuture<DataSource> dataSourceFuture = CompletableFuture.completedFuture(null);
8259

83-
@Autowired(required = false)
84-
protected List<Consumer<Builder>> databaseCustomizers = new ArrayList<>();
60+
@Autowired
61+
protected Environment environment;
62+
63+
@Autowired
64+
protected GenericDatabaseProvider databaseProvider;
8565

8666
protected int maxAttempts = DEFAULT_MAX_RETRY_ATTEMPTS;
8767

@@ -114,29 +94,21 @@ public Object getTarget() throws Exception {
11494
}
11595

11696
@Override
117-
public void releaseTarget(Object target) throws Exception {
97+
public void releaseTarget(Object target) {
11898
// nothing to do
11999
}
120100

121101
@Override
122102
public synchronized ListenableFuture<DataSource> reload(Flyway flyway) {
123103
Executor executor = bootstrapExecutor != null ? bootstrapExecutor : Runnable::run;
124104

125-
List<Consumer<Builder>> customizers = ImmutableList.<Consumer<Builder>>builder()
126-
.add(DEFAULT_DATABASE_CONFIGURATION)
127-
.addAll(databaseCustomizers)
128-
.add(FORCED_DATABASE_CONFIGURATION)
129-
.build();
130-
131105
CompletableFuture<DataSource> reloadFuture = dataSourceFuture.thenApplyAsync(x -> {
132106
for (int current = 1; current <= maxAttempts; current++) {
133107
try {
108+
String providerName = environment.getProperty("embedded-database.provider", ProviderType.ZONKY.toString());
134109
FlywayDatabasePreparer preparer = new FlywayDatabasePreparer(flyway);
135-
PreparedDbProvider provider = PreparedDbProvider.forPreparer(preparer, customizers);
136-
137-
PGSimpleDataSource dataSource = provider.createDataSource().unwrap(PGSimpleDataSource.class);
138-
Semaphore semaphore = CONNECTION_SEMAPHORES.get(dataSource.getPortNumber());
139-
return new BlockingDataSourceWrapper(dataSource, semaphore);
110+
DatabaseDescriptor descriptor = new DatabaseDescriptor(DatabaseType.POSTGRES, ProviderType.valueOf(providerName));
111+
return databaseProvider.getDatabase(preparer, descriptor);
140112
} catch (Exception e) {
141113
if (ExceptionUtils.indexOfType(e, IOException.class) == -1 || current == maxAttempts) {
142114
throw new CompletionException(e);
@@ -196,7 +168,7 @@ public FlywayDatabasePreparer(Flyway flyway) {
196168
}
197169

198170
@Override
199-
public void prepare(DataSource ds) throws SQLException {
171+
public void prepare(DataSource ds) {
200172
preparerDataSourceHolder.set(ds);
201173
try {
202174
flyway.migrate();

embedded-database-spring-test/src/main/java/io/zonky/test/db/logging/EmbeddedDatabaseReporter.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package io.zonky.test.db.logging;
1818

19+
import org.apache.commons.lang3.StringUtils;
1920
import org.postgresql.ds.common.BaseDataSource;
2021
import org.slf4j.Logger;
2122
import org.slf4j.LoggerFactory;
@@ -41,7 +42,11 @@ public static void reportDataSource(DataSource dataSource, AnnotatedElement elem
4142
private static String getJdbcUrl(DataSource dataSource) {
4243
try {
4344
BaseDataSource ds = dataSource.unwrap(BaseDataSource.class);
44-
return String.format(JDBC_FORMAT, ds.getPortNumber(), ds.getDatabaseName(), ds.getUser());
45+
if (StringUtils.isBlank(ds.getPassword())) {
46+
return String.format(JDBC_FORMAT, ds.getPortNumber(), ds.getDatabaseName(), ds.getUser());
47+
} else {
48+
return String.format(JDBC_FORMAT + "&password=%s", ds.getPortNumber(), ds.getDatabaseName(), ds.getUser(), ds.getPassword());
49+
}
4550
} catch (Exception e) {
4651
logger.warn("Unexpected error occurred while resolving url to the embedded database", e);
4752
return "unknown";

embedded-database-spring-test/src/main/java/io/zonky/test/db/postgres/EmbeddedPostgresContextCustomizerFactory.java

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,16 @@
1818

1919
import com.google.common.collect.ImmutableMap;
2020
import io.zonky.test.db.AutoConfigureEmbeddedDatabase;
21-
import io.zonky.test.db.AutoConfigureEmbeddedDatabase.EmbeddedDatabaseType;
21+
import io.zonky.test.db.AutoConfigureEmbeddedDatabase.DatabaseProvider;
2222
import io.zonky.test.db.AutoConfigureEmbeddedDatabase.Replace;
2323
import io.zonky.test.db.flyway.DefaultFlywayDataSourceContext;
2424
import io.zonky.test.db.flyway.FlywayClassUtils;
2525
import io.zonky.test.db.flyway.FlywayDataSourceContext;
26+
import io.zonky.test.db.provider.impl.DockerPostgresDatabaseProvider;
27+
import io.zonky.test.db.provider.impl.OpenTablePostgresDatabaseProvider;
28+
import io.zonky.test.db.provider.impl.PrefetchingDatabaseProvider;
29+
import io.zonky.test.db.provider.impl.YandexPostgresDatabaseProvider;
30+
import io.zonky.test.db.provider.impl.ZonkyPostgresDatabaseProvider;
2631
import org.apache.commons.lang3.StringUtils;
2732
import org.flywaydb.core.Flyway;
2833
import org.flywaydb.test.annotation.FlywayTest;
@@ -46,12 +51,15 @@
4651
import org.springframework.test.context.ContextCustomizer;
4752
import org.springframework.test.context.ContextCustomizerFactory;
4853
import org.springframework.test.context.MergedContextConfiguration;
54+
import org.springframework.util.Assert;
4955
import org.springframework.util.ObjectUtils;
5056

5157
import javax.sql.DataSource;
5258
import java.lang.reflect.AnnotatedElement;
5359
import java.util.List;
5460

61+
import static io.zonky.test.db.AutoConfigureEmbeddedDatabase.EmbeddedDatabaseType;
62+
5563
/**
5664
* Implementation of the {@link org.springframework.test.context.ContextCustomizerFactory} interface,
5765
* which is responsible for initialization of the embedded postgres database and its registration to the application context.
@@ -89,7 +97,7 @@ public PreloadableEmbeddedPostgresContextCustomizer(AutoConfigureEmbeddedDatabas
8997

9098
@Override
9199
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
92-
context.addBeanFactoryPostProcessor(new EnvironmentPostProcessor(context.getEnvironment()));
100+
context.addBeanFactoryPostProcessor(new EnvironmentPostProcessor(context.getEnvironment(), databaseAnnotation));
93101

94102
Class<?> testClass = mergedConfig.getTestClass();
95103
FlywayTest[] flywayAnnotations = findFlywayTestAnnotations(testClass);
@@ -126,16 +134,24 @@ public int hashCode() {
126134
protected static class EnvironmentPostProcessor implements BeanDefinitionRegistryPostProcessor {
127135

128136
private final ConfigurableEnvironment environment;
137+
private final AutoConfigureEmbeddedDatabase databaseAnnotation;
129138

130-
public EnvironmentPostProcessor(ConfigurableEnvironment environment) {
139+
public EnvironmentPostProcessor(ConfigurableEnvironment environment, AutoConfigureEmbeddedDatabase databaseAnnotation) {
131140
this.environment = environment;
141+
this.databaseAnnotation = databaseAnnotation;
132142
}
133143

134144
@Override
135145
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
146+
ImmutableMap.Builder<String, Object> builder = ImmutableMap.builder();
147+
builder.put("spring.test.database.replace", "NONE");
148+
149+
if (databaseAnnotation.provider() != DatabaseProvider.DEFAULT) {
150+
builder.put("embedded-database.provider", databaseAnnotation.provider().toString());
151+
}
152+
136153
environment.getPropertySources().addFirst(new MapPropertySource(
137-
PreloadableEmbeddedPostgresContextCustomizer.class.getSimpleName(),
138-
ImmutableMap.of("spring.test.database.replace", "NONE")));
154+
PreloadableEmbeddedPostgresContextCustomizer.class.getSimpleName(), builder.build()));
139155
}
140156

141157
@Override
@@ -156,8 +172,16 @@ public PreloadableEmbeddedPostgresRegistrar(AutoConfigureEmbeddedDatabase databa
156172

157173
@Override
158174
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
175+
Assert.isInstanceOf(ConfigurableListableBeanFactory.class, registry,
176+
"Embedded Database Auto-configuration can only be used with a ConfigurableListableBeanFactory");
159177
ConfigurableListableBeanFactory beanFactory = (ConfigurableListableBeanFactory) registry;
160178

179+
registerBeanIfMissing(registry, "defaultDatabaseProvider", PrefetchingDatabaseProvider.class);
180+
registerBeanIfMissing(registry, "dockerPostgresProvider", DockerPostgresDatabaseProvider.class);
181+
registerBeanIfMissing(registry, "zonkyPostgresProvider", ZonkyPostgresDatabaseProvider.class);
182+
registerBeanIfMissing(registry, "openTablePostgresProvider", OpenTablePostgresDatabaseProvider.class);
183+
registerBeanIfMissing(registry, "yandexPostgresProvider", YandexPostgresDatabaseProvider.class);
184+
161185
BeanDefinitionHolder dataSourceInfo = getDataSourceBeanDefinition(beanFactory, databaseAnnotation);
162186

163187
RootBeanDefinition dataSourceDefinition = new RootBeanDefinition();
@@ -198,6 +222,13 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
198222
}
199223
}
200224

225+
protected static void registerBeanIfMissing(BeanDefinitionRegistry registry, String beanName, Class<?> beanClass) {
226+
if (!registry.containsBeanDefinition(beanName)) {
227+
RootBeanDefinition providerDefinition = new RootBeanDefinition(beanClass);
228+
registry.registerBeanDefinition(beanName, providerDefinition);
229+
}
230+
}
231+
201232
protected static BeanDefinitionRegistry getBeanDefinitionRegistry(ApplicationContext context) {
202233
if (context instanceof BeanDefinitionRegistry) {
203234
return (BeanDefinitionRegistry) context;

0 commit comments

Comments
 (0)