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