Throw exception on duplicate Database registration (#3838) (#3847)

* Throw exception on duplicate Database registration (#3838)

DatabaseFactory.create() with register(true) previously returned the
existing Database instance when another instance was already registered
under the same name (added in #3759). This silently caused two
independently created Database instances (e.g. with different
DataSourceConfig.url values) to collide/share state, since the
second "instance" was actually the first one in disguise.

Change this to throw an IllegalStateException instead, forcing
callers to either use a unique DatabaseConfig name or setRegister(false)
when the Database is not intended to be registered/looked up by name.

* Also handle deregistration on shutdown

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
This commit is contained in:
Rob Bygrave
2026-07-14 00:34:49 +12:00
committed by GitHub
co-authored by robin.bygrave
parent bdbe743f56
commit 73c9ff8d80
7 changed files with 93 additions and 45 deletions
@@ -62,9 +62,10 @@ public interface DatabaseBuilder {
/**
* Build and return the Database instance.
* <p>
* When {@link #setRegister(boolean)} is set to true, and a database with the same
* name is already registered, this may return the existing registered database
* rather than creating a new one.
* When {@link #setRegister(boolean)} is set to true (the default), and a database
* with the same name is already registered, this throws an {@link IllegalStateException}.
* Use a unique name, or use {@link #setRegister(boolean)} with {@code false} if the
* Database instance is not intended to be registered/looked up by name.
*/
Database build();
@@ -7,8 +7,6 @@ import jakarta.persistence.PersistenceException;
import java.util.concurrent.locks.ReentrantLock;
import static java.lang.System.Logger.Level.WARNING;
/**
* Low-level factory for creating {@link Database} instances.
* <p>
@@ -81,10 +79,10 @@ public final class DatabaseFactory {
// We're explicitly creating a database to be registered, so avoid
// triggering DbContext static initialisation to auto-create a default one.
DbPrimary.setSkip(true);
Database existing = DbContext.getInstance().getRegistered(name);
if (existing != null) {
EbeanVersion.log.log(WARNING, "Using existing database with name:{0}", name);
return existing;
if (DbContext.getInstance().contains(name)) {
throw new IllegalStateException("A Database with name [" + name + "] is already registered."
+ " Use a unique DatabaseConfig name, or set DatabaseConfig.setRegister(false)"
+ " if this Database instance is not intended to be registered/looked up by name.");
}
}
Database server = createInternal(config);
@@ -123,6 +121,24 @@ public final class DatabaseFactory {
}
}
/**
* Remove the registration of this Database.
* <p>
* This is invoked when a Database is shutdown so that its registered name
* becomes available again for a subsequently created Database with the same name.
*/
public static void unregister(Database server) {
lock.lock();
try {
DbContext.getInstance().deregister(server);
if (server.name().equals(defaultServerName)) {
defaultServerName = null;
}
} finally {
lock.unlock();
}
}
/**
* Shutdown gracefully all Database instances cleaning up any resources as required.
* <p>
@@ -4,7 +4,6 @@ import io.ebean.config.BeanNotEnhancedException;
import io.ebean.datasource.DataSourceConfigurationException;
import jakarta.persistence.PersistenceException;
import org.jspecify.annotations.Nullable;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
@@ -77,9 +76,8 @@ final class DbContext {
return defaultDatabase;
}
@Nullable
Database getRegistered(String name) {
return concMap.get(name);
boolean contains(String name) {
return concMap.containsKey(name);
}
/**
@@ -122,6 +120,27 @@ final class DbContext {
registerWithName(server.name(), server, isDefault);
}
/**
* Remove the registration for this Database (typically on shutdown) so that
* its name becomes available again for a subsequently created Database.
* <p>
* Only removes the registration if it currently maps to this exact instance
* (avoids removing a different Database subsequently registered with the same name).
*/
void deregister(Database server) {
lock.lock();
try {
String name = server.name();
concMap.remove(name, server);
syncMap.remove(name, server);
if (defaultDatabase == server) {
defaultDatabase = null;
}
} finally {
lock.unlock();
}
}
private void registerWithName(String name, Database server, boolean isDefault) {
lock.lock();
try {
@@ -1,6 +1,7 @@
package io.ebean.event;
import io.ebean.Database;
import io.ebean.DatabaseFactory;
import io.ebean.EbeanVersion;
import io.ebean.service.SpiContainer;
@@ -188,6 +189,7 @@ public final class ShutdownManager {
*/
public static void unregisterDatabase(Database server) {
databases.remove(server);
DatabaseFactory.unregister(server);
}
private static class ShutdownHook extends Thread {
@@ -5,17 +5,23 @@ import io.ebean.Database;
import io.ebean.redis.DuelCache;
import org.domain.Person;
import org.domain.query.QPerson;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import javax.sql.DataSource;
import static org.assertj.core.api.Assertions.assertThat;
class ClusterTest {
private Database createOther(DataSource dataSource) {
return Database.builder()
.dataSource(dataSource)
private static Database db;
private static Database other;
@BeforeAll
static void setup() {
// ensure the default server exists first
db = DB.getDefault();
other = Database.builder()
.dataSource(db.pluginApi().dataSource())
.loadFromProperties()
.defaultDatabase(false)
.name("other")
@@ -24,12 +30,13 @@ class ClusterTest {
.build();
}
@AfterAll
static void tearDown() {
other.shutdown(false, false);
}
@Test
void testBothNear() throws InterruptedException {
// ensure the default server exists first
final Database db = DB.getDefault();
Database other = createOther(db.pluginApi().dataSource());
new QPerson()
.name.eq("Someone")
.delete();
@@ -59,10 +66,6 @@ class ClusterTest {
@Test
void test() throws InterruptedException {
// ensure the default server exists first
final Database db = DB.getDefault();
Database other = createOther(db.pluginApi().dataSource());
for (int i = 0; i < 10; i++) {
Person foo = new Person("name " + i);
foo.save();
@@ -5,17 +5,23 @@ import io.ebean.Database;
import io.ebean.redisson.DuelCache;
import org.domain.Person;
import org.domain.query.QPerson;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import javax.sql.DataSource;
import static org.assertj.core.api.Assertions.assertThat;
class ClusterTest {
private Database createOther(DataSource dataSource) {
return Database.builder()
.dataSource(dataSource)
private static Database db;
private static Database other;
@BeforeAll
static void setup() {
// ensure the default server exists first
db = DB.getDefault();
other = Database.builder()
.dataSource(db.pluginApi().dataSource())
.loadFromProperties()
.defaultDatabase(false)
.name("other")
@@ -24,12 +30,13 @@ class ClusterTest {
.build();
}
@AfterAll
static void tearDown() {
other.shutdown(false, false);
}
@Test
void testBothNear() throws InterruptedException {
// ensure the default server exists first
final Database db = DB.getDefault();
Database other = createOther(db.pluginApi().dataSource());
new QPerson()
.name.eq("Someone")
.delete();
@@ -59,10 +66,6 @@ class ClusterTest {
@Test
void test() throws InterruptedException {
// ensure the default server exists first
final Database db = DB.getDefault();
Database other = createOther(db.pluginApi().dataSource());
for (int i = 0; i < 10; i++) {
Person foo = new Person("name " + i);
foo.save();
@@ -15,6 +15,7 @@ import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class EbeanServerFactory_ServerConfigStart_Test {
@@ -83,7 +84,7 @@ public class EbeanServerFactory_ServerConfigStart_Test {
}
@Test
public void create_registeredDatabase_twice_returnsExistingInstance() {
public void create_registeredDatabase_twice_throwsException() {
DatabaseBuilder config = new DatabaseConfig();
config.setName("h2");
@@ -100,12 +101,15 @@ public class EbeanServerFactory_ServerConfigStart_Test {
config.addServerConfigStartup(serverConfig -> startupCount.incrementAndGet());
Database db = DatabaseFactory.create(config);
Database existing = DatabaseFactory.create(config);
try {
assertThatThrownBy(() -> DatabaseFactory.create(config))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("already registered");
assertThat(existing).isSameAs(db);
assertThat(startupCount.get()).isEqualTo(1);
db.shutdown(true, false);
assertThat(startupCount.get()).isEqualTo(1);
} finally {
db.shutdown(true, false);
}
}
@Test