Skip to content

Commit 8da768a

Browse files
committed
KEYCLOAK-2529 Concurrent startup by more cluster nodes at the same time. Added DBLockProvider
1 parent ad63b18 commit 8da768a

File tree

37 files changed

+1751
-240
lines changed

37 files changed

+1751
-240
lines changed

docbook/auth-server-docs/reference/en/en-US/modules/clustering.xml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,29 @@
5757
database. This can be a relational database or Mongo. To make sure your database doesn't become a single
5858
point of failure you may also want to deploy your database to a cluster.
5959
</para>
60+
<section>
61+
<title>DB lock</title>
62+
<para>Note that Keycloak supports concurrent startup by more cluster nodes at the same. This is ensured by DB lock, which prevents that some
63+
startup actions (migrating database from previous version, importing realms at startup, initial bootstrap of admin user) are always executed just by one
64+
cluster node at a time and other cluster nodes need to wait until the current node finishes startup actions and release the DB lock.
65+
</para>
66+
<para>
67+
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.
68+
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
69+
it's possible to configure it in <literal>standalone/configuration/keycloak-server.json</literal>:
70+
<programlisting>
71+
<![CDATA[
72+
"dblock": {
73+
"jpa": {
74+
"lockWaitTimeout": 900,
75+
"lockRecheckTime": 2
76+
}
77+
}
78+
]]>
79+
</programlisting>
80+
or similarly if you're using Mongo (just by replace <literal>jpa</literal> with <literal>mongo</literal>)
81+
</para>
82+
</section>
6083
</section>
6184

6285
<section>

model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ private void lazyInit(KeycloakSession session) {
126126
properties.put("hibernate.dialect", driverDialect);
127127
}
128128

129-
String schema = config.get("schema");
129+
String schema = getSchema();
130130
if (schema != null) {
131131
properties.put(JpaUtils.HIBERNATE_DEFAULT_SCHEMA, schema);
132132
}
@@ -167,7 +167,7 @@ private void lazyInit(KeycloakSession session) {
167167
}
168168

169169
if (currentVersion == null || !JpaUpdaterProvider.LAST_VERSION.equals(currentVersion)) {
170-
updater.update(session, connection, schema);
170+
updater.update(connection, schema);
171171
} else {
172172
logger.debug("Database is up to date");
173173
}
@@ -212,7 +212,8 @@ protected void prepareOperationalInfo(Connection connection) {
212212
}
213213
}
214214

