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.checkArgument;
18 import static com.google.common.base.Preconditions.checkNotNull;
19 import static com.google.common.base.Preconditions.checkState;
20 import static de.softwareforge.testing.postgres.embedded.DatabaseInfo.PG_DEFAULT_DB;
21 import static de.softwareforge.testing.postgres.embedded.DatabaseInfo.PG_DEFAULT_USER;
22 import static de.softwareforge.testing.postgres.embedded.EmbeddedUtil.formatDuration;
23 import static java.lang.String.format;
24
25 import java.io.File;
26 import java.io.FileOutputStream;
27 import java.io.IOException;
28 import java.io.InputStreamReader;
29 import java.net.ConnectException;
30 import java.net.InetAddress;
31 import java.net.InetSocketAddress;
32 import java.net.Socket;
33 import java.nio.channels.FileLock;
34 import java.nio.channels.OverlappingFileLockException;
35 import java.nio.charset.StandardCharsets;
36 import java.nio.file.Path;
37 import java.sql.Connection;
38 import java.sql.ResultSet;
39 import java.sql.SQLException;
40 import java.sql.Statement;
41 import java.time.Duration;
42 import java.util.HashMap;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.Map.Entry;
46 import java.util.Objects;
47 import java.util.concurrent.TimeUnit;
48 import java.util.concurrent.atomic.AtomicBoolean;
49 import javax.sql.DataSource;
50
51 import com.google.common.annotations.VisibleForTesting;
52 import com.google.common.base.Stopwatch;
53 import com.google.common.collect.ImmutableList;
54 import com.google.common.collect.ImmutableMap;
55 import com.google.common.io.CharStreams;
56 import com.google.common.io.Closeables;
57 import edu.umd.cs.findbugs.annotations.NonNull;
58 import edu.umd.cs.findbugs.annotations.Nullable;
59 import org.postgresql.ds.PGSimpleDataSource;
60 import org.slf4j.Logger;
61 import org.slf4j.LoggerFactory;
62
63
64
65
66 public final class EmbeddedPostgres implements AutoCloseable {
67
68
69
70
71 public static final String DEFAULT_POSTGRES_VERSION = "13";
72
73 static final String[] LOCALHOST_SERVER_NAMES = new String[]{"localhost"};
74
75 private static final String PG_TEMPLATE_DB = "template1";
76
77 @VisibleForTesting
78 static final Duration DEFAULT_PG_STARTUP_WAIT = Duration.ofSeconds(10);
79
80
81 private static final long MINIMUM_AGE_IN_MS = Duration.ofMinutes(10).toMillis();
82
83
84 private static final String DATA_DIRECTORY_PREFIX = "data-";
85
86 private static final String PG_STOP_MODE = "fast";
87 private static final String PG_STOP_WAIT_SECONDS = "5";
88 static final String LOCK_FILE_NAME = "epg-lock";
89
90 private final Logger logger;
91
92 private final String instanceId;
93 private final File postgresInstallDirectory;
94 private final File dataDirectory;
95
96 private final Duration serverStartupWait;
97 private final int port;
98 private final AtomicBoolean started = new AtomicBoolean();
99 private final AtomicBoolean closed = new AtomicBoolean();
100
101 private final ImmutableMap<String, String> serverConfiguration;
102 private final ImmutableMap<String, String> localeConfiguration;
103 private final ImmutableMap<String, String> connectionProperties;
104
105 private final File lockFile;
106 private volatile FileOutputStream lockStream;
107 private volatile FileLock lock;
108
109 private final boolean removeDataOnShutdown;
110
111 private final ProcessBuilder.Redirect errorRedirector;
112 private final ProcessBuilder.Redirect outputRedirector;
113
114
115
116
117
118 @NonNull
119 public static EmbeddedPostgres defaultInstance() throws IOException {
120 return builderWithDefaults().build();
121 }
122
123
124
125
126 @NonNull
127 public static EmbeddedPostgres.Builder builderWithDefaults() {
128 return new Builder().withDefaults();
129 }
130
131
132
133
134 @NonNull
135 public static EmbeddedPostgres.Builder builder() {
136 return new Builder();
137 }
138
139 private EmbeddedPostgres(
140 final String instanceId,
141 final File postgresInstallDirectory,
142 final File dataDirectory,
143 final boolean removeDataOnShutdown,
144 final Map<String, String> serverConfiguration,
145 final Map<String, String> localeConfiguration,
146 final Map<String, String> connectionProperties,
147 final int port,
148 final ProcessBuilder.Redirect errorRedirector,
149 final ProcessBuilder.Redirect outputRedirector,
150 final Duration serverStartupWait) {
151
152 this.instanceId = checkNotNull(instanceId, "instanceId is null");
153
154 this.logger = LoggerFactory.getLogger(toString());
155
156 this.postgresInstallDirectory = checkNotNull(postgresInstallDirectory, "postgresInstallDirectory is null");
157 this.dataDirectory = checkNotNull(dataDirectory, "dataDirectory is null");
158
159 this.removeDataOnShutdown = removeDataOnShutdown;
160
161 this.serverConfiguration = ImmutableMap.copyOf(checkNotNull(serverConfiguration, "serverConfiguration is null"));
162 this.localeConfiguration = ImmutableMap.copyOf(checkNotNull(localeConfiguration, "localeConfiguration is null"));
163 this.connectionProperties = ImmutableMap.copyOf(checkNotNull(connectionProperties, "connectionProperties is null"));
164
165 this.port = port;
166
167 this.errorRedirector = checkNotNull(errorRedirector, "errorRedirector is null");
168 this.outputRedirector = checkNotNull(outputRedirector, "outputRedirector is null");
169
170 this.serverStartupWait = checkNotNull(serverStartupWait, "serverStartupWait is null");
171 this.lockFile = new File(this.dataDirectory, LOCK_FILE_NAME);
172
173 logger.debug(format("data dir is %s, install dir is %s", this.dataDirectory, this.postgresInstallDirectory));
174 }
175
176
177
178
179
180
181
182
183
184 @NonNull
185 public DataSource createTemplateDataSource() throws SQLException {
186 return createDataSource(PG_DEFAULT_USER, PG_TEMPLATE_DB, getPort(), getConnectionProperties());
187 }
188
189
190
191
192
193
194 @NonNull
195 public DataSource createDefaultDataSource() throws SQLException {
196 return createDataSource(PG_DEFAULT_USER, PG_DEFAULT_DB, getPort(), getConnectionProperties());
197 }
198
199
200
201
202
203
204
205 @NonNull
206 public DataSource createDataSource(@NonNull String user, @NonNull String databaseName) throws SQLException {
207 return createDataSource(user, databaseName, getPort(), getConnectionProperties());
208 }
209
210 static DataSource createDataSource(String user, String databaseName, int port, Map<String, String> connectionProperties) throws SQLException {
211 checkNotNull(user, "user is null");
212 checkNotNull(databaseName, "databaseName is null");
213 checkNotNull(connectionProperties, "connectionProperties is null");
214
215 final PGSimpleDataSource ds = new PGSimpleDataSource();
216
217 ds.setServerNames(LOCALHOST_SERVER_NAMES);
218 ds.setPortNumbers(new int[]{port});
219 ds.setDatabaseName(databaseName);
220 ds.setUser(user);
221
222 for (final Entry<String, String> entry : connectionProperties.entrySet()) {
223 ds.setProperty(entry.getKey(), entry.getValue());
224 }
225
226 return ds;
227 }
228
229
230
231
232 public int getPort() {
233 return port;
234 }
235
236
237
238
239 @NonNull
240 ImmutableMap<String, String> getConnectionProperties() {
241 return connectionProperties;
242 }
243
244
245
246
247
248 @NonNull
249 public String instanceId() {
250 return instanceId;
251 }
252
253 @Override
254 public String toString() {
255 return this.getClass().getName() + "$" + this.instanceId;
256 }
257
258 @Override
259 public boolean equals(Object o) {
260 if (this == o) {
261 return true;
262 }
263 if (o == null || getClass() != o.getClass()) {
264 return false;
265 }
266 EmbeddedPostgres/../../de/softwareforge/testing/postgres/embedded/EmbeddedPostgres.html#EmbeddedPostgres">EmbeddedPostgres that = (EmbeddedPostgres) o;
267 return instanceId.equals(that.instanceId);
268 }
269
270 @Override
271 public int hashCode() {
272 return Objects.hash(instanceId);
273 }
274
275
276 DatabaseInfo createDefaultDatabaseInfo() {
277 return DatabaseInfo.builder().port(getPort()).connectionProperties(getConnectionProperties()).build();
278 }
279
280
281 private void boot() throws IOException {
282 EmbeddedUtil.mkdirs(this.dataDirectory);
283
284 if (this.removeDataOnShutdown || !new File(this.dataDirectory, "postgresql.conf").exists()) {
285 initDatabase();
286 }
287
288 lock();
289
290 startDatabase();
291 }
292
293
294 private synchronized void lock() throws IOException {
295 this.lockStream = new FileOutputStream(this.lockFile);
296 this.lock = lockStream.getChannel().tryLock();
297 checkState(lock != null, "could not lock %s", lockFile);
298 }
299
300 private synchronized void unlock() throws IOException {
301 if (lock != null) {
302 lock.release();
303 }
304 Closeables.close(lockStream, true);
305 }
306
307 private void initDatabase() throws IOException {
308 ImmutableList.Builder<String> commandBuilder = ImmutableList.builder();
309 commandBuilder.add(pgBin("initdb"))
310 .addAll(createInitDbOptions())
311 .add("-A", "trust",
312 "-U", PG_DEFAULT_USER,
313 "-D", this.dataDirectory.getPath(),
314 "-E", "UTF-8");
315 final Stopwatch watch = system(commandBuilder.build());
316 logger.debug(format("initdb completed in %s", formatDuration(watch.elapsed())));
317 }
318
319 private void startDatabase() throws IOException {
320 checkState(!started.getAndSet(true), "database already started!");
321
322 final ImmutableList.Builder<String> commandBuilder = ImmutableList.builder();
323 commandBuilder.add(pgBin("pg_ctl"),
324 "-D", this.dataDirectory.getPath(),
325 "-o", String.join(" ", createInitOptions()),
326 "start"
327 );
328
329 final Stopwatch watch = Stopwatch.createStarted();
330 final Process postmaster = spawn("pg", commandBuilder.build());
331
332 logger.info(format("started as pid %d on port %d", postmaster.pid(), port));
333 logger.debug(format("Waiting up to %s for server startup to finish", formatDuration(serverStartupWait)));
334
335 Runtime.getRuntime().addShutdownHook(newCloserThread());
336
337 checkState(waitForServerStartup(), "Could not start pg, interrupted?");
338 logger.debug(format("startup complete in %s", formatDuration(watch.elapsed())));
339 }
340
341 private void stopDatabase(File dataDirectory) throws IOException {
342 final ImmutableList.Builder<String> commandBuilder = ImmutableList.builder();
343 commandBuilder.add(pgBin("pg_ctl"),
344 "-D", dataDirectory.getPath(),
345 "stop",
346 "-m", PG_STOP_MODE,
347 "-t", PG_STOP_WAIT_SECONDS, "-w");
348
349 final Stopwatch watch = system(commandBuilder.build());
350 logger.debug(format("shutdown complete in %s", formatDuration(watch.elapsed())));
351 }
352
353 private List<String> createInitOptions() {
354 final ImmutableList.Builder<String> initOptions = ImmutableList.builder();
355 initOptions.add(
356 "-p", Integer.toString(port),
357 "-F");
358
359 serverConfiguration.forEach((key, value) -> {
360 initOptions.add("-c");
361 if (value.length() > 0) {
362 initOptions.add(key + "=" + value);
363 } else {
364 initOptions.add(key + "=true");
365 }
366 });
367
368 return initOptions.build();
369 }
370
371 @VisibleForTesting
372 List<String> createInitDbOptions() {
373 final ImmutableList.Builder<String> localeOptions = ImmutableList.builder();
374
375 localeConfiguration.forEach((key, value) -> {
376 if (value.length() > 0) {
377 localeOptions.add("--" + key + "=" + value);
378 } else {
379 localeOptions.add("--" + key);
380 }
381 });
382 return localeOptions.build();
383 }
384
385 private boolean waitForServerStartup() throws IOException {
386 Throwable lastCause = null;
387 final long start = System.nanoTime();
388 final long maxWaitNs = TimeUnit.NANOSECONDS.convert(serverStartupWait.toMillis(), TimeUnit.MILLISECONDS);
389 while (System.nanoTime() - start < maxWaitNs) {
390 try {
391 if (verifyReady()) {
392 return true;
393 }
394 } catch (final SQLException e) {
395 lastCause = e;
396 logger.trace("while waiting for server startup:", e);
397 }
398
399 try {
400 Thread.sleep(100);
401 } catch (final InterruptedException e) {
402 Thread.currentThread().interrupt();
403 return false;
404 }
405 }
406 throw new IOException("Gave up waiting for server to start after " + serverStartupWait.toMillis() + "ms", lastCause);
407 }
408
409 private boolean verifyReady() throws IOException, SQLException {
410
411 final InetAddress localhost = InetAddress.getLoopbackAddress();
412 try (Socket sock = new Socket()) {
413 sock.setSoTimeout((int) Duration.ofMillis(500).toMillis());
414 sock.connect(new InetSocketAddress(localhost, port), (int) Duration.ofMillis(500).toMillis());
415 } catch (ConnectException e) {
416 return false;
417 }
418
419
420 try (Connection c = createDefaultDataSource().getConnection();
421 Statement s = c.createStatement();
422 ResultSet rs = s.executeQuery("SELECT 1")) {
423 checkState(rs.next(), "expecting single row");
424 checkState(rs.getInt(1) == 1, "expecting 1 as result");
425 checkState(!rs.next(), "expecting single row");
426 return true;
427 }
428 }
429
430 private Thread newCloserThread() {
431 final Thread closeThread = new Thread(() -> {
432 try {
433 EmbeddedPostgres.this.close();
434 } catch (IOException e) {
435 logger.trace("while closing instance:", e);
436 }
437 });
438
439 closeThread.setName("pg-closer");
440 return closeThread;
441 }
442
443
444
445
446 @Override
447 public void close() throws IOException {
448 if (closed.getAndSet(true)) {
449 return;
450 }
451
452 try {
453 stopDatabase(this.dataDirectory);
454 } catch (final Exception e) {
455 logger.error("could not stop pg:", e);
456 }
457
458 unlock();
459
460 if (removeDataOnShutdown) {
461 try {
462 EmbeddedUtil.rmdirs(dataDirectory);
463 } catch (Exception e) {
464 logger.error(format("Could not clean up directory %s:", dataDirectory.getAbsolutePath()), e);
465 }
466 } else {
467 logger.debug(format("preserved data directory %s", dataDirectory.getAbsolutePath()));
468 }
469 }
470
471 @VisibleForTesting
472 File getDataDirectory() {
473 return dataDirectory;
474 }
475
476 @VisibleForTesting
477 Map<String, String> getLocaleConfiguration() {
478 return localeConfiguration;
479 }
480
481
482 private void cleanOldDataDirectories(File parentDirectory) {
483 final File[] children = parentDirectory.listFiles();
484 if (children == null) {
485 return;
486 }
487 for (final File dir : children) {
488 if (!dir.isDirectory()) {
489 continue;
490 }
491
492
493 if (!dir.getName().startsWith(DATA_DIRECTORY_PREFIX)) {
494 continue;
495 }
496
497
498 final File lockFile = new File(dir, LOCK_FILE_NAME);
499 if (!lockFile.exists()) {
500 continue;
501 }
502
503
504
505
506 if (System.currentTimeMillis() - lockFile.lastModified() < MINIMUM_AGE_IN_MS) {
507 continue;
508 }
509
510 try (FileOutputStream fos = new FileOutputStream(lockFile);
511 FileLock lock = fos.getChannel().tryLock()) {
512 if (lock != null) {
513 logger.debug(format("found stale data directory %s", dir));
514 if (new File(dir, "postmaster.pid").exists()) {
515 try {
516 stopDatabase(dir);
517 logger.debug("shutting down orphaned database!");
518 } catch (Exception e) {
519 logger.warn(format("failed to orphaned database in %s:", dir), e);
520 }
521 }
522 EmbeddedUtil.rmdirs(dir);
523 }
524 } catch (final OverlappingFileLockException e) {
525
526 logger.trace("while cleaning old data directories:", e);
527 } catch (final Exception e) {
528 logger.warn("while cleaning old data directories:", e);
529 }
530 }
531 }
532
533 private String pgBin(String binaryName) {
534 final String extension = EmbeddedUtil.IS_OS_WINDOWS ? ".exe" : "";
535 return new File(this.postgresInstallDirectory, "bin/" + binaryName + extension).getPath();
536 }
537
538 private Process spawn(@Nullable String processName, List<String> commandAndArgs) throws IOException {
539 final ProcessBuilder builder = new ProcessBuilder(commandAndArgs);
540 builder.redirectErrorStream(true);
541 builder.redirectError(errorRedirector);
542 builder.redirectOutput(outputRedirector);
543 final Process process = builder.start();
544
545 processName = processName != null ? processName : process.info().command().map(EmbeddedUtil::getFileBaseName).orElse("<unknown>");
546 String name = format("%s (%d)", processName, process.pid());
547
548 ProcessOutputLogger.logOutput(logger, name, process);
549 return process;
550 }
551
552
553 private Stopwatch system(List<String> commandAndArgs) throws IOException {
554 checkArgument(commandAndArgs.size() > 0, "No commandAndArgs given!");
555 String prefix = EmbeddedUtil.getFileBaseName(commandAndArgs.get(0));
556
557 Stopwatch watch = Stopwatch.createStarted();
558 Process process = spawn(prefix, commandAndArgs);
559 try {
560 if (process.waitFor() != 0) {
561 try (InputStreamReader reader = new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8)) {
562 throw new IllegalStateException(format("Process %s failed%n%s",
563 commandAndArgs, CharStreams.toString(reader)));
564 }
565 }
566 } catch (InterruptedException e) {
567 Thread.currentThread().interrupt();
568 }
569
570 return watch;
571 }
572
573
574
575
576
577
578
579 @Deprecated
580 @FunctionalInterface
581 public interface BuilderCustomizer {
582
583
584
585
586
587
588
589
590 void customize(@NonNull Builder builder) throws IOException, SQLException;
591 }
592
593
594
595
596 public static class Builder {
597
598 private File installationBaseDirectory = null;
599 private File dataDirectory = null;
600
601 private final Map<String, String> serverConfiguration = new HashMap<>();
602 private final Map<String, String> localeConfiguration = new HashMap<>();
603 private boolean removeDataOnShutdown = true;
604 private int port = 0;
605 private String serverVersion = DEFAULT_POSTGRES_VERSION;
606 private final Map<String, String> connectionProperties = new HashMap<>();
607 private NativeBinaryManager nativeBinaryManager = null;
608 private Duration serverStartupWait = DEFAULT_PG_STARTUP_WAIT;
609
610 private ProcessBuilder.Redirect errRedirector = ProcessBuilder.Redirect.PIPE;
611 private ProcessBuilder.Redirect outRedirector = ProcessBuilder.Redirect.PIPE;
612
613 Builder() {
614 }
615
616
617
618
619
620
621
622
623
624
625
626 @NonNull
627 public Builder withDefaults() {
628 serverConfiguration.put("timezone", "UTC");
629 serverConfiguration.put("synchronous_commit", "off");
630 serverConfiguration.put("max_connections", "300");
631 return this;
632 }
633
634
635
636
637
638
639
640 @NonNull
641 public Builder setServerStartupWait(@NonNull Duration serverStartupWait) {
642 checkNotNull(serverStartupWait, "serverStartupWait is null");
643 checkArgument(!serverStartupWait.isNegative(), "Negative durations are not permitted.");
644
645 this.serverStartupWait = serverStartupWait;
646 return this;
647 }
648
649
650
651
652
653
654
655
656 @NonNull
657 public Builder setRemoveDataOnShutdown(boolean removeDataOnShutdown) {
658 this.removeDataOnShutdown = removeDataOnShutdown;
659 return this;
660 }
661
662
663
664
665
666
667
668
669 @NonNull
670 public Builder setDataDirectory(@NonNull Path dataDirectory) {
671 checkNotNull(dataDirectory, "dataDirectory is null");
672 return setDataDirectory(dataDirectory.toFile());
673 }
674
675
676
677
678
679
680
681
682 @NonNull
683 public Builder setDataDirectory(@NonNull String dataDirectory) {
684 checkNotNull(dataDirectory, "dataDirectory is null");
685 return setDataDirectory(new File(dataDirectory));
686 }
687
688
689
690
691
692
693
694
695 @NonNull
696 public Builder setDataDirectory(@NonNull File dataDirectory) {
697 this.dataDirectory = checkNotNull(dataDirectory, "dataDirectory is null");
698 return this;
699 }
700
701
702
703
704
705
706
707
708
709
710
711
712 @NonNull
713 public Builder addServerConfiguration(@NonNull String key, @NonNull String value) {
714 checkNotNull(key, "key is null");
715 checkNotNull(value, "value is null");
716 this.serverConfiguration.put(key, value);
717 return this;
718 }
719
720
721
722
723 @Deprecated
724 @NonNull
725 public Builder addLocaleConfiguration(@NonNull String key, @NonNull String value) {
726 checkNotNull(key, "key is null");
727 checkNotNull(value, "value is null");
728 this.localeConfiguration.put(key, value);
729 return this;
730 }
731
732
733
734
735
736
737
738
739
740
741
742
743
744 @NonNull
745 public Builder addInitDbConfiguration(@NonNull String key, @NonNull String value) {
746 checkNotNull(key, "key is null");
747 checkNotNull(value, "value is null");
748 this.localeConfiguration.put(key, value);
749 return this;
750 }
751
752
753
754
755
756
757
758
759
760
761 @NonNull
762 public Builder addConnectionProperty(@NonNull String key, @NonNull String value) {
763 checkNotNull(key, "key is null");
764 checkNotNull(value, "value is null");
765 this.connectionProperties.put(key, value);
766 return this;
767 }
768
769
770
771
772
773
774
775
776
777
778 @NonNull
779 public Builder setInstallationBaseDirectory(@NonNull File installationBaseDirectory) {
780 checkNotNull(installationBaseDirectory, "installationBaseDirectory is null");
781 this.installationBaseDirectory = installationBaseDirectory;
782 this.nativeBinaryManager = null;
783 return this;
784 }
785
786
787
788
789
790
791
792
793 @NonNull
794 public Builder setPort(int port) {
795 checkState(port > 1023 && port < 65535, "Port %s is not within 1024..65535", port);
796 this.port = port;
797 return this;
798 }
799
800
801
802
803
804
805
806
807
808
809 @NonNull
810 public Builder setServerVersion(@NonNull String serverVersion) {
811 this.serverVersion = checkNotNull(serverVersion, "serverVersion is null");
812
813 return this;
814 }
815
816
817
818
819
820
821
822 @NonNull
823 public Builder setErrorRedirector(@NonNull ProcessBuilder.Redirect errRedirector) {
824 this.errRedirector = checkNotNull(errRedirector, "errRedirector is null");
825 return this;
826 }
827
828
829
830
831
832
833
834 @NonNull
835 public Builder setOutputRedirector(@NonNull ProcessBuilder.Redirect outRedirector) {
836 this.outRedirector = checkNotNull(outRedirector, "outRedirector is null");
837 return this;
838 }
839
840
841
842
843
844
845
846
847
848 @NonNull
849 public Builder setNativeBinaryManager(@NonNull NativeBinaryManager nativeBinaryManager) {
850 this.nativeBinaryManager = checkNotNull(nativeBinaryManager, "nativeBinaryManager is null");
851 return this;
852 }
853
854
855
856
857
858
859
860
861
862 @NonNull
863 public Builder useLocalPostgresInstallation(@NonNull File directory) {
864 checkNotNull(directory, "directory is null");
865 checkState(directory.exists() && directory.isDirectory(), "'%s' either does not exist or is not a directory!", directory);
866 return setNativeBinaryManager(() -> directory);
867 }
868
869
870
871
872
873
874
875 @NonNull
876 public EmbeddedPostgres build() throws IOException {
877
878 final String instanceId = EmbeddedUtil.randomAlphaNumeric(16);
879
880 int port = this.port != 0 ? this.port : EmbeddedUtil.allocatePort();
881
882
883 final File parentDirectory = EmbeddedUtil.getWorkingDirectory();
884 EmbeddedUtil.mkdirs(parentDirectory);
885
886 NativeBinaryManager nativeBinaryManager = this.nativeBinaryManager;
887 if (nativeBinaryManager == null) {
888 final String serverVersion = System.getProperty("pg-embedded.postgres-version", this.serverVersion);
889 nativeBinaryManager = new TarXzCompressedBinaryManager(new ZonkyIOPostgresLocator(serverVersion));
890 }
891
892
893 File installationBaseDirectory = Objects.requireNonNullElse(this.installationBaseDirectory, parentDirectory);
894 nativeBinaryManager.setInstallationBaseDirectory(installationBaseDirectory);
895
896
897 final File postgresInstallDirectory = nativeBinaryManager.getLocation();
898
899 File dataDirectory = this.dataDirectory;
900 if (dataDirectory == null) {
901 dataDirectory = new File(parentDirectory, DATA_DIRECTORY_PREFIX + instanceId);
902 }
903
904 EmbeddedPostgresbeddedPostgres.html#EmbeddedPostgres">EmbeddedPostgres embeddedPostgres = new EmbeddedPostgres(instanceId, postgresInstallDirectory, dataDirectory,
905 removeDataOnShutdown, serverConfiguration, localeConfiguration, connectionProperties,
906 port, errRedirector, outRedirector,
907 serverStartupWait);
908
909 embeddedPostgres.cleanOldDataDirectories(parentDirectory);
910
911 embeddedPostgres.boot();
912
913 return embeddedPostgres;
914 }
915 }
916 }