KEYCLOAK-18073 avoid ModelDuplicateException during parallel starup of servers

This commit is contained in:
vramik 2021-05-24 19:44:07 +02:00 committed by Hynek Mlnařík
parent eb631bf63b
commit 3aa06c2721
11 changed files with 219 additions and 97 deletions

View file

@ -24,6 +24,7 @@ embed-server
/subsystem=keycloak-server/spi=deploymentState:add(default-provider=map)
/subsystem=keycloak-server/spi=deploymentState/provider=map:add(enabled=true,properties={resourcesVersionSeed=1JZ379bzyOCFA})
/subsystem=keycloak-server/spi=user:add(default-provider=map)
/subsystem=keycloak-server/spi=dblock:add(default-provider=none)
## For dev and single-node purposes, these are set to "map".
## For clustered deployments, these should be "infinispan" as map storage does not support distributed storage yet

View file

@ -0,0 +1,89 @@
/*
* Copyright 2021 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.Config;
import org.keycloak.common.Profile;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
public class NoLockingDBLockProviderFactory implements DBLockProviderFactory, EnvironmentDependentProviderFactory {
public static final String PROVIDER_ID = "none";
@Override
public void setTimeouts(long lockRecheckTimeMillis, long lockWaitTimeoutMillis) {
}
@Override
public DBLockProvider create(KeycloakSession session) {
return INSTANCE;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public boolean isSupported() {
return Profile.isFeatureEnabled(Profile.Feature.MAP_STORAGE);
}
private static final DBLockProvider INSTANCE = new DBLockProvider() {
@Override
public void waitForLock(DBLockProvider.Namespace lock) {
}
@Override
public void releaseLock() {
}
@Override
public DBLockProvider.Namespace getCurrentLock() {
return null;
}
@Override
public boolean supportsForcedUnlock() {
return false;
}
@Override
public void destroyLockInfo() {
}
@Override
public void close() {
}
};
}

View file

@ -0,0 +1,18 @@
#
# Copyright 2021 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.models.dblock.NoLockingDBLockProviderFactory

View file

@ -29,6 +29,8 @@ import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.models.dblock.DBLockManager;
import org.keycloak.models.dblock.DBLockProvider;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.PostMigrationEvent;
import org.keycloak.models.utils.RepresentationToModel;
@ -119,12 +121,27 @@ public class KeycloakApplication extends Application {
}
protected void startup() {
this.sessionFactory = createSessionFactory();
KeycloakApplication.sessionFactory = createSessionFactory();
ExportImportManager exportImportManager = bootstrap();
ExportImportManager[] exportImportManager = new ExportImportManager[1];
if (exportImportManager.isRunExport()) {
exportImportManager.runExport();
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
@Override
public void run(KeycloakSession session) {
DBLockManager dbLockManager = new DBLockManager(session);
dbLockManager.checkForcedUnlock();
DBLockProvider dbLock = dbLockManager.getDBLock();
dbLock.waitForLock(DBLockProvider.Namespace.KEYCLOAK_BOOT);
try {
exportImportManager[0] = bootstrap();
} finally {
dbLock.releaseLock();
}
}
});
if (exportImportManager[0].isRunExport()) {
exportImportManager[0].runExport();
}
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {

View file

@ -1188,6 +1188,7 @@
<systemPropertyVariables>
<keycloak.profile.feature.map_storage>enabled</keycloak.profile.feature.map_storage>
<keycloak.mapStorage.provider>concurrenthashmap</keycloak.mapStorage.provider>
<keycloak.dblock.provider>none</keycloak.dblock.provider>
<keycloak.realm.provider>map</keycloak.realm.provider>
<keycloak.client.provider>map</keycloak.client.provider>
<keycloak.clientScope.provider>map</keycloak.clientScope.provider>

View file

@ -43,6 +43,10 @@
}
},
"dblock": {
"provider": "${keycloak.dblock.provider:jpa}"
},
"realm": {
"provider": "${keycloak.realm.provider:jpa}"
},

View file

@ -17,6 +17,9 @@
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;
@ -27,22 +30,12 @@ import org.keycloak.models.dblock.DBLockManager;
import org.keycloak.models.dblock.DBLockProvider;
import org.keycloak.models.dblock.DBLockProviderFactory;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.ModelTest;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@AuthServerContainerExclude(AuthServer.REMOTE)
public class DBLockTest extends AbstractTestRealmKeycloakTest {
@RequireProvider(value=DBLockProvider.class, only="jpa")
public class DBLockTest extends KeycloakModelTest {
private static final Logger log = Logger.getLogger(DBLockTest.class);
@ -58,8 +51,7 @@ public class DBLockTest extends AbstractTestRealmKeycloakTest {
@Before
public void before() throws Exception {
testingClient.server().run(session -> {
inComittedTransaction(1, (session , i) -> {
// Set timeouts for testing
DBLockManager lockManager = new DBLockManager(session);
DBLockProviderFactory lockFactory = lockManager.getDBLockFactory();
@ -67,15 +59,14 @@ public class DBLockTest extends AbstractTestRealmKeycloakTest {
// Drop lock table, just to simulate racing threads for create lock table and insert lock record into it.
lockManager.getDBLock().destroyLockInfo();
return null;
});
}
@Test
@ModelTest
public void simpleLockTest(KeycloakSession session) throws Exception {
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionLC) -> {
DBLockProvider dbLock = new DBLockManager(sessionLC).getDBLock();
public void simpleLockTest() throws Exception {
inComittedTransaction(1, (session , i) -> {
DBLockProvider dbLock = new DBLockManager(session).getDBLock();
dbLock.waitForLock(DBLockProvider.Namespace.DATABASE);
try {
Assert.assertEquals(DBLockProvider.Namespace.DATABASE, dbLock.getCurrentLock());
@ -83,15 +74,15 @@ public class DBLockTest extends AbstractTestRealmKeycloakTest {
dbLock.releaseLock();
}
Assert.assertNull(dbLock.getCurrentLock());
return null;
});
}
@Test
@ModelTest
public void simpleNestedLockTest(KeycloakSession session) throws Exception {
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionLC) -> {
public void simpleNestedLockTest() throws Exception {
inComittedTransaction(1, (session , i) -> {
// first session lock DATABASE
DBLockProvider dbLock1 = new DBLockManager(sessionLC).getDBLock();
DBLockProvider dbLock1 = new DBLockManager(session).getDBLock();
dbLock1.waitForLock(DBLockProvider.Namespace.DATABASE);
try {
Assert.assertEquals(DBLockProvider.Namespace.DATABASE, dbLock1.getCurrentLock());
@ -111,41 +102,84 @@ public class DBLockTest extends AbstractTestRealmKeycloakTest {
dbLock1.releaseLock();
}
Assert.assertNull(dbLock1.getCurrentLock());
return null;
});
}
@Test
@ModelTest
public void testLockConcurrentlyGeneral(KeycloakSession session) throws Exception {
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionLC) -> {
testLockConcurrentlyInternal(sessionLC, DBLockProvider.Namespace.DATABASE);
public void testLockConcurrentlyGeneral() throws Exception {
inComittedTransaction(1, (session , i) -> {
testLockConcurrentlyInternal(session, DBLockProvider.Namespace.DATABASE);
return null;
});
}
@Test
@ModelTest
public void testLockConcurrentlyOffline(KeycloakSession session) throws Exception {
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionLC) -> {
testLockConcurrentlyInternal(sessionLC, DBLockProvider.Namespace.OFFLINE_SESSIONS);
public void testLockConcurrentlyOffline() throws Exception {
inComittedTransaction(1, (session , i) -> {
testLockConcurrentlyInternal(session, DBLockProvider.Namespace.OFFLINE_SESSIONS);
return null;
});
}
@Test
@ModelTest
public void testTwoLocksCurrently(KeycloakSession session) throws Exception {
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionLC) -> {
testTwoLocksCurrentlyInternal(sessionLC, DBLockProvider.Namespace.DATABASE, DBLockProvider.Namespace.OFFLINE_SESSIONS);
public void testTwoLocksCurrently() throws Exception {
inComittedTransaction(1, (session , i) -> {
testTwoLocksCurrentlyInternal(session, DBLockProvider.Namespace.DATABASE, DBLockProvider.Namespace.OFFLINE_SESSIONS);
return null;
});
}
@Test
@ModelTest
public void testTwoNestedLocksCurrently(KeycloakSession session) throws Exception {
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession sessionLC) -> {
testTwoNestedLocksCurrentlyInternal(sessionLC, DBLockProvider.Namespace.KEYCLOAK_BOOT, DBLockProvider.Namespace.DATABASE);
public void testTwoNestedLocksCurrently() throws Exception {
inComittedTransaction(1, (session , i) -> {
testTwoNestedLocksCurrentlyInternal(session, DBLockProvider.Namespace.KEYCLOAK_BOOT, DBLockProvider.Namespace.DATABASE);
return null;
});
}
private void testLockConcurrentlyInternal(KeycloakSession sessionLC, DBLockProvider.Namespace lock) {
long startupTime = System.currentTimeMillis();
final Semaphore semaphore = new Semaphore();
final KeycloakSessionFactory sessionFactory = sessionLC.getKeycloakSessionFactory();
List<Thread> threads = new LinkedList<>();
for (int i = 0; i < THREADS_COUNT; i++) {
Thread thread = new Thread(() -> {
for (int j = 0; j < ITERATIONS_PER_THREAD; j++) {
try {
KeycloakModelUtils.runJobInTransaction(sessionFactory, session1 ->
lock(session1, lock, semaphore));
} catch (RuntimeException e) {
semaphore.setException(e);
throw e;
}
}
});
threads.add(thread);
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
long took = (System.currentTimeMillis() - startupTime);
log.infof("DBLockTest executed in %d ms with total counter %d. THREADS_COUNT=%d, ITERATIONS_PER_THREAD=%d", took, semaphore.getTotal(), THREADS_COUNT, ITERATIONS_PER_THREAD);
Assert.assertEquals(THREADS_COUNT * ITERATIONS_PER_THREAD, semaphore.getTotal());
Assert.assertNull(semaphore.getException());
}
private void testTwoLocksCurrentlyInternal(KeycloakSession sessionLC, DBLockProvider.Namespace lock1, DBLockProvider.Namespace lock2) {
final Semaphore semaphore = new Semaphore();
final KeycloakSessionFactory sessionFactory = sessionLC.getKeycloakSessionFactory();
@ -218,48 +252,6 @@ public class DBLockTest extends AbstractTestRealmKeycloakTest {
Assert.assertNull(semaphore.getException());
}
private void testLockConcurrentlyInternal(KeycloakSession sessionLC, DBLockProvider.Namespace lock) {
long startupTime = System.currentTimeMillis();
final Semaphore semaphore = new Semaphore();
final KeycloakSessionFactory sessionFactory = sessionLC.getKeycloakSessionFactory();
List<Thread> threads = new LinkedList<>();
for (int i = 0; i < THREADS_COUNT; i++) {
Thread thread = new Thread(() -> {
for (int j = 0; j < ITERATIONS_PER_THREAD; j++) {
try {
KeycloakModelUtils.runJobInTransaction(sessionFactory, session1 ->
lock(session1, lock, semaphore));
} catch (RuntimeException e) {
semaphore.setException(e);
throw e;
}
}
});
threads.add(thread);
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
long took = (System.currentTimeMillis() - startupTime);
log.infof("DBLockTest executed in %d ms with total counter %d. THREADS_COUNT=%d, ITERATIONS_PER_THREAD=%d", took, semaphore.getTotal(), THREADS_COUNT, ITERATIONS_PER_THREAD);
Assert.assertEquals(THREADS_COUNT * ITERATIONS_PER_THREAD, semaphore.getTotal());
Assert.assertNull(semaphore.getException());
}
private void lock(KeycloakSession session, DBLockProvider.Namespace lock, Semaphore semaphore) {
DBLockProvider dbLock = new DBLockManager(session).getDBLock();
dbLock.waitForLock(lock);
@ -287,15 +279,11 @@ public class DBLockTest extends AbstractTestRealmKeycloakTest {
}
}
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
}
// Ensure just one thread is allowed to run at the same time
private class Semaphore {
private AtomicInteger counter = new AtomicInteger(0);
private AtomicInteger totalIncreases = new AtomicInteger(0);
private final AtomicInteger counter = new AtomicInteger(0);
private final AtomicInteger totalIncreases = new AtomicInteger(0);
private volatile Exception exception = null;
@ -332,8 +320,4 @@ public class DBLockTest extends AbstractTestRealmKeycloakTest {
return totalIncreases.get();
}
}
}

View file

@ -83,6 +83,7 @@ import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.keycloak.models.DeploymentStateProviderFactory;
import org.keycloak.models.dblock.DBLockSpi;
/**
* Base of testcases that operate on session level. The tests derived from this class
@ -198,6 +199,7 @@ public abstract class KeycloakModelTest {
.add(ClientSpi.class)
.add(ComponentFactorySpi.class)
.add(ClusterSpi.class)
.add(DBLockSpi.class)
.add(EventStoreSpi.class)
.add(ExecutorsSpi.class)
.add(GroupSpi.class)

View file

@ -30,7 +30,6 @@ import org.keycloak.models.session.UserSessionPersisterSpi;
import org.keycloak.migration.MigrationProviderFactory;
import org.keycloak.migration.MigrationSpi;
import org.keycloak.testsuite.model.KeycloakModelParameters;
import org.keycloak.models.dblock.DBLockSpi;
import org.keycloak.models.jpa.JpaClientProviderFactory;
import org.keycloak.models.jpa.JpaClientScopeProviderFactory;
import org.keycloak.models.jpa.JpaGroupProviderFactory;
@ -53,7 +52,6 @@ public class Jpa extends KeycloakModelParameters {
static final Set<Class<? extends Spi>> ALLOWED_SPIS = ImmutableSet.<Class<? extends Spi>>builder()
// jpa-specific
.add(DBLockSpi.class)
.add(JpaConnectionSpi.class)
.add(JpaUpdaterSpi.class)
.add(LiquibaseConnectionSpi.class)
@ -105,6 +103,7 @@ public class Jpa extends KeycloakModelParameters {
.spi("user").defaultProvider("jpa")
.spi("realm").defaultProvider("jpa")
.spi("deploymentState").defaultProvider("jpa")
.spi("dblock").defaultProvider("jpa")
;
}
}

View file

@ -20,6 +20,7 @@ import org.keycloak.authorization.store.StoreFactorySpi;
import org.keycloak.models.DeploymentStateSpi;
import org.keycloak.models.UserLoginFailureSpi;
import org.keycloak.models.UserSessionSpi;
import org.keycloak.models.dblock.NoLockingDBLockProviderFactory;
import org.keycloak.models.map.authSession.MapRootAuthenticationSessionProviderFactory;
import org.keycloak.models.map.authorization.MapAuthorizationStoreFactory;
import org.keycloak.models.map.loginFailure.MapUserLoginFailureProviderFactory;
@ -64,6 +65,7 @@ public class Map extends KeycloakModelParameters {
.add(MapUserProviderFactory.class)
.add(MapUserSessionProviderFactory.class)
.add(MapUserLoginFailureProviderFactory.class)
.add(NoLockingDBLockProviderFactory.class)
.add(MapStorageProviderFactory.class)
.build();
@ -85,6 +87,7 @@ public class Map extends KeycloakModelParameters {
.spi("user").defaultProvider(MapUserProviderFactory.PROVIDER_ID)
.spi(UserSessionSpi.NAME).defaultProvider(MapUserSessionProviderFactory.PROVIDER_ID)
.spi(UserLoginFailureSpi.NAME).defaultProvider(MapUserLoginFailureProviderFactory.PROVIDER_ID)
.spi("dblock").defaultProvider(NoLockingDBLockProviderFactory.PROVIDER_ID)
;
}
}

View file

@ -21,6 +21,10 @@
}
},
"dblock": {
"provider": "${keycloak.dblock.provider:jpa}"
},
"realm": {
"provider": "${keycloak.realm.provider:jpa}"
},