215-
private Connection getConnection() {
215+
@Override
216+
public Connection getConnection() {
216217
try {
217218
String dataSourceLookup = config.get("dataSource");
218219
if (dataSourceLookup != null) {
@@ -226,6 +227,11 @@ private Connection getConnection() {
226227
throw new RuntimeException("Failed to connect to database", e);
227228
}
228229
}
230+
231+
@Override
232+
public String getSchema() {
233+
return config.get("schema");
234+
}
229235

230236
@Override
231237
public Map<String,String> getOperationalInfo() {

model/jpa/src/main/java/org/keycloak/connections/jpa/JpaConnectionProviderFactory.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,18 @@
1717

1818
package org.keycloak.connections.jpa;
1919

20+
import java.sql.Connection;
21+
2022
import org.keycloak.provider.ProviderFactory;
2123

2224
/**
2325
* @author <a href="mailto:[email protected]">Stian Thorgersen</a>
2426
*/
2527
public interface JpaConnectionProviderFactory extends ProviderFactory<JpaConnectionProvider> {
2628

29+
// Caller is responsible for closing connection
30+
Connection getConnection();
31+
32+
String getSchema();
33+
2734
}

model/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
package org.keycloak.connections.jpa.updater;
1919

20-
import org.keycloak.models.KeycloakSession;
2120
import org.keycloak.provider.Provider;
2221

2322
import java.sql.Connection;
@@ -33,7 +32,7 @@ public interface JpaUpdaterProvider extends Provider {
3332

3433
public String getCurrentVersionSql(String defaultSchema);
3534

36-
public void update(KeycloakSession session, Connection connection, String defaultSchema);
35+
public void update(Connection connection, String defaultSchema);
3736

3837
public void validate(Connection connection, String defaultSchema);
3938

model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java

Lines changed: 12 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,10 @@
2020
import liquibase.Contexts;
2121
import liquibase.Liquibase;
2222
import liquibase.changelog.ChangeSet;
23-
import liquibase.changelog.DatabaseChangeLog;
2423
import liquibase.changelog.RanChangeSet;
25-
import liquibase.database.Database;
26-
import liquibase.database.DatabaseFactory;
27-
import liquibase.database.core.DB2Database;
28-
import liquibase.database.jvm.JdbcConnection;
29-
import liquibase.logging.LogFactory;
30-
import liquibase.logging.LogLevel;
31-
import liquibase.resource.ClassLoaderResourceAccessor;
32-
import liquibase.servicelocator.ServiceLocator;
3324
import org.jboss.logging.Logger;
3425
import org.keycloak.connections.jpa.updater.JpaUpdaterProvider;
26+
import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProvider;
3527
import org.keycloak.models.KeycloakSession;
3628

3729
import java.sql.Connection;
@@ -46,16 +38,22 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
4638

4739
private static final Logger logger = Logger.getLogger(LiquibaseJpaUpdaterProvider.class);
4840

49-
private static final String CHANGELOG = "META-INF/jpa-changelog-master.xml";
50-
private static final String DB2_CHANGELOG = "META-INF/db2-jpa-changelog-master.xml";
41+
public static final String CHANGELOG = "META-INF/jpa-changelog-master.xml";
42+
public static final String DB2_CHANGELOG = "META-INF/db2-jpa-changelog-master.xml";
43+
44+
private final KeycloakSession session;
45+
46+
public LiquibaseJpaUpdaterProvider(KeycloakSession session) {
47+
this.session = session;
48+
}
5149

5250
@Override
5351
public String getCurrentVersionSql(String defaultSchema) {
5452
return "SELECT ID from " + getTable("DATABASECHANGELOG", defaultSchema) + " ORDER BY DATEEXECUTED DESC LIMIT 1";
5553
}
5654

5755
@Override
58-
public void update(KeycloakSession session, Connection connection, String defaultSchema) {
56+
public void update(Connection connection, String defaultSchema) {
5957
logger.debug("Starting database update");
6058

6159
// Need ThreadLocal as liquibase doesn't seem to have API to inject custom objects into tasks
@@ -108,145 +106,14 @@ public void validate(Connection connection, String defaultSchema) {
108106
}
109107

110108
private Liquibase getLiquibase(Connection connection, String defaultSchema) throws Exception {
111-
ServiceLocator sl = ServiceLocator.getInstance();
112-
113-
if (!System.getProperties().containsKey("liquibase.scan.packages")) {
114-
if (sl.getPackages().remove("liquibase.core")) {
115-
sl.addPackageToScan("liquibase.core.xml");
116-
}
117-
118-
if (sl.getPackages().remove("liquibase.parser")) {
119-
sl.addPackageToScan("liquibase.parser.core.xml");
120-
}
121-
122-
if (sl.getPackages().remove("liquibase.serializer")) {
123-
sl.addPackageToScan("liquibase.serializer.core.xml");
124-
}
125-
126-
sl.getPackages().remove("liquibase.ext");
127-
sl.getPackages().remove("liquibase.sdk");
128-
}
129-
130-
LogFactory.setInstance(new LogWrapper());
131-
132-
// Adding PostgresPlus support to liquibase
133-
DatabaseFactory.getInstance().register(new PostgresPlusDatabase());
134-
135-
Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
136-
if (defaultSchema != null) {
137-
database.setDefaultSchemaName(defaultSchema);
138-
}
139-
140-
String changelog = (database instanceof DB2Database) ? DB2_CHANGELOG : CHANGELOG;
141-
logger.debugf("Using changelog file: %s", changelog);
142-
return new Liquibase(changelog, new ClassLoaderResourceAccessor(getClass().getClassLoader()), database);
109+
LiquibaseConnectionProvider liquibaseProvider = session.getProvider(LiquibaseConnectionProvider.class);
110+
return liquibaseProvider.getLiquibase(connection, defaultSchema);
143111
}
144112

145113
@Override
146114
public void close() {
147115
}
148116

149-
private static class LogWrapper extends LogFactory {
150-
151-
private liquibase.logging.Logger logger = new liquibase.logging.Logger() {
152-
@Override
153-
public void setName(String name) {
154-
}
155-
156-
@Override
157-
public void setLogLevel(String level) {
158-
}
159-
160-
@Override
161-
public void setLogLevel(LogLevel level) {
162-
}
163-
164-
@Override
165-
public void setLogLevel(String logLevel, String logFile) {
166-
}
167-
168-
@Override
169-
public void severe(String message) {
170-
LiquibaseJpaUpdaterProvider.logger.error(message);
171-
}
172-
173-
@Override
174-
public void severe(String message, Throwable e) {
175-
LiquibaseJpaUpdaterProvider.logger.error(message, e);
176-
}
177-
178-
@Override
179-
public void warning(String message) {
180-
// Ignore this warning as cascaded drops doesn't work anyway with all DBs, which we need to support
181-
if ("Database does not support drop with cascade".equals(message)) {
182-
LiquibaseJpaUpdaterProvider.logger.debug(message);
183-
} else {
184-
LiquibaseJpaUpdaterProvider.logger.warn(message);
185-
}
186-
}
187-
188-
@Override
189-
public void warning(String message, Throwable e) {
190-
LiquibaseJpaUpdaterProvider.logger.warn(message, e);
191-
}
192-
193-
@Override
194-
public void info(String message) {
195-
LiquibaseJpaUpdaterProvider.logger.debug(message);
196-
}
197-
198-
@Override
199-
public void info(String message, Throwable e) {
200-
LiquibaseJpaUpdaterProvider.logger.debug(message, e);
201-
}
202-
203-
@Override
204-
public void debug(String message) {
205-
LiquibaseJpaUpdaterProvider.logger.trace(message);
206-
}
207-
208-
@Override
209-
public LogLevel getLogLevel() {
210-
if (LiquibaseJpaUpdaterProvider.logger.isTraceEnabled()) {
211-
return LogLevel.DEBUG;
212-
} else if (LiquibaseJpaUpdaterProvider.logger.isDebugEnabled()) {
213-
return LogLevel.INFO;
214-
} else {
215-
return LogLevel.WARNING;
216-
}
217-
}
218-
219-
@Override
220-
public void debug(String message, Throwable e) {
221-
LiquibaseJpaUpdaterProvider.logger.trace(message, e);
222-
}
223-
224-
@Override
225-
public void setChangeLog(DatabaseChangeLog databaseChangeLog) {
226-
}
227-
228-
@Override
229-
public void setChangeSet(ChangeSet changeSet) {
230-
}
231-
232-
@Override
233-
public int getPriority() {
234-
return 0;
235-
}
236-
};
237-
238-
@Override
239-
public liquibase.logging.Logger getLog(String name) {
240-
return logger;
241-
}
242-
243-
@Override
244-
public liquibase.logging.Logger getLog() {
245-
return logger;
246-
}
247-
248-
}
249-
250117
public static String getTable(String table, String defaultSchema) {
251118
return defaultSchema != null ? defaultSchema + "." + table : table;
252119
}

model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProviderFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public class LiquibaseJpaUpdaterProviderFactory implements JpaUpdaterProviderFac
3030

3131
@Override
3232
public JpaUpdaterProvider create(KeycloakSession session) {
33-
return new LiquibaseJpaUpdaterProvider();
33+
return new LiquibaseJpaUpdaterProvider(session);
3434
}
3535

3636
@Override

0 commit comments

Comments
 (0)