diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/clustering.xml b/docbook/auth-server-docs/reference/en/en-US/modules/clustering.xml
index 850326eb44..cb36fc4c09 100755
--- a/docbook/auth-server-docs/reference/en/en-US/modules/clustering.xml
+++ b/docbook/auth-server-docs/reference/en/en-US/modules/clustering.xml
@@ -57,6 +57,29 @@
database. This can be a relational database or Mongo. To make sure your database doesn't become a single
point of failure you may also want to deploy your database to a cluster.
+
+ DB lock
+ Note that Keycloak supports concurrent startup by more cluster nodes at the same. This is ensured by DB lock, which prevents that some
+ startup actions (migrating database from previous version, importing realms at startup, initial bootstrap of admin user) are always executed just by one
+ cluster node at a time and other cluster nodes need to wait until the current node finishes startup actions and release the DB lock.
+
+
+ By default, the maximum timeout for lock is 900 seconds, so in case that second node is not able to acquire the lock within 900 seconds, it fails to start.
+ The lock checking is done every 2 seconds by default. Typically you won't need to increase/decrease the default value, but just in case
+ it's possible to configure it in standalone/configuration/keycloak-server.json:
+
+
+
+ or similarly if you're using Mongo (just by replace jpa with mongo)
+
+
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java
index 144df70ec5..4d931488c2 100755
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java
@@ -126,7 +126,7 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
properties.put("hibernate.dialect", driverDialect);
}
- String schema = config.get("schema");
+ String schema = getSchema();
if (schema != null) {
properties.put(JpaUtils.HIBERNATE_DEFAULT_SCHEMA, schema);
}
@@ -167,7 +167,7 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
}
if (currentVersion == null || !JpaUpdaterProvider.LAST_VERSION.equals(currentVersion)) {
- updater.update(session, connection, schema);
+ updater.update(connection, schema);
} else {
logger.debug("Database is up to date");
}
@@ -212,7 +212,8 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
}
}
- private Connection getConnection() {
+ @Override
+ public Connection getConnection() {
try {
String dataSourceLookup = config.get("dataSource");
if (dataSourceLookup != null) {
@@ -226,6 +227,11 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
throw new RuntimeException("Failed to connect to database", e);
}
}
+
+ @Override
+ public String getSchema() {
+ return config.get("schema");
+ }
@Override
public Map getOperationalInfo() {
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java
index edede60965..8d687bcef9 100644
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java
@@ -17,6 +17,8 @@
package org.keycloak.connections.jpa;
+import java.sql.Connection;
+
import org.keycloak.provider.ProviderFactory;
/**
@@ -24,4 +26,9 @@ import org.keycloak.provider.ProviderFactory;
*/
public interface JpaConnectionProviderFactory extends ProviderFactory {
+ // Caller is responsible for closing connection
+ Connection getConnection();
+
+ String getSchema();
+
}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java
index 2e2c5d1f29..0e0dea2e7e 100755
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java
@@ -17,7 +17,6 @@
package org.keycloak.connections.jpa.updater;
-import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.Provider;
import java.sql.Connection;
@@ -33,7 +32,7 @@ public interface JpaUpdaterProvider extends Provider {
public String getCurrentVersionSql(String defaultSchema);
- public void update(KeycloakSession session, Connection connection, String defaultSchema);
+ public void update(Connection connection, String defaultSchema);
public void validate(Connection connection, String defaultSchema);
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java
index 80bb46710a..61075c7d3c 100755
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java
@@ -20,18 +20,10 @@ package org.keycloak.connections.jpa.updater.liquibase;
import liquibase.Contexts;
import liquibase.Liquibase;
import liquibase.changelog.ChangeSet;
-import liquibase.changelog.DatabaseChangeLog;
import liquibase.changelog.RanChangeSet;
-import liquibase.database.Database;
-import liquibase.database.DatabaseFactory;
-import liquibase.database.core.DB2Database;
-import liquibase.database.jvm.JdbcConnection;
-import liquibase.logging.LogFactory;
-import liquibase.logging.LogLevel;
-import liquibase.resource.ClassLoaderResourceAccessor;
-import liquibase.servicelocator.ServiceLocator;
import org.jboss.logging.Logger;
import org.keycloak.connections.jpa.updater.JpaUpdaterProvider;
+import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProvider;
import org.keycloak.models.KeycloakSession;
import java.sql.Connection;
@@ -46,8 +38,14 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
private static final Logger logger = Logger.getLogger(LiquibaseJpaUpdaterProvider.class);
- private static final String CHANGELOG = "META-INF/jpa-changelog-master.xml";
- private static final String DB2_CHANGELOG = "META-INF/db2-jpa-changelog-master.xml";
+ public static final String CHANGELOG = "META-INF/jpa-changelog-master.xml";
+ public static final String DB2_CHANGELOG = "META-INF/db2-jpa-changelog-master.xml";
+
+ private final KeycloakSession session;
+
+ public LiquibaseJpaUpdaterProvider(KeycloakSession session) {
+ this.session = session;
+ }
@Override
public String getCurrentVersionSql(String defaultSchema) {
@@ -55,7 +53,7 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
}
@Override
- public void update(KeycloakSession session, Connection connection, String defaultSchema) {
+ public void update(Connection connection, String defaultSchema) {
logger.debug("Starting database update");
// Need ThreadLocal as liquibase doesn't seem to have API to inject custom objects into tasks
@@ -108,145 +106,14 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
}
private Liquibase getLiquibase(Connection connection, String defaultSchema) throws Exception {
- ServiceLocator sl = ServiceLocator.getInstance();
-
- if (!System.getProperties().containsKey("liquibase.scan.packages")) {
- if (sl.getPackages().remove("liquibase.core")) {
- sl.addPackageToScan("liquibase.core.xml");
- }
-
- if (sl.getPackages().remove("liquibase.parser")) {
- sl.addPackageToScan("liquibase.parser.core.xml");
- }
-
- if (sl.getPackages().remove("liquibase.serializer")) {
- sl.addPackageToScan("liquibase.serializer.core.xml");
- }
-
- sl.getPackages().remove("liquibase.ext");
- sl.getPackages().remove("liquibase.sdk");
- }
-
- LogFactory.setInstance(new LogWrapper());
-
- // Adding PostgresPlus support to liquibase
- DatabaseFactory.getInstance().register(new PostgresPlusDatabase());
-
- Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
- if (defaultSchema != null) {
- database.setDefaultSchemaName(defaultSchema);
- }
-
- String changelog = (database instanceof DB2Database) ? DB2_CHANGELOG : CHANGELOG;
- logger.debugf("Using changelog file: %s", changelog);
- return new Liquibase(changelog, new ClassLoaderResourceAccessor(getClass().getClassLoader()), database);
+ LiquibaseConnectionProvider liquibaseProvider = session.getProvider(LiquibaseConnectionProvider.class);
+ return liquibaseProvider.getLiquibase(connection, defaultSchema);
}
@Override
public void close() {
}
- private static class LogWrapper extends LogFactory {
-
- private liquibase.logging.Logger logger = new liquibase.logging.Logger() {
- @Override
- public void setName(String name) {
- }
-
- @Override
- public void setLogLevel(String level) {
- }
-
- @Override
- public void setLogLevel(LogLevel level) {
- }
-
- @Override
- public void setLogLevel(String logLevel, String logFile) {
- }
-
- @Override
- public void severe(String message) {
- LiquibaseJpaUpdaterProvider.logger.error(message);
- }
-
- @Override
- public void severe(String message, Throwable e) {
- LiquibaseJpaUpdaterProvider.logger.error(message, e);
- }
-
- @Override
- public void warning(String message) {
- // Ignore this warning as cascaded drops doesn't work anyway with all DBs, which we need to support
- if ("Database does not support drop with cascade".equals(message)) {
- LiquibaseJpaUpdaterProvider.logger.debug(message);
- } else {
- LiquibaseJpaUpdaterProvider.logger.warn(message);
- }
- }
-
- @Override
- public void warning(String message, Throwable e) {
- LiquibaseJpaUpdaterProvider.logger.warn(message, e);
- }
-
- @Override
- public void info(String message) {
- LiquibaseJpaUpdaterProvider.logger.debug(message);
- }
-
- @Override
- public void info(String message, Throwable e) {
- LiquibaseJpaUpdaterProvider.logger.debug(message, e);
- }
-
- @Override
- public void debug(String message) {
- LiquibaseJpaUpdaterProvider.logger.trace(message);
- }
-
- @Override
- public LogLevel getLogLevel() {
- if (LiquibaseJpaUpdaterProvider.logger.isTraceEnabled()) {
- return LogLevel.DEBUG;
- } else if (LiquibaseJpaUpdaterProvider.logger.isDebugEnabled()) {
- return LogLevel.INFO;
- } else {
- return LogLevel.WARNING;
- }
- }
-
- @Override
- public void debug(String message, Throwable e) {
- LiquibaseJpaUpdaterProvider.logger.trace(message, e);
- }
-
- @Override
- public void setChangeLog(DatabaseChangeLog databaseChangeLog) {
- }
-
- @Override
- public void setChangeSet(ChangeSet changeSet) {
- }
-
- @Override
- public int getPriority() {
- return 0;
- }
- };
-
- @Override
- public liquibase.logging.Logger getLog(String name) {
- return logger;
- }
-
- @Override
- public liquibase.logging.Logger getLog() {
- return logger;
- }
-
- }
-
public static String getTable(String table, String defaultSchema) {
return defaultSchema != null ? defaultSchema + "." + table : table;
}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProviderFactory.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProviderFactory.java
index 28982b9333..26eac9a646 100755
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProviderFactory.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProviderFactory.java
@@ -30,7 +30,7 @@ public class LiquibaseJpaUpdaterProviderFactory implements JpaUpdaterProviderFac
@Override
public JpaUpdaterProvider create(KeycloakSession session) {
- return new LiquibaseJpaUpdaterProvider();
+ return new LiquibaseJpaUpdaterProvider(session);
}
@Override
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/DefaultLiquibaseConnectionProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/DefaultLiquibaseConnectionProvider.java
new file mode 100644
index 0000000000..d0c686a0fe
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/DefaultLiquibaseConnectionProvider.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.connections.jpa.updater.liquibase.conn;
+
+import java.sql.Connection;
+
+import liquibase.Liquibase;
+import liquibase.changelog.ChangeSet;
+import liquibase.changelog.DatabaseChangeLog;
+import liquibase.database.Database;
+import liquibase.database.DatabaseFactory;
+import liquibase.database.core.DB2Database;
+import liquibase.database.jvm.JdbcConnection;
+import liquibase.exception.LiquibaseException;
+import liquibase.lockservice.LockService;
+import liquibase.lockservice.LockServiceFactory;
+import liquibase.logging.LogFactory;
+import liquibase.logging.LogLevel;
+import liquibase.resource.ClassLoaderResourceAccessor;
+import liquibase.servicelocator.ServiceLocator;
+import liquibase.sqlgenerator.SqlGeneratorFactory;
+import org.jboss.logging.Logger;
+import org.keycloak.Config;
+import org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProvider;
+import org.keycloak.connections.jpa.updater.liquibase.PostgresPlusDatabase;
+import org.keycloak.connections.jpa.updater.liquibase.lock.CustomInsertLockRecordGenerator;
+import org.keycloak.connections.jpa.updater.liquibase.lock.CustomLockService;
+import org.keycloak.connections.jpa.updater.liquibase.lock.DummyLockService;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * @author Marek Posolda
+ */
+public class DefaultLiquibaseConnectionProvider implements LiquibaseConnectionProviderFactory, LiquibaseConnectionProvider {
+
+ private static final Logger logger = Logger.getLogger(DefaultLiquibaseConnectionProvider.class);
+
+ private volatile boolean initialized = false;
+
+ @Override
+ public LiquibaseConnectionProvider create(KeycloakSession session) {
+ if (!initialized) {
+ synchronized (this) {
+ if (!initialized) {
+ baseLiquibaseInitialization();
+ initialized = true;
+ }
+ }
+ }
+ return this;
+ }
+
+ protected void baseLiquibaseInitialization() {
+ ServiceLocator sl = ServiceLocator.getInstance();
+
+ if (!System.getProperties().containsKey("liquibase.scan.packages")) {
+ if (sl.getPackages().remove("liquibase.core")) {
+ sl.addPackageToScan("liquibase.core.xml");
+ }
+
+ if (sl.getPackages().remove("liquibase.parser")) {
+ sl.addPackageToScan("liquibase.parser.core.xml");
+ }
+
+ if (sl.getPackages().remove("liquibase.serializer")) {
+ sl.addPackageToScan("liquibase.serializer.core.xml");
+ }
+
+ sl.getPackages().remove("liquibase.ext");
+ sl.getPackages().remove("liquibase.sdk");
+ }
+
+ LogFactory.setInstance(new LogWrapper());
+
+ // Adding PostgresPlus support to liquibase
+ DatabaseFactory.getInstance().register(new PostgresPlusDatabase());
+
+ // Change command for creating lock and drop DELETE lock record from it
+ SqlGeneratorFactory.getInstance().register(new CustomInsertLockRecordGenerator());
+
+ // We wrap liquibase update in CustomLockService provided by DBLockProvider. No need to lock inside liquibase itself.
+ LockServiceFactory.getInstance().register(new DummyLockService());
+ }
+
+
+ @Override
+ public void init(Config.Scope config) {
+
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public String getId() {
+ return "default";
+ }
+
+ @Override
+ public Liquibase getLiquibase(Connection connection, String defaultSchema) throws LiquibaseException {
+ Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
+ if (defaultSchema != null) {
+ database.setDefaultSchemaName(defaultSchema);
+ }
+
+ String changelog = (database instanceof DB2Database) ? LiquibaseJpaUpdaterProvider.DB2_CHANGELOG : LiquibaseJpaUpdaterProvider.CHANGELOG;
+ logger.debugf("Using changelog file: %s", changelog);
+ return new Liquibase(changelog, new ClassLoaderResourceAccessor(getClass().getClassLoader()), database);
+ }
+
+ private static class LogWrapper extends LogFactory {
+
+ private liquibase.logging.Logger logger = new liquibase.logging.Logger() {
+ @Override
+ public void setName(String name) {
+ }
+
+ @Override
+ public void setLogLevel(String level) {
+ }
+
+ @Override
+ public void setLogLevel(LogLevel level) {
+ }
+
+ @Override
+ public void setLogLevel(String logLevel, String logFile) {
+ }
+
+ @Override
+ public void severe(String message) {
+ DefaultLiquibaseConnectionProvider.logger.error(message);
+ }
+
+ @Override
+ public void severe(String message, Throwable e) {
+ DefaultLiquibaseConnectionProvider.logger.error(message, e);
+ }
+
+ @Override
+ public void warning(String message) {
+ // Ignore this warning as cascaded drops doesn't work anyway with all DBs, which we need to support
+ if ("Database does not support drop with cascade".equals(message)) {
+ DefaultLiquibaseConnectionProvider.logger.debug(message);
+ } else {
+ DefaultLiquibaseConnectionProvider.logger.warn(message);
+ }
+ }
+
+ @Override
+ public void warning(String message, Throwable e) {
+ DefaultLiquibaseConnectionProvider.logger.warn(message, e);
+ }
+
+ @Override
+ public void info(String message) {
+ DefaultLiquibaseConnectionProvider.logger.debug(message);
+ }
+
+ @Override
+ public void info(String message, Throwable e) {
+ DefaultLiquibaseConnectionProvider.logger.debug(message, e);
+ }
+
+ @Override
+ public void debug(String message) {
+ if (DefaultLiquibaseConnectionProvider.logger.isTraceEnabled()) {
+ DefaultLiquibaseConnectionProvider.logger.trace(message);
+ }
+ }
+
+ @Override
+ public LogLevel getLogLevel() {
+ if (DefaultLiquibaseConnectionProvider.logger.isTraceEnabled()) {
+ return LogLevel.DEBUG;
+ } else if (DefaultLiquibaseConnectionProvider.logger.isDebugEnabled()) {
+ return LogLevel.INFO;
+ } else {
+ return LogLevel.WARNING;
+ }
+ }
+
+ @Override
+ public void debug(String message, Throwable e) {
+ DefaultLiquibaseConnectionProvider.logger.trace(message, e);
+ }
+
+ @Override
+ public void setChangeLog(DatabaseChangeLog databaseChangeLog) {
+ }
+
+ @Override
+ public void setChangeSet(ChangeSet changeSet) {
+ }
+
+ @Override
+ public int getPriority() {
+ return 0;
+ }
+ };
+
+ @Override
+ public liquibase.logging.Logger getLog(String name) {
+ return logger;
+ }
+
+ @Override
+ public liquibase.logging.Logger getLog() {
+ return logger;
+ }
+
+ }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionProvider.java
new file mode 100644
index 0000000000..5aa81cc84e
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionProvider.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.connections.jpa.updater.liquibase.conn;
+
+import java.sql.Connection;
+
+import liquibase.Liquibase;
+import liquibase.exception.LiquibaseException;
+import org.keycloak.provider.Provider;
+
+/**
+ * @author Marek Posolda
+ */
+public interface LiquibaseConnectionProvider extends Provider {
+
+ Liquibase getLiquibase(Connection connection, String defaultSchema) throws LiquibaseException;
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionProviderFactory.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionProviderFactory.java
new file mode 100644
index 0000000000..7dfe5f9d55
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionProviderFactory.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.connections.jpa.updater.liquibase.conn;
+
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ * @author Marek Posolda
+ */
+public interface LiquibaseConnectionProviderFactory extends ProviderFactory {
+}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionSpi.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionSpi.java
new file mode 100644
index 0000000000..7aeda323b3
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionSpi.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.connections.jpa.updater.liquibase.conn;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * @author Marek Posolda
+ */
+public class LiquibaseConnectionSpi implements Spi {
+
+ @Override
+ public boolean isInternal() {
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return "connectionsLiquibase";
+ }
+
+ @Override
+ public Class extends Provider> getProviderClass() {
+ return LiquibaseConnectionProvider.class;
+ }
+
+ @Override
+ public Class extends ProviderFactory> getProviderFactoryClass() {
+ return LiquibaseConnectionProviderFactory.class;
+ }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/CustomInsertLockRecordGenerator.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/CustomInsertLockRecordGenerator.java
new file mode 100644
index 0000000000..3b1e2f9571
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/CustomInsertLockRecordGenerator.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.connections.jpa.updater.liquibase.lock;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import liquibase.database.Database;
+import liquibase.exception.ValidationErrors;
+import liquibase.sql.Sql;
+import liquibase.sqlgenerator.SqlGeneratorChain;
+import liquibase.sqlgenerator.core.AbstractSqlGenerator;
+import liquibase.statement.core.DeleteStatement;
+import liquibase.statement.core.InitializeDatabaseChangeLogLockTableStatement;
+
+/**
+ * We need to remove DELETE SQL command, which liquibase adds by default when inserting record to table lock. This is causing buggy behaviour
+ *
+ * @author Marek Posolda
+ */
+public class CustomInsertLockRecordGenerator extends AbstractSqlGenerator {
+
+ @Override
+ public int getPriority() {
+ return super.getPriority() + 1; // Ensure bigger priority than InitializeDatabaseChangeLogLockTableGenerator
+ }
+
+ @Override
+ public ValidationErrors validate(InitializeDatabaseChangeLogLockTableStatement initializeDatabaseChangeLogLockTableStatement, Database database, SqlGeneratorChain sqlGeneratorChain) {
+ return new ValidationErrors();
+ }
+
+ @Override
+ public Sql[] generateSql(InitializeDatabaseChangeLogLockTableStatement statement, Database database, SqlGeneratorChain sqlGeneratorChain) {
+ // Generated by InitializeDatabaseChangeLogLockTableGenerator
+ Sql[] sqls = sqlGeneratorChain.generateSql(statement, database);
+
+ // Removing delete statement
+ List result = new ArrayList<>();
+ for (Sql sql : sqls) {
+ String sqlCommand = sql.toSql();
+ if (!sqlCommand.toUpperCase().contains("DELETE")) {
+ result.add(sql);
+ }
+ }
+
+ return result.toArray(new Sql[result.size()]);
+ }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/CustomLockService.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/CustomLockService.java
new file mode 100644
index 0000000000..0c246ca9a3
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/CustomLockService.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.connections.jpa.updater.liquibase.lock;
+
+import java.lang.reflect.Field;
+import java.text.DateFormat;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+
+import liquibase.database.Database;
+import liquibase.database.core.DerbyDatabase;
+import liquibase.exception.DatabaseException;
+import liquibase.exception.LockException;
+import liquibase.executor.Executor;
+import liquibase.executor.ExecutorService;
+import liquibase.lockservice.DatabaseChangeLogLock;
+import liquibase.lockservice.StandardLockService;
+import liquibase.logging.LogFactory;
+import liquibase.sql.visitor.AbstractSqlVisitor;
+import liquibase.sql.visitor.SqlVisitor;
+import liquibase.statement.core.CreateDatabaseChangeLogLockTableStatement;
+import liquibase.statement.core.DropTableStatement;
+import liquibase.statement.core.InitializeDatabaseChangeLogLockTableStatement;
+import liquibase.statement.core.RawSqlStatement;
+import org.jboss.logging.Logger;
+import org.keycloak.common.util.Time;
+import org.keycloak.common.util.reflections.Reflections;
+
+/**
+ * Liquibase lock service, which has some bugfixes and assumes timeouts to be configured in milliseconds
+ *
+ * @author Marek Posolda
+ */
+public class CustomLockService extends StandardLockService {
+
+ private static final Logger log = Logger.getLogger(CustomLockService.class);
+
+ private long changeLogLocRecheckTimeMillis = -1;
+
+ @Override
+ public void setChangeLogLockRecheckTime(long changeLogLocRecheckTime) {
+ super.setChangeLogLockRecheckTime(changeLogLocRecheckTime);
+ this.changeLogLocRecheckTimeMillis = changeLogLocRecheckTime;
+ }
+
+ // Bug in StandardLockService.getChangeLogLockRecheckTime()
+ @Override
+ public Long getChangeLogLockRecheckTime() {
+ if (changeLogLocRecheckTimeMillis == -1) {
+ return super.getChangeLogLockRecheckTime();
+ } else {
+ return changeLogLocRecheckTimeMillis;
+ }
+ }
+
+ @Override
+ public void init() throws DatabaseException {
+ boolean createdTable = false;
+ Executor executor = ExecutorService.getInstance().getExecutor(database);
+
+ if (!hasDatabaseChangeLogLockTable()) {
+
+ try {
+ if (log.isTraceEnabled()) {
+ log.trace("Create Database Lock Table");
+ }
+ executor.execute(new CreateDatabaseChangeLogLockTableStatement());
+ database.commit();
+ } catch (DatabaseException de) {
+ log.warn("Failed to create lock table. Maybe other transaction created in the meantime. Retrying...");
+ if (log.isDebugEnabled()) {
+ log.debug(de.getMessage(), de); //Log details at debug level
+ }
+ database.rollback();
+ throw new LockRetryException(de);
+ }
+
+ log.debugf("Created database lock table with name: %s", database.escapeTableName(database.getLiquibaseCatalogName(), database.getLiquibaseSchemaName(), database.getDatabaseChangeLogLockTableName()));
+
+ try {
+ Field field = Reflections.findDeclaredField(StandardLockService.class, "hasDatabaseChangeLogLockTable");
+ Reflections.setAccessible(field);
+ field.set(CustomLockService.this, true);
+ } catch (IllegalAccessException iae) {
+ throw new RuntimeException(iae);
+ }
+
+ createdTable = true;
+ }
+
+
+ if (!isDatabaseChangeLogLockTableInitialized(createdTable)) {
+ try {
+ if (log.isTraceEnabled()) {
+ log.trace("Initialize Database Lock Table");
+ }
+ executor.execute(new InitializeDatabaseChangeLogLockTableStatement());
+ database.commit();
+
+ } catch (DatabaseException de) {
+ log.warn("Failed to insert first record to the lock table. Maybe other transaction inserted in the meantime. Retrying...");
+ if (log.isDebugEnabled()) {
+ log.debug(de.getMessage(), de); // Log details at debug level
+ }
+ database.rollback();
+ throw new LockRetryException(de);
+ }
+
+ log.debug("Initialized record in the database lock table");
+ }
+
+
+ // Keycloak doesn't support Derby, but keep it for sure...
+ if (executor.updatesDatabase() && database instanceof DerbyDatabase && ((DerbyDatabase) database).supportsBooleanDataType()) { //check if the changelog table is of an old smallint vs. boolean format
+ String lockTable = database.escapeTableName(database.getLiquibaseCatalogName(), database.getLiquibaseSchemaName(), database.getDatabaseChangeLogLockTableName());
+ Object obj = executor.queryForObject(new RawSqlStatement("select min(locked) as test from " + lockTable + " fetch first row only"), Object.class);
+ if (!(obj instanceof Boolean)) { //wrong type, need to recreate table
+ executor.execute(new DropTableStatement(database.getLiquibaseCatalogName(), database.getLiquibaseSchemaName(), database.getDatabaseChangeLogLockTableName(), false));
+ executor.execute(new CreateDatabaseChangeLogLockTableStatement());
+ executor.execute(new InitializeDatabaseChangeLogLockTableStatement());
+ }
+ }
+
+ }
+
+ @Override
+ public void waitForLock() throws LockException {
+ boolean locked = false;
+ long startTime = Time.toMillis(Time.currentTime());
+ long timeToGiveUp = startTime + (getChangeLogLockWaitTime());
+
+ while (!locked && Time.toMillis(Time.currentTime()) < timeToGiveUp) {
+ locked = acquireLock();
+ if (!locked) {
+ int remainingTime = ((int)(timeToGiveUp / 1000)) - Time.currentTime();
+ log.debugf("Waiting for changelog lock... Remaining time: %d seconds", remainingTime);
+ try {
+ Thread.sleep(getChangeLogLockRecheckTime());
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ if (!locked) {
+ DatabaseChangeLogLock[] locks = listLocks();
+ String lockedBy;
+ if (locks.length > 0) {
+ DatabaseChangeLogLock lock = locks[0];
+ lockedBy = lock.getLockedBy() + " since " + DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(lock.getLockGranted());
+ } else {
+ lockedBy = "UNKNOWN";
+ }
+ throw new LockException("Could not acquire change log lock. Currently locked by " + lockedBy);
+ }
+ }
+
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/DummyLockService.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/DummyLockService.java
new file mode 100644
index 0000000000..61ef19fdfa
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/DummyLockService.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.connections.jpa.updater.liquibase.lock;
+
+import liquibase.exception.LockException;
+import liquibase.lockservice.StandardLockService;
+
+/**
+ * Dummy lock service injected to Liquibase. Doesn't need to do anything as we already have a lock when Liquibase update is called.
+ *
+ * @author Marek Posolda
+ */
+public class DummyLockService extends StandardLockService {
+
+ @Override
+ public void waitForLock() throws LockException {
+ }
+
+ @Override
+ public void releaseLock() throws LockException {
+ }
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/LiquibaseDBLockProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/LiquibaseDBLockProvider.java
new file mode 100644
index 0000000000..8769053102
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/LiquibaseDBLockProvider.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.connections.jpa.updater.liquibase.lock;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+
+import liquibase.Liquibase;
+import liquibase.exception.DatabaseException;
+import liquibase.exception.LiquibaseException;
+import liquibase.exception.LockException;
+import liquibase.lockservice.LockService;
+import org.jboss.logging.Logger;
+import org.keycloak.connections.jpa.JpaConnectionProvider;
+import org.keycloak.connections.jpa.JpaConnectionProviderFactory;
+import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.dblock.DBLockProvider;
+
+/**
+ * @author Marek Posolda
+ */
+public class LiquibaseDBLockProvider implements DBLockProvider {
+
+ private static final Logger logger = Logger.getLogger(LiquibaseDBLockProvider.class);
+
+ // 3 should be sufficient (Potentially one failure for createTable and one for insert record)
+ private int DEFAULT_MAX_ATTEMPTS = 3;
+
+
+ private final LiquibaseDBLockProviderFactory factory;
+ private final KeycloakSession session;
+
+ private LockService lockService;
+ private Connection dbConnection;
+
+ private int maxAttempts = DEFAULT_MAX_ATTEMPTS;
+
+ public LiquibaseDBLockProvider(LiquibaseDBLockProviderFactory factory, KeycloakSession session) {
+ this.factory = factory;
+ this.session = session;
+ init();
+ }
+
+ private void init() {
+ LiquibaseConnectionProvider liquibaseProvider = session.getProvider(LiquibaseConnectionProvider.class);
+ JpaConnectionProviderFactory jpaProviderFactory = (JpaConnectionProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(JpaConnectionProvider.class);
+
+ this.dbConnection = jpaProviderFactory.getConnection();
+ String defaultSchema = jpaProviderFactory.getSchema();
+
+ try {
+ Liquibase liquibase = liquibaseProvider.getLiquibase(dbConnection, defaultSchema);
+
+ this.lockService = new CustomLockService();
+ lockService.setChangeLogLockWaitTime(factory.getLockWaitTimeoutMillis());
+ lockService.setChangeLogLockRecheckTime(factory.getLockRecheckTimeMillis());
+ lockService.setDatabase(liquibase.getDatabase());
+ } catch (LiquibaseException exception) {
+ safeRollbackConnection();
+ safeCloseConnection();
+ throw new IllegalStateException(exception);
+ }
+ }
+
+ // Assumed transaction was rolled-back and we want to start with new DB connection
+ private void restart() {
+ safeCloseConnection();
+ this.dbConnection = null;
+ this.lockService = null;
+ init();
+ }
+
+
+ @Override
+ public void waitForLock() {
+ while (maxAttempts > 0) {
+ try {
+ lockService.waitForLock();
+ this.maxAttempts = DEFAULT_MAX_ATTEMPTS;
+ return;
+ } catch (LockException le) {
+ if (le.getCause() != null && le.getCause() instanceof LockRetryException) {
+ // Indicates we should try to acquire lock again in different transaction
+ restart();
+ maxAttempts--;
+ } else {
+ throw new IllegalStateException("Failed to retrieve lock", le);
+
+ // TODO: Possibility to forcefully retrieve lock after timeout instead of just give-up?
+ }
+ }
+ }
+ }
+
+
+ @Override
+ public void releaseLock() {
+ try {
+ lockService.releaseLock();
+ } catch (LockException e) {
+ logger.error("Could not release lock", e);
+ }
+ lockService.reset();
+ }
+
+ @Override
+ public void destroyLockInfo() {
+ try {
+ this.lockService.destroy();
+ dbConnection.commit();
+ logger.debug("Destroyed lock table");
+ } catch (DatabaseException | SQLException de) {
+ logger.error("Failed to destroy lock table");
+ safeRollbackConnection();
+ }
+ }
+
+ @Override
+ public void close() {
+ safeCloseConnection();
+ }
+
+ private void safeRollbackConnection() {
+ if (dbConnection != null) {
+ try {
+ this.dbConnection.rollback();
+ } catch (SQLException se) {
+ logger.warn("Failed to rollback connection after error", se);
+ }
+ }
+ }
+
+ private void safeCloseConnection() {
+ // Close after creating EntityManagerFactory to prevent in-mem databases from closing
+ if (dbConnection != null) {
+ try {
+ dbConnection.close();
+ } catch (SQLException e) {
+ logger.warn("Failed to close connection", e);
+ }
+ }
+ }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/LiquibaseDBLockProviderFactory.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/LiquibaseDBLockProviderFactory.java
new file mode 100644
index 0000000000..5ce8be3f47
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/LiquibaseDBLockProviderFactory.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.connections.jpa.updater.liquibase.lock;
+
+import org.jboss.logging.Logger;
+import org.keycloak.Config;
+import org.keycloak.common.util.Time;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.dblock.DBLockProviderFactory;
+
+/**
+ * @author Marek Posolda
+ */
+public class LiquibaseDBLockProviderFactory implements DBLockProviderFactory {
+
+ private static final Logger logger = Logger.getLogger(LiquibaseDBLockProviderFactory.class);
+
+ private long lockRecheckTimeMillis;
+ private long lockWaitTimeoutMillis;
+
+ protected long getLockRecheckTimeMillis() {
+ return lockRecheckTimeMillis;
+ }
+
+ protected long getLockWaitTimeoutMillis() {
+ return lockWaitTimeoutMillis;
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ int lockRecheckTime = config.getInt("lockRecheckTime", 2);
+ int lockWaitTimeout = config.getInt("lockWaitTimeout", 900);
+ this.lockRecheckTimeMillis = Time.toMillis(lockRecheckTime);
+ this.lockWaitTimeoutMillis = Time.toMillis(lockWaitTimeout);
+ logger.debugf("Liquibase lock provider configured with lockWaitTime: %d seconds, lockRecheckTime: %d seconds", lockWaitTimeout, lockRecheckTime);
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public LiquibaseDBLockProvider create(KeycloakSession session) {
+ return new LiquibaseDBLockProvider(this, session);
+ }
+
+ @Override
+ public void setTimeouts(long lockRecheckTimeMillis, long lockWaitTimeoutMillis) {
+ this.lockRecheckTimeMillis = lockRecheckTimeMillis;
+ this.lockWaitTimeoutMillis = lockWaitTimeoutMillis;
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public String getId() {
+ return "jpa";
+ }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/LockRetryException.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/LockRetryException.java
new file mode 100644
index 0000000000..e86e2b5dda
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/LockRetryException.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.connections.jpa.updater.liquibase.lock;
+
+/**
+ * Indicates that retrieve lock wasn't successful, but it worth to retry it in different transaction (For example if we were trying to create LOCK table, but other transaction
+ * created the table in the meantime etc)
+ *
+ * @author Marek Posolda
+ */
+public class LockRetryException extends RuntimeException {
+
+ public LockRetryException(String message) {
+ super(message);
+ }
+
+ public LockRetryException(Throwable cause) {
+ super(cause);
+ }
+
+ public LockRetryException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/model/jpa/src/main/resources/META-INF/db2-jpa-changelog-master.xml b/model/jpa/src/main/resources/META-INF/db2-jpa-changelog-master.xml
index ae4592d575..2d6b669676 100644
--- a/model/jpa/src/main/resources/META-INF/db2-jpa-changelog-master.xml
+++ b/model/jpa/src/main/resources/META-INF/db2-jpa-changelog-master.xml
@@ -30,4 +30,5 @@
+
diff --git a/model/jpa/src/main/resources/META-INF/services/org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProviderFactory b/model/jpa/src/main/resources/META-INF/services/org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProviderFactory
new file mode 100644
index 0000000000..f7e18f0ce0
--- /dev/null
+++ b/model/jpa/src/main/resources/META-INF/services/org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProviderFactory
@@ -0,0 +1,18 @@
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.connections.jpa.updater.liquibase.conn.DefaultLiquibaseConnectionProvider
\ No newline at end of file
diff --git a/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.dblock.DBLockProviderFactory b/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.dblock.DBLockProviderFactory
new file mode 100644
index 0000000000..2b2ac0234b
--- /dev/null
+++ b/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.dblock.DBLockProviderFactory
@@ -0,0 +1,18 @@
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.connections.jpa.updater.liquibase.lock.LiquibaseDBLockProviderFactory
\ No newline at end of file
diff --git a/model/jpa/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/model/jpa/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index 16b42ba3ee..94c651287a 100644
--- a/model/jpa/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/model/jpa/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -16,4 +16,5 @@
#
org.keycloak.connections.jpa.JpaConnectionSpi
-org.keycloak.connections.jpa.updater.JpaUpdaterSpi
\ No newline at end of file
+org.keycloak.connections.jpa.updater.JpaUpdaterSpi
+org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionSpi
\ No newline at end of file
diff --git a/model/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java b/model/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java
index 744d14e02e..74367c46d9 100755
--- a/model/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java
+++ b/model/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java
@@ -78,7 +78,13 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
private static final Logger logger = Logger.getLogger(DefaultMongoConnectionFactoryProvider.class);
- private volatile MongoClient client;
+ private static final int STATE_BEFORE_INIT = 0; // Even before MongoClient is created
+ private static final int STATE_BEFORE_UPDATE = 1; // Mongo client was created, but DB is not yet updated to last version
+ private static final int STATE_AFTER_UPDATE = 2; // Mongo client was created and DB updated. DB is fully initialized now
+
+ private volatile int state = STATE_BEFORE_INIT;
+
+ private MongoClient client;
private MongoStore mongoStore;
private DB db;
@@ -86,15 +92,6 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
private Map operationalInfo;
- @Override
- public MongoConnectionProvider create(KeycloakSession session) {
- lazyInit(session);
-
- TransactionMongoStoreInvocationContext invocationContext = new TransactionMongoStoreInvocationContext(mongoStore);
- session.getTransaction().enlist(new MongoKeycloakTransaction(invocationContext));
- return new DefaultMongoConnectionProvider(db, mongoStore, invocationContext);
- }
-
@Override
public void init(Config.Scope config) {
this.config = config;
@@ -105,33 +102,22 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
}
+ @Override
+ public DB getDBBeforeUpdate() {
+ lazyInitBeforeUpdate();
+ return db;
+ }
- private void lazyInit(KeycloakSession session) {
- if (client == null) {
+ private void lazyInitBeforeUpdate() {
+ if (state == STATE_BEFORE_INIT) {
synchronized (this) {
- if (client == null) {
+ if (state == STATE_BEFORE_INIT) {
try {
this.client = createMongoClient();
-
String dbName = config.get("db", "keycloak");
this.db = client.getDB(dbName);
- String databaseSchema = config.get("databaseSchema");
- if (databaseSchema != null) {
- if (databaseSchema.equals("update")) {
- MongoUpdaterProvider mongoUpdater = session.getProvider(MongoUpdaterProvider.class);
-
- if (mongoUpdater == null) {
- throw new RuntimeException("Can't update database: Mongo updater provider not found");
- }
-
- mongoUpdater.update(session, db);
- } else {
- throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema);
- }
- }
-
- this.mongoStore = new MongoStoreImpl(db, getManagedEntities());
+ state = STATE_BEFORE_UPDATE;
} catch (Exception e) {
throw new RuntimeException(e);
}
@@ -140,6 +126,53 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
}
}
+
+ @Override
+ public MongoConnectionProvider create(KeycloakSession session) {
+ lazyInit(session);
+
+ TransactionMongoStoreInvocationContext invocationContext = new TransactionMongoStoreInvocationContext(mongoStore);
+ session.getTransaction().enlist(new MongoKeycloakTransaction(invocationContext));
+ return new DefaultMongoConnectionProvider(db, mongoStore, invocationContext);
+ }
+
+ private void lazyInit(KeycloakSession session) {
+ lazyInitBeforeUpdate();
+
+ if (state == STATE_BEFORE_UPDATE) {
+ synchronized (this) {
+ if (state == STATE_BEFORE_UPDATE) {
+ try {
+ update(session);
+ this.mongoStore = new MongoStoreImpl(db, getManagedEntities());
+
+ state = STATE_AFTER_UPDATE;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ }
+ }
+
+ private void update(KeycloakSession session) {
+ String databaseSchema = config.get("databaseSchema");
+ if (databaseSchema != null) {
+ if (databaseSchema.equals("update")) {
+ MongoUpdaterProvider mongoUpdater = session.getProvider(MongoUpdaterProvider.class);
+
+ if (mongoUpdater == null) {
+ throw new RuntimeException("Can't update database: Mongo updater provider not found");
+ }
+
+ mongoUpdater.update(session, db);
+ } else {
+ throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema);
+ }
+ }
+ }
+
+
private Class[] getManagedEntities() throws ClassNotFoundException {
Class[] entityClasses = new Class[entities.length];
for (int i = 0; i < entities.length; i++) {
@@ -160,6 +193,7 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
return "default";
}
+
/**
* Override this method if you want more possibility to configure Mongo client. It can be also used to inject mongo client
* from different source.
diff --git a/model/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProvider.java b/model/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProvider.java
index 0e9f84a7a9..f13eaf7a4e 100644
--- a/model/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProvider.java
+++ b/model/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProvider.java
@@ -27,6 +27,9 @@ import org.keycloak.provider.Provider;
*/
public interface MongoConnectionProvider extends Provider {
+ /**
+ * @return Fully updated and initialized DB
+ */
DB getDB();
MongoStore getMongoStore();
diff --git a/model/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProviderFactory.java b/model/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProviderFactory.java
index 2d5d4ca149..42a18533d9 100644
--- a/model/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProviderFactory.java
+++ b/model/mongo/src/main/java/org/keycloak/connections/mongo/MongoConnectionProviderFactory.java
@@ -17,10 +17,17 @@
package org.keycloak.connections.mongo;
+import com.mongodb.DB;
import org.keycloak.provider.ProviderFactory;
/**
* @author Stian Thorgersen
*/
public interface MongoConnectionProviderFactory extends ProviderFactory {
+
+ /**
+ * @return DB object, which may not be yet updated to current Keycloak version. Useful just if something needs to be done even before DB update (for example acquire DB lock)
+ */
+ DB getDBBeforeUpdate();
+
}
diff --git a/model/mongo/src/main/java/org/keycloak/connections/mongo/lock/MongoDBLockProvider.java b/model/mongo/src/main/java/org/keycloak/connections/mongo/lock/MongoDBLockProvider.java
new file mode 100644
index 0000000000..e3383fece0
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/connections/mongo/lock/MongoDBLockProvider.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.connections.mongo.lock;
+
+import com.mongodb.BasicDBObject;
+import com.mongodb.DB;
+import com.mongodb.DBCursor;
+import com.mongodb.DBObject;
+import com.mongodb.DuplicateKeyException;
+import com.mongodb.WriteResult;
+import org.jboss.logging.Logger;
+import org.keycloak.common.util.HostUtils;
+import org.keycloak.common.util.Time;
+import org.keycloak.models.dblock.DBLockProvider;
+
+/**
+ * @author Marek Posolda
+ */
+public class MongoDBLockProvider implements DBLockProvider {
+
+ private static final String DB_LOCK_COLLECTION = "dblock";
+ private static final Logger logger = Logger.getLogger(MongoDBLockProvider .class);
+
+ private final MongoDBLockProviderFactory factory;
+ private final DB db;
+
+ public MongoDBLockProvider(MongoDBLockProviderFactory factory, DB db) {
+ this.factory = factory;
+ this.db = db;
+ }
+
+
+ @Override
+ public void waitForLock() {
+ boolean locked = false;
+ long startTime = Time.toMillis(Time.currentTime());
+ long timeToGiveUp = startTime + (factory.getLockWaitTimeoutMillis());
+
+ while (!locked && Time.toMillis(Time.currentTime()) < timeToGiveUp) {
+ locked = acquireLock();
+ if (!locked) {
+ int remainingTime = ((int)(timeToGiveUp / 1000)) - Time.currentTime();
+ logger.debugf("Waiting for changelog lock... Remaining time: %d seconds", remainingTime);
+ try {
+ Thread.sleep(factory.getLockRecheckTimeMillis());
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ if (!locked) {
+ DBObject query = new BasicDBObject("_id", 1);
+ DBCursor cursor = db.getCollection(DB_LOCK_COLLECTION).find(query);
+ String lockedBy;
+ if (cursor.hasNext()) {
+ DBObject dbObj = cursor.next();
+ lockedBy = dbObj.get("lockedBy") + " since " + Time.toDate(((int)((long) dbObj.get("lockedSince") / 1000)));
+ } else {
+ lockedBy = "UNKNOWN";
+ }
+ throw new IllegalStateException("Could not acquire change log lock. Currently locked by " + lockedBy);
+ }
+ }
+
+
+ private boolean acquireLock() {
+ DBObject query = new BasicDBObject("locked", false);
+
+ BasicDBObject update = new BasicDBObject("locked", true);
+ update.append("_id", 1);
+ update.append("lockedSince", Time.toMillis(Time.currentTime()));
+ update.append("lockedBy", HostUtils.getHostName()); // Maybe replace with something better, but doesn't matter for now
+
+ try {
+ WriteResult wr = db.getCollection(DB_LOCK_COLLECTION).update(query, update, true, false);
+ if (wr.getN() == 1) {
+ logger.debugf("Successfully acquired DB lock");
+ return true;
+ } else {
+ return false;
+ }
+ } catch (DuplicateKeyException dke) {
+ logger.debugf("Failed acquire lock. Reason: %s", dke.getMessage());
+ }
+
+ return false;
+ }
+
+
+ @Override
+ public void releaseLock() {
+ DBObject query = new BasicDBObject("locked", true);
+
+ BasicDBObject update = new BasicDBObject("locked", false);
+ update.append("_id", 1);
+ update.append("lockedBy", null);
+ update.append("lockedSince", null);
+
+ try {
+ WriteResult wr = db.getCollection(DB_LOCK_COLLECTION).update(query, update, true, false);
+ if (wr.getN() > 0) {
+ logger.debugf("Successfully released DB lock");
+ } else {
+ logger.warnf("Attempt to release DB lock, but nothing was released");
+ }
+ } catch (DuplicateKeyException dke) {
+ logger.debugf("Failed release lock. Reason: %s", dke.getMessage());
+ }
+ }
+
+ @Override
+ public void destroyLockInfo() {
+ db.getCollection(DB_LOCK_COLLECTION).remove(new BasicDBObject());
+ logger.debugf("Destroyed lock collection");
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/connections/mongo/lock/MongoDBLockProviderFactory.java b/model/mongo/src/main/java/org/keycloak/connections/mongo/lock/MongoDBLockProviderFactory.java
new file mode 100644
index 0000000000..7bd6e02181
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/connections/mongo/lock/MongoDBLockProviderFactory.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.connections.mongo.lock;
+
+import com.mongodb.DB;
+import org.jboss.logging.Logger;
+import org.keycloak.Config;
+import org.keycloak.common.util.Time;
+import org.keycloak.connections.mongo.MongoConnectionProvider;
+import org.keycloak.connections.mongo.MongoConnectionProviderFactory;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.dblock.DBLockProviderFactory;
+
+/**
+ * @author Marek Posolda
+ */
+public class MongoDBLockProviderFactory implements DBLockProviderFactory {
+
+ private static final Logger logger = Logger.getLogger(MongoDBLockProviderFactory.class);
+
+ private long lockRecheckTimeMillis;
+ private long lockWaitTimeoutMillis;
+
+ protected long getLockRecheckTimeMillis() {
+ return lockRecheckTimeMillis;
+ }
+
+ protected long getLockWaitTimeoutMillis() {
+ return lockWaitTimeoutMillis;
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ int lockRecheckTime = config.getInt("lockRecheckTime", 2);
+ int lockWaitTimeout = config.getInt("lockWaitTimeout", 900);
+ this.lockRecheckTimeMillis = Time.toMillis(lockRecheckTime);
+ this.lockWaitTimeoutMillis = Time.toMillis(lockWaitTimeout);
+ logger.debugf("Mongo lock provider configured with lockWaitTime: %d seconds, lockRecheckTime: %d seconds", lockWaitTimeout, lockRecheckTime);
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public MongoDBLockProvider create(KeycloakSession session) {
+ MongoConnectionProviderFactory mongoConnectionFactory = (MongoConnectionProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(MongoConnectionProvider.class);
+ DB db = mongoConnectionFactory.getDBBeforeUpdate();
+ return new MongoDBLockProvider(this, db);
+ }
+
+ @Override
+ public void setTimeouts(long lockRecheckTimeMillis, long lockWaitTimeoutMillis) {
+ this.lockRecheckTimeMillis = lockRecheckTimeMillis;
+ this.lockWaitTimeoutMillis = lockWaitTimeoutMillis;
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public String getId() {
+ return "mongo";
+ }
+}
diff --git a/model/mongo/src/main/resources/META-INF/services/org.keycloak.models.dblock.DBLockProviderFactory b/model/mongo/src/main/resources/META-INF/services/org.keycloak.models.dblock.DBLockProviderFactory
new file mode 100644
index 0000000000..4c6c4aa4c8
--- /dev/null
+++ b/model/mongo/src/main/resources/META-INF/services/org.keycloak.models.dblock.DBLockProviderFactory
@@ -0,0 +1,35 @@
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.connections.mongo.lock.MongoDBLockProviderFactory
\ No newline at end of file
diff --git a/server-spi/src/main/java/org/keycloak/models/dblock/DBLockProvider.java b/server-spi/src/main/java/org/keycloak/models/dblock/DBLockProvider.java
new file mode 100644
index 0000000000..b5fb417aca
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/models/dblock/DBLockProvider.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.dblock;
+
+import org.keycloak.provider.Provider;
+
+/**
+ * Global database lock to ensure that some actions in DB can be done just be one cluster node at a time.
+ *
+ *
+ * @author Marek Posolda
+ */
+public interface DBLockProvider extends Provider {
+
+
+ /**
+ * Try to retrieve DB lock or wait if retrieve was unsuccessful. Throw exception if lock can't be retrieved within specified timeout (900 seconds by default)
+ */
+ void waitForLock();
+
+
+ void releaseLock();
+
+
+ /**
+ * Will destroy whole state of DB lock (drop table/collection to track locking).
+ * */
+ void destroyLockInfo();
+}
diff --git a/server-spi/src/main/java/org/keycloak/models/dblock/DBLockProviderFactory.java b/server-spi/src/main/java/org/keycloak/models/dblock/DBLockProviderFactory.java
new file mode 100644
index 0000000000..747a168126
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/models/dblock/DBLockProviderFactory.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.dblock;
+
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ * @author Marek Posolda
+ */
+public interface DBLockProviderFactory extends ProviderFactory {
+
+ /**
+ * Useful for testing to override provided configuration
+ */
+ void setTimeouts(long lockRecheckTimeMillis, long lockWaitTimeoutMillis);
+}
diff --git a/server-spi/src/main/java/org/keycloak/models/dblock/DBLockSpi.java b/server-spi/src/main/java/org/keycloak/models/dblock/DBLockSpi.java
new file mode 100644
index 0000000000..cd78f0f0e7
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/models/dblock/DBLockSpi.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.dblock;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * @author Marek Posolda
+ */
+public class DBLockSpi implements Spi {
+
+ @Override
+ public boolean isInternal() {
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return "dblock";
+ }
+
+ @Override
+ public Class extends Provider> getProviderClass() {
+ return DBLockProvider.class;
+ }
+
+ @Override
+ public Class extends ProviderFactory> getProviderFactoryClass() {
+ return DBLockProviderFactory.class;
+ }
+}
diff --git a/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index 7bb58fc556..ffbb0ac35a 100755
--- a/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -21,6 +21,7 @@ org.keycloak.models.RealmSpi
org.keycloak.models.UserSessionSpi
org.keycloak.models.UserSpi
org.keycloak.models.session.UserSessionPersisterSpi
+org.keycloak.models.dblock.DBLockSpi
org.keycloak.migration.MigrationSpi
org.keycloak.hash.PasswordHashSpi
org.keycloak.events.EventListenerSpi
diff --git a/services/src/main/java/org/keycloak/services/ServicesLogger.java b/services/src/main/java/org/keycloak/services/ServicesLogger.java
index 0927f38592..8af67e5d7e 100644
--- a/services/src/main/java/org/keycloak/services/ServicesLogger.java
+++ b/services/src/main/java/org/keycloak/services/ServicesLogger.java
@@ -401,4 +401,8 @@ public interface ServicesLogger extends BasicLogger {
@LogMessage(level = ERROR)
@Message(id=90, value="Failed to close ProviderSession")
void failedToCloseProviderSession(@Cause Throwable t);
+
+ @LogMessage(level = WARN)
+ @Message(id=91, value="Forced release of DB lock at startup requested by System property. Make sure to not use this in production environment! And especially when more cluster nodes are started concurrently.")
+ void forcedReleaseDBLock();
}
diff --git a/services/src/main/java/org/keycloak/services/managers/DBLockManager.java b/services/src/main/java/org/keycloak/services/managers/DBLockManager.java
new file mode 100644
index 0000000000..1909c36f41
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/managers/DBLockManager.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.services.managers;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.KeycloakSessionTask;
+import org.keycloak.models.RealmProvider;
+import org.keycloak.models.RealmProviderFactory;
+import org.keycloak.models.dblock.DBLockProvider;
+import org.keycloak.models.dblock.DBLockProviderFactory;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.services.ServicesLogger;
+
+/**
+ * @author Marek Posolda
+ */
+public class DBLockManager {
+
+ protected static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
+
+ public void waitForLock(KeycloakSessionFactory sessionFactory) {
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+ @Override
+ public void run(KeycloakSession session) {
+ DBLockProvider lock = getDBLock(session);
+ lock.waitForLock();
+ }
+
+ });
+ }
+
+
+ public void releaseLock(KeycloakSessionFactory sessionFactory) {
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+ @Override
+ public void run(KeycloakSession session) {
+ DBLockProvider lock = getDBLock(session);
+ lock.releaseLock();
+ }
+
+ });
+ }
+
+
+ public void checkForcedUnlock(KeycloakSessionFactory sessionFactory) {
+ if (Boolean.getBoolean("keycloak.dblock.forceUnlock")) {
+ logger.forcedReleaseDBLock();
+ releaseLock(sessionFactory);
+ }
+ }
+
+
+ // Try to detect ID from realmProvider
+ public DBLockProvider getDBLock(KeycloakSession session) {
+ String realmProviderId = getRealmProviderId(session);
+ return session.getProvider(DBLockProvider.class, realmProviderId);
+ }
+
+ public DBLockProviderFactory getDBLockFactory(KeycloakSession session) {
+ String realmProviderId = getRealmProviderId(session);
+ return (DBLockProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(DBLockProvider.class, realmProviderId);
+ }
+
+ private String getRealmProviderId(KeycloakSession session) {
+ RealmProviderFactory realmProviderFactory = (RealmProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(RealmProvider.class);
+ return realmProviderFactory.getId();
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
index 0ef806bfe7..55feae3d93 100644
--- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
+++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
@@ -25,6 +25,7 @@ import org.keycloak.Config;
import org.keycloak.exportimport.ExportImportManager;
import org.keycloak.migration.MigrationModelManager;
import org.keycloak.models.*;
+import org.keycloak.services.managers.DBLockManager;
import org.keycloak.models.utils.PostMigrationEvent;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.representations.idm.RealmRepresentation;
@@ -39,7 +40,6 @@ import org.keycloak.services.resources.admin.AdminRoot;
import org.keycloak.services.scheduled.ClearExpiredEvents;
import org.keycloak.services.scheduled.ClearExpiredUserSessions;
import org.keycloak.services.scheduled.ClusterAwareScheduledTaskRunner;
-import org.keycloak.services.scheduled.ScheduledTaskRunner;
import org.keycloak.services.util.JsonConfigProvider;
import org.keycloak.services.util.ObjectMapperResolver;
import org.keycloak.timer.TimerProvider;
@@ -91,44 +91,56 @@ public class KeycloakApplication extends Application {
singletons.add(new ObjectMapperResolver(Boolean.parseBoolean(System.getProperty("keycloak.jsonPrettyPrint", "false"))));
- migrateModel();
-
- boolean bootstrapAdminUser = false;
-
- KeycloakSession session = sessionFactory.create();
ExportImportManager exportImportManager;
+
+ DBLockManager dbLockManager = new DBLockManager();
+ dbLockManager.checkForcedUnlock(sessionFactory);
+ dbLockManager.waitForLock(sessionFactory);
try {
- session.getTransaction().begin();
+ migrateModel();
- ApplianceBootstrap applianceBootstrap = new ApplianceBootstrap(session);
- exportImportManager = new ExportImportManager(session);
+ KeycloakSession session = sessionFactory.create();
+ try {
+ session.getTransaction().begin();
- boolean createMasterRealm = applianceBootstrap.isNewInstall();
- if (exportImportManager.isRunImport() && exportImportManager.isImportMasterIncluded()) {
- createMasterRealm = false;
+ ApplianceBootstrap applianceBootstrap = new ApplianceBootstrap(session);
+ exportImportManager = new ExportImportManager(session);
+
+ boolean createMasterRealm = applianceBootstrap.isNewInstall();
+ if (exportImportManager.isRunImport() && exportImportManager.isImportMasterIncluded()) {
+ createMasterRealm = false;
+ }
+
+ if (createMasterRealm) {
+ applianceBootstrap.createMasterRealm(contextPath);
+ }
+ session.getTransaction().commit();
+ } catch (RuntimeException re) {
+ if (session.getTransaction().isActive()) {
+ session.getTransaction().rollback();
+ }
+ throw re;
+ } finally {
+ session.close();
}
- if (createMasterRealm) {
- applianceBootstrap.createMasterRealm(contextPath);
+ if (exportImportManager.isRunImport()) {
+ exportImportManager.runImport();
+ } else {
+ importRealms();
}
- session.getTransaction().commit();
+
+ importAddUser();
} finally {
- session.close();
+ dbLockManager.releaseLock(sessionFactory);
}
- if (exportImportManager.isRunImport()) {
- exportImportManager.runImport();
- } else {
- importRealms();
- }
-
- importAddUser();
-
if (exportImportManager.isRunExport()) {
exportImportManager.runExport();
}
- session = sessionFactory.create();
+ boolean bootstrapAdminUser = false;
+ KeycloakSession session = sessionFactory.create();
try {
session.getTransaction().begin();
bootstrapAdminUser = new ApplianceBootstrap(session).isNoMasterUser();
diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml
index 531a2ae3ae..6660302226 100755
--- a/testsuite/integration/pom.xml
+++ b/testsuite/integration/pom.xml
@@ -237,6 +237,16 @@
test
+
+ mysql
+ mysql-connector-java
+
+
+ org.postgresql
+ postgresql
+ ${postgresql.version}
+
+
@@ -468,41 +478,6 @@
-
-
-
-
- keycloak.connectionsJpa.driver
- com.mysql.jdbc.Driver
-
-
- mysql
-
-
- mysql
- mysql-connector-java
-
-
-
-
-
-
-
-
- keycloak.connectionsJpa.driver
- org.postgresql.Driver
-
-
- postgresql
-
-
- org.postgresql
- postgresql
- ${postgresql.version}
-
-
-
-
clean-jpa
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/DBLockTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/DBLockTest.java
new file mode 100644
index 0000000000..f33362ce50
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/DBLockTest.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.model;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.jboss.logging.Logger;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.KeycloakSessionTask;
+import org.keycloak.services.managers.DBLockManager;
+import org.keycloak.models.dblock.DBLockProvider;
+import org.keycloak.models.dblock.DBLockProviderFactory;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+/**
+ * @author Marek Posolda
+ */
+public class DBLockTest extends AbstractModelTest {
+
+ private static final Logger log = Logger.getLogger(DBLockTest.class);
+
+ private static final int SLEEP_TIME_MILLIS = 10;
+ private static final int THREADS_COUNT = 20;
+ private static final int ITERATIONS_PER_THREAD = 2;
+
+ private static final int LOCK_TIMEOUT_MILLIS = 240000; // Rather bigger to handle slow DB connections in testing env
+ private static final int LOCK_RECHECK_MILLIS = 10;
+
+ @Before
+ @Override
+ public void before() throws Exception {
+ super.before();
+
+ // Set timeouts for testing
+ DBLockManager lockManager = new DBLockManager();
+ DBLockProviderFactory lockFactory = lockManager.getDBLockFactory(session);
+ lockFactory.setTimeouts(LOCK_RECHECK_MILLIS, LOCK_TIMEOUT_MILLIS);
+
+ // Drop lock table, just to simulate racing threads for create lock table and insert lock record into it.
+ lockManager.getDBLock(session).destroyLockInfo();
+
+ commit();
+ }
+
+ @Test
+ public void testLockConcurrently() throws Exception {
+ long startupTime = System.currentTimeMillis();
+
+ final Semaphore semaphore = new Semaphore();
+ final KeycloakSessionFactory sessionFactory = realmManager.getSession().getKeycloakSessionFactory();
+
+ List threads = new LinkedList<>();
+ for (int i=0 ; i(expected.getDefaultRoles()), new HashSet<>(actual.getDefaultRoles()));
Assert.assertEquals(expected.getSmtpConfig(), actual.getSmtpConfig());
diff --git a/testsuite/integration/src/test/resources/log4j.properties b/testsuite/integration/src/test/resources/log4j.properties
index a199a4174d..fd730de9eb 100755
--- a/testsuite/integration/src/test/resources/log4j.properties
+++ b/testsuite/integration/src/test/resources/log4j.properties
@@ -39,7 +39,7 @@ log4j.logger.org.keycloak.testsuite=${keycloak.testsuite.logging.level}
# Liquibase updates logged with "info" by default. Logging level can be changed by system property "keycloak.liquibase.logging.level"
keycloak.liquibase.logging.level=info
-log4j.logger.org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProvider=${keycloak.liquibase.logging.level}
+log4j.logger.org.keycloak.connections.jpa.updater.liquibase=${keycloak.liquibase.logging.level}
# Enable to view infinispan initialization
# log4j.logger.org.keycloak.models.sessions.infinispan.initializer=trace