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 private interface InstanceProvider extends Supplier<DatabaseInfo>, AutoCloseable {
180
181 default void start() {
182 }
183
184 @Override
185 default void close() {
186 }
187
188 @Override
189 DatabaseInfo get();
190 }
191
192 private final class InstanceProviderPipeline implements InstanceProvider, Runnable {
193
194 private final ExecutorService executor;
195 private final SynchronousQueue<DatabaseInfo> nextDatabase = new SynchronousQueue<>();
196
197 private final AtomicBoolean closed = new AtomicBoolean();
198
199 InstanceProviderPipeline() {
200 this.executor = Executors.newSingleThreadExecutor(
201 new ThreadFactoryBuilder()
202 .setDaemon(true)
203 .setNameFormat("instance-creator-" + pg.instanceId() + "-%d")
204 .build());
205
206 }
207
208 @Override
209 public void start() {
210 this.executor.submit(this);
211 }
212
213 @Override
214 public void close() {
215 if (!this.closed.getAndSet(true)) {
216 executor.shutdownNow();
217 }
218 }
219
220 @Override
221 public void run() {
222 while (!closed.get()) {
223 try {
224 final String newDbName = EmbeddedUtil.randomLowercase(12);
225 try {
226 createDatabase(pg.createDefaultDataSource(), newDbName);
227 nextDatabase.put(DatabaseInfo.builder()
228 .dbName(newDbName)
229 .port(pg.getPort())
230 .connectionProperties(pg.getConnectionProperties())
231 .build());
232 } catch (SQLException e) {
233
234 if (!e.getSQLState().equals("57P01")) {
235 LOG.warn("Caught SQL Exception (" + e.getSQLState() + "):", e);
236 nextDatabase.put(DatabaseInfo.forException(e));
237 }
238 }
239 } catch (InterruptedException e) {
240 Thread.currentThread().interrupt();
241 return;
242 } catch (Exception e) {
243 LOG.warn("Caught exception in instance provider loop:", e);
244 }
245 }
246 }
247
248 @Override
249 public DatabaseInfo get() {
250 try {
251 return nextDatabase.take();
252 } catch (final InterruptedException e) {
253 Thread.currentThread().interrupt();
254 throw new IllegalStateException(e);
255 }
256 }
257 }
258
259 private static void createDatabase(final DataSource dataSource, final String databaseName) throws SQLException {
260 try (Connection c = dataSource.getConnection();
261 Statement stmt = c.createStatement()) {
262 stmt.executeUpdate(format("CREATE DATABASE %s OWNER %s ENCODING = '%s'", databaseName, PG_DEFAULT_USER, PG_DEFAULT_ENCODING));
263 }
264 }
265
266
267
268
269
270
271 public abstract static class Builder<T> {
272
273 protected ImmutableSet.Builder<EmbeddedPostgresPreparer<DataSource>> databasePreparers = ImmutableSet.builder();
274 protected ImmutableSet.Builder<EmbeddedPostgresPreparer<EmbeddedPostgres.Builder>> instancePreparers = ImmutableSet.builder();
275 protected final boolean multiMode;
276
277
278
279
280
281
282 protected Builder(boolean multiMode) {
283 this.multiMode = multiMode;
284 }
285
286
287
288
289
290
291
292
293
294
295 @Nonnull
296 public Builder<T> withDatabasePreparer(@Nonnull EmbeddedPostgresPreparer<DataSource> databasePreparer) {
297 this.databasePreparers.add(checkNotNull(databasePreparer, "databasePreparer is null"));
298 return this;
299 }
300
301
302
303
304
305
306
307
308
309
310 @Nonnull
311 public Builder<T> withDatabasePreparers(@Nonnull Set<EmbeddedPostgresPreparer<DataSource>> databasePreparers) {
312 this.databasePreparers.addAll(checkNotNull(databasePreparers, "databasePreparers is null"));
313 return this;
314 }
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
338 @Nonnull
339 public Builder<T> withInstancePreparers(@Nonnull Set<EmbeddedPostgresPreparer<EmbeddedPostgres.Builder>> instancePreparers) {
340 this.instancePreparers.addAll(checkNotNull(instancePreparers, "instancePreparers is null"));
341 return this;
342 }
343
344
345
346
347
348
349 @Nonnull
350 public abstract T build();
351 }
352
353
354
355
356 public static final class DatabaseManagerBuilder extends Builder<DatabaseManager> {
357
358
359
360
361
362
363
364 public DatabaseManagerBuilder(boolean multiMode) {
365 super(multiMode);
366 }
367
368
369
370
371
372
373 @Override
374 @Nonnull
375 public DatabaseManager build() {
376 return new DatabaseManager(databasePreparers.build(), instancePreparers.build(), multiMode);
377 }
378 }
379 }