1
2
3
4
5
6
7
8
9
10
11
12
13
14 package de.softwareforge.testing.postgres.embedded;
15
16 import static com.google.common.base.Preconditions.checkNotNull;
17 import static com.google.common.base.Preconditions.checkState;
18 import static de.softwareforge.testing.postgres.embedded.DatabaseInfo.PG_DEFAULT_USER;
19 import static java.lang.String.format;
20
21 import java.io.IOException;
22 import java.sql.Connection;
23 import java.sql.SQLException;
24 import java.sql.Statement;
25 import java.util.Set;
26 import java.util.concurrent.ExecutorService;
27 import java.util.concurrent.Executors;
28 import java.util.concurrent.SynchronousQueue;
29 import java.util.concurrent.atomic.AtomicBoolean;
30 import java.util.function.Supplier;
31 import javax.sql.DataSource;
32
33 import com.google.common.collect.ImmutableSet;
34 import com.google.common.util.concurrent.ThreadFactoryBuilder;
35 import edu.umd.cs.findbugs.annotations.NonNull;
36 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
39
40
41
42
43 public final class DatabaseManager implements AutoCloseable {
44
45 private static final String PG_DEFAULT_ENCODING = "utf8";
46
47 public static final Logger LOG = LoggerFactory.getLogger(DatabaseManager.class);
48
49 private final AtomicBoolean closed = new AtomicBoolean();
50 private final AtomicBoolean started = new AtomicBoolean();
51
52 private final Set<EmbeddedPostgresPreparer<DataSource>> databasePreparers;
53 private final Set<EmbeddedPostgresPreparer<EmbeddedPostgres.Builder>> instancePreparers;
54 private final boolean multiMode;
55
56 private volatile InstanceProvider instanceProvider = null;
57 private volatile EmbeddedPostgres pg = null;
58
59 private DatabaseManager(Set<EmbeddedPostgresPreparer<DataSource>> databasePreparers,
60 Set<EmbeddedPostgresPreparer<EmbeddedPostgres.Builder>> instancePreparers,
61 boolean multiMode) {
62 this.databasePreparers = checkNotNull(databasePreparers, "databasePreparers is null");
63 this.instancePreparers = checkNotNull(instancePreparers, "instancePreparers is null");
64 this.multiMode = multiMode;
65 }
66
67
68
69
70
71
72 @NonNull
73 public static Builder<DatabaseManager> multiDatabases() {
74 return new DatabaseManagerBuilder(true);
75 }
76
77
78
79
80
81
82 @NonNull
83 public static Builder<DatabaseManager> singleDatabase() {
84 return new DatabaseManagerBuilder(false);
85 }
86
87
88
89
90
91
92
93
94 @NonNull
95 public DatabaseManager start() throws IOException, SQLException {
96 if (!started.getAndSet(true)) {
97
98
99 EmbeddedPostgres.Builder builder = EmbeddedPostgres.builder();
100
101 for (EmbeddedPostgresPreparer<EmbeddedPostgres.Builder> instancePreparer : instancePreparers) {
102 instancePreparer.prepare(builder);
103 }
104
105 this.pg = builder.build();
106
107 final DataSource dataSource;
108
109 if (multiMode) {
110
111 dataSource = pg.createTemplateDataSource();
112
113
114 this.instanceProvider = new InstanceProviderPipeline();
115 } else {
116
117 dataSource = pg.createDefaultDataSource();
118
119
120 this.instanceProvider = () -> pg.createDefaultDatabaseInfo();
121 }
122
123 for (EmbeddedPostgresPreparer<DataSource> databasePreparer : databasePreparers) {
124 databasePreparer.prepare(dataSource);
125 }
126
127 this.instanceProvider.start();
128 }
129
130 return this;
131 }
132
133 @Override
134 public void close() throws Exception {
135 checkState(started.get(), "not yet started!");
136 if (!closed.getAndSet(true)) {
137 instanceProvider.close();
138 pg.close();
139 }
140 }
141
142
143
144
145
146
147
148
149 @NonNull
150 public DatabaseInfo getDatabaseInfo() throws SQLException {
151 checkState(started.get(), "not yet started!");
152
153 DatabaseInfo databaseInfo = instanceProvider.get();
154 if (databaseInfo.exception().isPresent()) {
155 throw databaseInfo.exception().get();
156 }
157
158 return databaseInfo;
159 }
160
161
162
163
164
165
166 @NonNull
167 public EmbeddedPostgres getEmbeddedPostgres() {
168 checkState(started.get(), "not yet started!");
169 return pg;
170 }
171
172 private interface InstanceProvider extends Supplier<DatabaseInfo>, AutoCloseable {
173
174 default void start() {
175 }
176
177 @Override
178 default void close() {
179 }
180
181 DatabaseInfo get();
182 }
183
184 private final class InstanceProviderPipeline implements InstanceProvider, Runnable {
185
186 private final ExecutorService executor;
187 private final SynchronousQueue<DatabaseInfo> nextDatabase = new SynchronousQueue<>();
188
189 private final AtomicBoolean closed = new AtomicBoolean();
190
191 InstanceProviderPipeline() {
192 this.executor = Executors.newSingleThreadExecutor(
193 new ThreadFactoryBuilder()
194 .setDaemon(true)
195 .setNameFormat("instance-creator-" + pg.instanceId() + "-%d")
196 .build());
197
198 }
199
200 @Override
201 public void start() {
202 this.executor.submit(this);
203 }
204
205 @Override
206 public void close() {
207 if (!this.closed.getAndSet(true)) {
208 executor.shutdownNow();
209 }
210 }
211
212 @Override
213 public void run() {
214 while (!closed.get()) {
215 try {
216 final String newDbName = EmbeddedUtil.randomLowercase(12);
217 try {
218 createDatabase(pg.createDefaultDataSource(), newDbName);
219 nextDatabase.put(DatabaseInfo.builder()
220 .dbName(newDbName)
221 .port(pg.getPort())
222 .connectionProperties(pg.getConnectionProperties())
223 .build());
224 } catch (SQLException e) {
225
226 if (!e.getSQLState().equals("57P01")) {
227 LOG.warn("Caught SQL Exception (" + e.getSQLState() + "):", e);
228 nextDatabase.put(DatabaseInfo.forException(e));
229 }
230 }
231 } catch (InterruptedException e) {
232 Thread.currentThread().interrupt();
233 return;
234 } catch (Throwable t) {
235 LOG.warn("Caught Throwable in instance provider loop:", t);
236 }
237 }
238 }
239
240 @Override
241 public DatabaseInfo get() {
242 try {
243 return nextDatabase.take();
244 } catch (final InterruptedException e) {
245 Thread.currentThread().interrupt();
246 throw new IllegalStateException(e);
247 }
248 }
249 }
250
251 @SuppressFBWarnings("RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE")
252 private static void createDatabase(final DataSource dataSource, final String databaseName) throws SQLException {
253 try (Connection c = dataSource.getConnection();
254 Statement stmt = c.createStatement()) {
255 stmt.executeUpdate(format("CREATE DATABASE %s OWNER %s ENCODING = '%s'", databaseName, PG_DEFAULT_USER, PG_DEFAULT_ENCODING));
256 }
257 }
258
259
260
261
262
263
264 public abstract static class Builder<T> {
265
266 protected ImmutableSet.Builder<EmbeddedPostgresPreparer<DataSource>> databasePreparers = ImmutableSet.builder();
267 protected ImmutableSet.Builder<EmbeddedPostgresPreparer<EmbeddedPostgres.Builder>> instancePreparers = ImmutableSet.builder();
268 protected final boolean multiMode;
269
270
271
272
273
274
275 protected Builder(boolean multiMode) {
276 this.multiMode = multiMode;
277 }
278
279
280
281
282 @Deprecated
283 @NonNull
284 public Builder<T> withPreparer(@NonNull DatabasePreparer databasePreparer) {
285 checkNotNull(databasePreparer, "databasePreparer is null");
286 return withDatabasePreparer(databasePreparer::prepare);
287 }
288
289
290
291
292
293
294
295
296
297 @NonNull
298 public Builder<T> withDatabasePreparer(@NonNull EmbeddedPostgresPreparer<DataSource> databasePreparer) {
299 this.databasePreparers.add(checkNotNull(databasePreparer, "databasePreparer is null"));
300 return this;
301 }
302
303
304
305
306
307
308
309
310
311 @NonNull
312 public Builder<T> withDatabasePreparers(@NonNull Set<EmbeddedPostgresPreparer<DataSource>> databasePreparers) {
313 this.databasePreparers.addAll(checkNotNull(databasePreparers, "databasePreparers is null"));
314 return this;
315 }
316
317
318
319
320
321
322
323
324 @NonNull
325 public Builder<T> withInstancePreparer(@NonNull EmbeddedPostgresPreparer<EmbeddedPostgres.Builder> instancePreparer) {
326 this.instancePreparers.add(checkNotNull(instancePreparer, "instancePreparer is null"));
327 return this;
328 }
329
330
331
332
333
334
335
336
337 @NonNull
338 public Builder<T> withInstancePreparers(@NonNull Set<EmbeddedPostgresPreparer<EmbeddedPostgres.Builder>> instancePreparers) {
339 this.instancePreparers.addAll(checkNotNull(instancePreparers, "instancePreparers is null"));
340 return this;
341 }
342
343
344
345
346 @Deprecated
347 @NonNull
348 public Builder<T> withCustomizer(@NonNull EmbeddedPostgres.BuilderCustomizer customizer) {
349 checkNotNull(customizer, "customizer is null");
350 this.instancePreparers.add(customizer::customize);
351 return this;
352 }
353
354
355
356
357
358
359 @NonNull
360 public abstract T build();
361 }
362
363
364
365
366 public static final class DatabaseManagerBuilder extends Builder<DatabaseManager> {
367
368
369
370
371
372
373
374 public DatabaseManagerBuilder(boolean multiMode) {
375 super(multiMode);
376 }
377
378
379
380
381
382
383 @Override
384 @NonNull
385 public DatabaseManager build() {
386 return new DatabaseManager(databasePreparers.build(), instancePreparers.build(), multiMode);
387 }
388 }
389 }