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 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 DatabaseInfo get(); 189 } 190 191 private final class InstanceProviderPipeline implements InstanceProvider, Runnable { 192 193 private final ExecutorService executor; 194 private final SynchronousQueue<DatabaseInfo> nextDatabase = new SynchronousQueue<>(); 195 196 private final AtomicBoolean closed = new AtomicBoolean(); 197 198 InstanceProviderPipeline() { 199 this.executor = Executors.newSingleThreadExecutor( 200 new ThreadFactoryBuilder() 201 .setDaemon(true) 202 .setNameFormat("instance-creator-" + pg.instanceId() + "-%d") 203 .build()); 204 205 } 206 207 @Override 208 public void start() { 209 this.executor.submit(this); 210 } 211 212 @Override 213 public void close() { 214 if (!this.closed.getAndSet(true)) { 215 executor.shutdownNow(); 216 } 217 } 218 219 @Override 220 public void run() { 221 while (!closed.get()) { 222 try { 223 final String newDbName = EmbeddedUtil.randomLowercase(12); 224 try { 225 createDatabase(pg.createDefaultDataSource(), newDbName); 226 nextDatabase.put(DatabaseInfo.builder() 227 .dbName(newDbName) 228 .port(pg.getPort()) 229 .connectionProperties(pg.getConnectionProperties()) 230 .build()); 231 } catch (SQLException e) { 232 // https://www.postgresql.org/docs/13/errcodes-appendix.html - 57P01 admin_shutdown 233 if (!e.getSQLState().equals("57P01")) { 234 LOG.warn("Caught SQL Exception (" + e.getSQLState() + "):", e); 235 nextDatabase.put(DatabaseInfo.forException(e)); 236 } 237 } 238 } catch (InterruptedException e) { 239 Thread.currentThread().interrupt(); 240 return; 241 } catch (Throwable t) { 242 LOG.warn("Caught Throwable in instance provider loop:", t); 243 } 244 } 245 } 246 247 @Override 248 public DatabaseInfo get() { 249 try { 250 return nextDatabase.take(); 251 } catch (final InterruptedException e) { 252 Thread.currentThread().interrupt(); 253 throw new IllegalStateException(e); 254 } 255 } 256 } 257 258 private static void createDatabase(final DataSource dataSource, final String databaseName) throws SQLException { 259 try (Connection c = dataSource.getConnection(); 260 Statement stmt = c.createStatement()) { 261 stmt.executeUpdate(format("CREATE DATABASE %s OWNER %s ENCODING = '%s'", databaseName, PG_DEFAULT_USER, PG_DEFAULT_ENCODING)); 262 } 263 } 264 265 /** 266 * Builder template. 267 * 268 * @param <T> Object to create. 269 */ 270 public abstract static class Builder<T> { 271 272 protected ImmutableSet.Builder<EmbeddedPostgresPreparer<DataSource>> databasePreparers = ImmutableSet.builder(); 273 protected ImmutableSet.Builder<EmbeddedPostgresPreparer<EmbeddedPostgres.Builder>> instancePreparers = ImmutableSet.builder(); 274 protected final boolean multiMode; 275 276 /** 277 * Creates a new builder. 278 * 279 * @param multiMode True if the resulting object should be in multi mode (create multiple database instances) or single mode (use only one instance). 280 */ 281 protected Builder(boolean multiMode) { 282 this.multiMode = multiMode; 283 } 284 285 /** 286 * 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 287 * used to create tables, sequences etc. or preload the databases with information. In multi database mode, the template database is used and each 288 * created database will have this information cloned. 289 * 290 * @param databasePreparer A {@link EmbeddedPostgresPreparer<DataSource>} instance. Must not be null. 291 * @return This object instance. 292 * @since 3.0 293 */ 294 @NonNull 295 public Builder<T> withDatabasePreparer(@NonNull EmbeddedPostgresPreparer<DataSource> databasePreparer) { 296 this.databasePreparers.add(checkNotNull(databasePreparer, "databasePreparer is null")); 297 return this; 298 } 299 300 /** 301 * Add preparers for the template database. Each preparer is called once when the database manager starts to prepare the template database. This can be 302 * used to create tables, sequences etc. or preload the databases with information. In multi database mode, the template database is used and each 303 * created database will have this information cloned. 304 * 305 * @param databasePreparers A set of {@link EmbeddedPostgresPreparer<DataSource>} instances. Must not be null. 306 * @return This object instance. 307 * @since 3.0 308 */ 309 @NonNull 310 public Builder<T> withDatabasePreparers(@NonNull Set<EmbeddedPostgresPreparer<DataSource>> databasePreparers) { 311 this.databasePreparers.addAll(checkNotNull(databasePreparers, "databasePreparers is null")); 312 return this; 313 } 314 315 /** 316 * Add a preparer for the {@link EmbeddedPostgres.Builder} object. Each preparer is called once when the {@link EmbeddedPostgres} instance that manages 317 * the server is created. 318 * 319 * @param instancePreparer A {@link EmbeddedPostgresPreparer<EmbeddedPostgres.Builder>} instance. Must not be null. 320 * @return This object instance. 321 * @since 3.0 322 */ 323 @NonNull 324 public Builder<T> withInstancePreparer(@NonNull EmbeddedPostgresPreparer<EmbeddedPostgres.Builder> instancePreparer) { 325 this.instancePreparers.add(checkNotNull(instancePreparer, "instancePreparer is null")); 326 return this; 327 } 328 329 /** 330 * Add preparers for the {@link EmbeddedPostgres.Builder} object. Each preparer is called once when the {@link EmbeddedPostgres} instance that manages 331 * the server is created. 332 * 333 * @param instancePreparers A set of {@link EmbeddedPostgresPreparer<EmbeddedPostgres.Builder>} instances. Must not be null. 334 * @return This object instance. 335 * @since 3.0 336 */ 337 @NonNull 338 public Builder<T> withInstancePreparers(@NonNull Set<EmbeddedPostgresPreparer<EmbeddedPostgres.Builder>> instancePreparers) { 339 this.instancePreparers.addAll(checkNotNull(instancePreparers, "instancePreparers is null")); 340 return this; 341 } 342 343 /** 344 * Creates a new instance. 345 * 346 * @return The instance to create. 347 */ 348 @NonNull 349 public abstract T build(); 350 } 351 352 /** 353 * Create new {@link DatabaseManager} instances. 354 */ 355 public static final class DatabaseManagerBuilder extends Builder<DatabaseManager> { 356 357 /** 358 * Creates a new builder for {@link DatabaseManager} instances. 359 * 360 * @param multiMode True if the database manager should return a new database instance for every {@link DatabaseManager#getDatabaseInfo()}} call, false 361 * if it should return the same database instance. 362 */ 363 public DatabaseManagerBuilder(boolean multiMode) { 364 super(multiMode); 365 } 366 367 /** 368 * Creates a new {@link DatabaseManager} instance from the builder. 369 * 370 * @return A database manager. Never null. 371 */ 372 @Override 373 @NonNull 374 public DatabaseManager build() { 375 return new DatabaseManager(databasePreparers.build(), instancePreparers.build(), multiMode); 376 } 377 } 378}