001/* 002 * Licensed under the Apache License, Version 2.0 (the "License"); 003 * you may not use this file except in compliance with the License. 004 * You may obtain a copy of the License at 005 * 006 * http://www.apache.org/licenses/LICENSE-2.0 007 * 008 * Unless required by applicable law or agreed to in writing, software 009 * distributed under the License is distributed on an "AS IS" BASIS, 010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 011 * See the License for the specific language governing permissions and 012 * limitations under the License. 013 */ 014 015package de.softwareforge.testing.postgres.embedded; 016 017import static com.google.common.base.Preconditions.checkNotNull; 018import static com.google.common.base.Preconditions.checkState; 019import static de.softwareforge.testing.postgres.embedded.DatabaseInfo.PG_DEFAULT_USER; 020import static java.lang.String.format; 021 022import jakarta.annotation.Nonnull; 023import java.io.IOException; 024import java.sql.Connection; 025import java.sql.SQLException; 026import java.sql.Statement; 027import java.util.Set; 028import java.util.concurrent.ExecutorService; 029import java.util.concurrent.Executors; 030import java.util.concurrent.SynchronousQueue; 031import java.util.concurrent.atomic.AtomicBoolean; 032import java.util.function.Supplier; 033import javax.sql.DataSource; 034 035import com.google.common.collect.ImmutableSet; 036import com.google.common.util.concurrent.ThreadFactoryBuilder; 037import org.slf4j.Logger; 038import org.slf4j.LoggerFactory; 039 040/** 041 * Controls database instances on a PostgreSQL server instances. 042 */ 043public final class DatabaseManager implements AutoCloseable { 044 045 private static final String PG_DEFAULT_ENCODING = "utf8"; 046 047 private static final Logger LOG = LoggerFactory.getLogger(DatabaseManager.class); 048 049 private final AtomicBoolean closed = new AtomicBoolean(); 050 private final AtomicBoolean started = new AtomicBoolean(); 051 052 private final Set<EmbeddedPostgresPreparer<DataSource>> databasePreparers; 053 private final Set<EmbeddedPostgresPreparer<EmbeddedPostgres.Builder>> instancePreparers; 054 private final boolean multiMode; 055 056 private volatile InstanceProvider instanceProvider = null; 057 private volatile EmbeddedPostgres pg = null; 058 059 private DatabaseManager(Set<EmbeddedPostgresPreparer<DataSource>> databasePreparers, 060 Set<EmbeddedPostgresPreparer<EmbeddedPostgres.Builder>> instancePreparers, 061 boolean multiMode) { 062 this.databasePreparers = checkNotNull(databasePreparers, "databasePreparers is null"); 063 this.instancePreparers = checkNotNull(instancePreparers, "instancePreparers is null"); 064 this.multiMode = multiMode; 065 } 066 067 /** 068 * Creates a new {@link Builder<DatabaseManager>} instance that will create a new database on each call to {@link DatabaseManager#getDatabaseInfo()}. 069 * 070 * @return A builder instance. 071 */ 072 @Nonnull 073 public static Builder<DatabaseManager> multiDatabases() { 074 return new DatabaseManagerBuilder(true); 075 } 076 077 /** 078 * Creates a new {@link Builder<DatabaseManager>} instance that will return a connection to the same database on each call to 079 * {@link DatabaseManager#getDatabaseInfo()}. 080 * 081 * @return A builder instance. 082 */ 083 @Nonnull 084 public static Builder<DatabaseManager> singleDatabase() { 085 return new DatabaseManagerBuilder(false); 086 } 087 088 /** 089 * Start the database server and the machinery that will provide new database instances. 090 * 091 * @return This object. 092 * @throws IOException The server could not be started. 093 * @throws SQLException A SQL problem occured while trying to initialize the database. 094 */ 095 @Nonnull 096 public DatabaseManager start() throws IOException, SQLException { 097 if (!started.getAndSet(true)) { 098 099 // bring up the embedded postgres server and call all instance preparer instances on it. 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 // apply database setup to the template database. 112 dataSource = pg.createTemplateDataSource(); 113 114 // the provider pipeline will create new instances based on the template database. 115 this.instanceProvider = new InstanceProviderPipeline(); 116 } else { 117 // apply database setup to the default database. 118 dataSource = pg.createDefaultDataSource(); 119 120 // always return a reference to the default database. 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 * Returns a {@link DatabaseInfo} instance that describes a database. If this database provider is in multi mode, every call to this method will return a 151 * new database instance. If it is in single mode, it will always return the same database instance. 152 * 153 * @return A {@link DatabaseInfo} instance. This is never null. 154 * @throws SQLException Any error that happened during the database creation is thrown here. 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 * Return the {@link EmbeddedPostgres} instance that manages the database server which holds all of the databases managed by this database manager. 170 * 171 * @return An {@link EmbeddedPostgres} instance. Never null. 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 // https://www.postgresql.org/docs/13/errcodes-appendix.html - 57P01 admin_shutdown 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 * Builder template. 268 * 269 * @param <T> Object to create. 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 * Creates a new builder. 279 * 280 * @param multiMode True if the resulting object should be in multi mode (create multiple database instances) or single mode (use only one instance). 281 */ 282 protected Builder(boolean multiMode) { 283 this.multiMode = multiMode; 284 } 285 286 /** 287 * Add a preparer for the template database. Each preparer is called once when the database manager starts to prepare the template database. This can be 288 * used to create tables, sequences etc. or preload the databases with information. In multi database mode, the template database is used and each 289 * created database will have this information cloned. 290 * 291 * @param databasePreparer A {@link EmbeddedPostgresPreparer<DataSource>} instance. Must not be null. 292 * @return This object instance. 293 * @since 3.0 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 * Add preparers for the template database. Each preparer is called once when the database manager starts to prepare the template database. This can be 303 * used to create tables, sequences etc. or preload the databases with information. In multi database mode, the template database is used and each 304 * created database will have this information cloned. 305 * 306 * @param databasePreparers A set of {@link EmbeddedPostgresPreparer<DataSource>} instances. Must not be null. 307 * @return This object instance. 308 * @since 3.0 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 * Add a preparer for the {@link EmbeddedPostgres.Builder} object. Each preparer is called once when the {@link EmbeddedPostgres} instance that manages 318 * the server is created. 319 * 320 * @param instancePreparer A {@link EmbeddedPostgresPreparer<EmbeddedPostgres.Builder>} instance. Must not be null. 321 * @return This object instance. 322 * @since 3.0 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 * Add preparers for the {@link EmbeddedPostgres.Builder} object. Each preparer is called once when the {@link EmbeddedPostgres} instance that manages 332 * the server is created. 333 * 334 * @param instancePreparers A set of {@link EmbeddedPostgresPreparer<EmbeddedPostgres.Builder>} instances. Must not be null. 335 * @return This object instance. 336 * @since 3.0 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 * Creates a new instance. 346 * 347 * @return The instance to create. 348 */ 349 @Nonnull 350 public abstract T build(); 351 } 352 353 /** 354 * Create new {@link DatabaseManager} instances. 355 */ 356 public static final class DatabaseManagerBuilder extends Builder<DatabaseManager> { 357 358 /** 359 * Creates a new builder for {@link DatabaseManager} instances. 360 * 361 * @param multiMode True if the database manager should return a new database instance for every {@link DatabaseManager#getDatabaseInfo()}} call, false 362 * if it should return the same database instance. 363 */ 364 public DatabaseManagerBuilder(boolean multiMode) { 365 super(multiMode); 366 } 367 368 /** 369 * Creates a new {@link DatabaseManager} instance from the builder. 370 * 371 * @return A database manager. Never null. 372 */ 373 @Override 374 @Nonnull 375 public DatabaseManager build() { 376 return new DatabaseManager(databasePreparers.build(), instancePreparers.build(), multiMode); 377 } 378 } 379}