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 java.io.IOException; 023import java.sql.Connection; 024import java.sql.SQLException; 025import java.sql.Statement; 026import java.util.Set; 027import java.util.concurrent.ExecutorService; 028import java.util.concurrent.Executors; 029import java.util.concurrent.SynchronousQueue; 030import java.util.concurrent.atomic.AtomicBoolean; 031import java.util.function.Supplier; 032import javax.sql.DataSource; 033 034import com.google.common.collect.ImmutableSet; 035import com.google.common.util.concurrent.ThreadFactoryBuilder; 036import edu.umd.cs.findbugs.annotations.NonNull; 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 public 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 instanceProvider.close(); 139 pg.close(); 140 } 141 } 142 143 /** 144 * 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 145 * new database instance. If it is in single mode, it will always return the same database instance. 146 * 147 * @return A {@link DatabaseInfo} instance. This is never null. 148 * @throws SQLException Any error that happened during the database creation is thrown here. 149 */ 150 @NonNull 151 public DatabaseInfo getDatabaseInfo() throws SQLException { 152 checkState(started.get(), "not yet started!"); 153 154 DatabaseInfo databaseInfo = instanceProvider.get(); 155 if (databaseInfo.exception().isPresent()) { 156 throw databaseInfo.exception().get(); 157 } 158 159 return databaseInfo; 160 } 161 162 /** 163 * Return the {@link EmbeddedPostgres} instance that manages the database server which holds all of the databases managed by this database manager. 164 * 165 * @return An {@link EmbeddedPostgres} instance. Never null. 166 */ 167 @NonNull 168 public EmbeddedPostgres getEmbeddedPostgres() { 169 checkState(started.get(), "not yet started!"); 170 return pg; 171 } 172 173 private interface InstanceProvider extends Supplier<DatabaseInfo>, AutoCloseable { 174 175 default void start() { 176 } 177 178 @Override 179 default void close() { 180 } 181 182 DatabaseInfo get(); 183 } 184 185 private final class InstanceProviderPipeline implements InstanceProvider, Runnable { 186 187 private final ExecutorService executor; 188 private final SynchronousQueue<DatabaseInfo> nextDatabase = new SynchronousQueue<>(); 189 190 private final AtomicBoolean closed = new AtomicBoolean(); 191 192 InstanceProviderPipeline() { 193 this.executor = Executors.newSingleThreadExecutor( 194 new ThreadFactoryBuilder() 195 .setDaemon(true) 196 .setNameFormat("instance-creator-" + pg.instanceId() + "-%d") 197 .build()); 198 199 } 200 201 @Override 202 public void start() { 203 this.executor.submit(this); 204 } 205 206 @Override 207 public void close() { 208 if (!this.closed.getAndSet(true)) { 209 executor.shutdownNow(); 210 } 211 } 212 213 @Override 214 public void run() { 215 while (!closed.get()) { 216 try { 217 final String newDbName = EmbeddedUtil.randomLowercase(12); 218 try { 219 createDatabase(pg.createDefaultDataSource(), newDbName); 220 nextDatabase.put(DatabaseInfo.builder() 221 .dbName(newDbName) 222 .port(pg.getPort()) 223 .connectionProperties(pg.getConnectionProperties()) 224 .build()); 225 } catch (SQLException e) { 226 // https://www.postgresql.org/docs/13/errcodes-appendix.html - 57P01 admin_shutdown 227 if (!e.getSQLState().equals("57P01")) { 228 LOG.warn("Caught SQL Exception (" + e.getSQLState() + "):", e); 229 nextDatabase.put(DatabaseInfo.forException(e)); 230 } 231 } 232 } catch (InterruptedException e) { 233 Thread.currentThread().interrupt(); 234 return; 235 } catch (Throwable t) { 236 LOG.warn("Caught Throwable in instance provider loop:", t); 237 } 238 } 239 } 240 241 @Override 242 public DatabaseInfo get() { 243 try { 244 return nextDatabase.take(); 245 } catch (final InterruptedException e) { 246 Thread.currentThread().interrupt(); 247 throw new IllegalStateException(e); 248 } 249 } 250 } 251 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 * Builder template. 261 * 262 * @param <T> Object to create. 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 * Creates a new builder. 272 * 273 * @param multiMode True if the resulting object should be in multi mode (create multiple database instances) or single mode (use only one instance). 274 */ 275 protected Builder(boolean multiMode) { 276 this.multiMode = multiMode; 277 } 278 279 /** 280 * @deprecated Use {@link DatabaseManager.Builder#withDatabasePreparer(EmbeddedPostgresPreparer)}. 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 * 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 291 * used to create tables, sequences etc. or preload the databases with information. In multi database mode, the template database is used and each 292 * created database will have this information cloned. 293 * 294 * @param databasePreparer A {@link EmbeddedPostgresPreparer<DataSource>} instance. Must not be null. 295 * @return This object instance. 296 * @since 3.0 297 */ 298 @NonNull 299 public Builder<T> withDatabasePreparer(@NonNull EmbeddedPostgresPreparer<DataSource> databasePreparer) { 300 this.databasePreparers.add(checkNotNull(databasePreparer, "databasePreparer is null")); 301 return this; 302 } 303 304 /** 305 * Add preparers for the template database. Each preparer is called once when the database manager starts to prepare the template database. This can be 306 * used to create tables, sequences etc. or preload the databases with information. In multi database mode, the template database is used and each 307 * created database will have this information cloned. 308 * 309 * @param databasePreparers A set of {@link EmbeddedPostgresPreparer<DataSource>} instances. Must not be null. 310 * @return This object instance. 311 * @since 3.0 312 */ 313 @NonNull 314 public Builder<T> withDatabasePreparers(@NonNull Set<EmbeddedPostgresPreparer<DataSource>> databasePreparers) { 315 this.databasePreparers.addAll(checkNotNull(databasePreparers, "databasePreparers is null")); 316 return this; 317 } 318 319 /** 320 * Add a preparer for the {@link EmbeddedPostgres.Builder} object. Each preparer is called once when the {@link EmbeddedPostgres} instance that manages 321 * the server is created. 322 * 323 * @param instancePreparer A {@link EmbeddedPostgresPreparer<EmbeddedPostgres.Builder>} instance. Must not be null. 324 * @return This object instance. 325 * @since 3.0 326 */ 327 @NonNull 328 public Builder<T> withInstancePreparer(@NonNull EmbeddedPostgresPreparer<EmbeddedPostgres.Builder> instancePreparer) { 329 this.instancePreparers.add(checkNotNull(instancePreparer, "instancePreparer is null")); 330 return this; 331 } 332 333 /** 334 * Add preparers for the {@link EmbeddedPostgres.Builder} object. Each preparer is called once when the {@link EmbeddedPostgres} instance that manages 335 * the server is created. 336 * 337 * @param instancePreparers A set of {@link EmbeddedPostgresPreparer<EmbeddedPostgres.Builder>} instances. Must not be null. 338 * @return This object instance. 339 * @since 3.0 340 */ 341 @NonNull 342 public Builder<T> withInstancePreparers(@NonNull Set<EmbeddedPostgresPreparer<EmbeddedPostgres.Builder>> instancePreparers) { 343 this.instancePreparers.addAll(checkNotNull(instancePreparers, "instancePreparers is null")); 344 return this; 345 } 346 347 /** 348 * @deprecated Use {@link DatabaseManager.Builder#withInstancePreparer(EmbeddedPostgresPreparer)}. 349 */ 350 @Deprecated 351 @NonNull 352 public Builder<T> withCustomizer(@NonNull EmbeddedPostgres.BuilderCustomizer customizer) { 353 checkNotNull(customizer, "customizer is null"); 354 this.instancePreparers.add(customizer::customize); 355 return this; 356 } 357 358 /** 359 * Creates a new instance. 360 * 361 * @return The instance to create. 362 */ 363 @NonNull 364 public abstract T build(); 365 } 366 367 /** 368 * Create new {@link DatabaseManager} instances. 369 */ 370 public static final class DatabaseManagerBuilder extends Builder<DatabaseManager> { 371 372 /** 373 * Creates a new builder for {@link DatabaseManager} instances. 374 * 375 * @param multiMode True if the database manager should return a new database instance for every {@link DatabaseManager#getDatabaseInfo()}} call, false 376 * if it should return the same database instance. 377 */ 378 public DatabaseManagerBuilder(boolean multiMode) { 379 super(multiMode); 380 } 381 382 /** 383 * Creates a new {@link DatabaseManager} instance from the builder. 384 * 385 * @return A database manager. Never null. 386 */ 387 @Override 388 @NonNull 389 public DatabaseManager build() { 390 return new DatabaseManager(databasePreparers.build(), instancePreparers.build(), multiMode); 391 } 392 } 393}