Remove GlobalLockProviderSpi (#25206)
Closes #24103 Signed-off-by: Michal Hajas <mhajas@redhat.com>
This commit is contained in:
parent
31b7c9d2c3
commit
ec061e77ed
16 changed files with 35 additions and 663 deletions
|
@ -1,104 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2022 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.jboss.logging.Logger;
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
|
||||||
import org.keycloak.models.KeycloakSessionTaskWithResult;
|
|
||||||
import org.keycloak.models.locking.GlobalLockProvider;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import static org.keycloak.models.locking.GlobalLockProvider.Constants.KEYCLOAK_BOOT;
|
|
||||||
|
|
||||||
public class DBLockGlobalLockProvider implements GlobalLockProvider {
|
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getLogger(DBLockGlobalLockProvider.class);
|
|
||||||
public static final String DATABASE = "database";
|
|
||||||
private final KeycloakSession session;
|
|
||||||
private final DBLockProvider dbLockProvider;
|
|
||||||
public DBLockGlobalLockProvider(KeycloakSession session, DBLockProvider dbLockProvider) {
|
|
||||||
this.session = session;
|
|
||||||
this.dbLockProvider = dbLockProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DBLockProvider.Namespace stringToNamespace(String lockName) {
|
|
||||||
switch (lockName) {
|
|
||||||
case DATABASE:
|
|
||||||
return DBLockProvider.Namespace.DATABASE;
|
|
||||||
case KEYCLOAK_BOOT:
|
|
||||||
return DBLockProvider.Namespace.KEYCLOAK_BOOT;
|
|
||||||
default:
|
|
||||||
throw new RuntimeException("Lock with name " + lockName + " not supported by DBLockGlobalLockProvider.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Acquires a new non-reentrant global lock that is visible to all Keycloak nodes. If the lock was successfully
|
|
||||||
* acquired the method runs the {@code task} and return result to the caller.
|
|
||||||
* <p />
|
|
||||||
* See {@link GlobalLockProvider#withLock(String, Duration, KeycloakSessionTaskWithResult)} for more details.
|
|
||||||
* <p />
|
|
||||||
* This implementation does NOT meet all requirements from the JavaDoc of {@link GlobalLockProvider#withLock(String, Duration, KeycloakSessionTaskWithResult)}
|
|
||||||
* because {@link DBLockProvider} does not provide a way to lock and unlock in separate transactions.
|
|
||||||
* Also, the database schema update currently requires to be running in the same thread that initiated the update
|
|
||||||
* therefore the {@code task} is also running in the caller thread/transaction.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public <V> V withLock(String lockName, Duration timeToWaitForLock, KeycloakSessionTaskWithResult<V> task) {
|
|
||||||
Objects.requireNonNull(lockName, "lockName cannot be null");
|
|
||||||
|
|
||||||
if (timeToWaitForLock != null) {
|
|
||||||
LOG.debug("DBLockGlobalLockProvider does not support setting timeToWaitForLock per lock.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dbLockProvider.getCurrentLock() != null) {
|
|
||||||
throw new IllegalStateException("this lock is not reentrant, already locked for " + dbLockProvider.getCurrentLock());
|
|
||||||
}
|
|
||||||
|
|
||||||
dbLockProvider.waitForLock(stringToNamespace(lockName));
|
|
||||||
try {
|
|
||||||
return task.run(session);
|
|
||||||
} finally {
|
|
||||||
releaseLock(lockName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void releaseLock(String lockName) {
|
|
||||||
if (dbLockProvider.getCurrentLock() != stringToNamespace(lockName)) {
|
|
||||||
throw new RuntimeException("Requested releasing lock with name " + lockName + ", but lock is currently acquired for " + dbLockProvider.getCurrentLock() + ".");
|
|
||||||
}
|
|
||||||
|
|
||||||
dbLockProvider.releaseLock();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void forceReleaseAllLocks() {
|
|
||||||
if (dbLockProvider.supportsForcedUnlock()) {
|
|
||||||
dbLockProvider.releaseLock();
|
|
||||||
} else {
|
|
||||||
throw new IllegalStateException("Forced unlock requested, but provider " + dbLockProvider + " does not support it.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2022 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.models.KeycloakSession;
|
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
|
||||||
import org.keycloak.models.locking.GlobalLockProvider;
|
|
||||||
import org.keycloak.models.locking.GlobalLockProviderFactory;
|
|
||||||
|
|
||||||
public class DBLockGlobalLockProviderFactory implements GlobalLockProviderFactory {
|
|
||||||
|
|
||||||
public static final String PROVIDER_ID = "dblock";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public GlobalLockProvider create(KeycloakSession session) {
|
|
||||||
DBLockManager dbLockManager = new DBLockManager(session);
|
|
||||||
dbLockManager.checkForcedUnlock();
|
|
||||||
return new DBLockGlobalLockProvider(session, dbLockManager.getDBLock());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init(Config.Scope config) { }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void postInit(KeycloakSessionFactory factory) { }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() { }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getId() {
|
|
||||||
return PROVIDER_ID;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
#
|
|
||||||
# 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.DBLockGlobalLockProviderFactory
|
|
|
@ -49,8 +49,8 @@ import org.keycloak.migration.MigrationModelManager;
|
||||||
import org.keycloak.migration.ModelVersion;
|
import org.keycloak.migration.ModelVersion;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.models.dblock.DBLockGlobalLockProvider;
|
import org.keycloak.models.dblock.DBLockManager;
|
||||||
import org.keycloak.models.locking.GlobalLockProvider;
|
import org.keycloak.models.dblock.DBLockProvider;
|
||||||
import org.keycloak.provider.ProviderConfigProperty;
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
import org.keycloak.provider.ProviderConfigurationBuilder;
|
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||||
import org.keycloak.provider.ServerInfoAwareProviderFactory;
|
import org.keycloak.provider.ServerInfoAwareProviderFactory;
|
||||||
|
@ -285,19 +285,25 @@ public class LegacyJpaConnectionProviderFactory extends AbstractJpaConnectionPro
|
||||||
}
|
}
|
||||||
|
|
||||||
private void update(Connection connection, String schema, KeycloakSession session, JpaUpdaterProvider updater) {
|
private void update(Connection connection, String schema, KeycloakSession session, JpaUpdaterProvider updater) {
|
||||||
GlobalLockProvider globalLock = session.getProvider(GlobalLockProvider.class);
|
DBLockManager dbLockManager = new DBLockManager(session);
|
||||||
globalLock.withLock(DBLockGlobalLockProvider.DATABASE, innerSession -> {
|
DBLockProvider dbLock2 = dbLockManager.getDBLock();
|
||||||
|
dbLock2.waitForLock(DBLockProvider.Namespace.DATABASE);
|
||||||
|
try {
|
||||||
updater.update(connection, schema);
|
updater.update(connection, schema);
|
||||||
return null;
|
} finally {
|
||||||
});
|
dbLock2.releaseLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void export(Connection connection, String schema, File databaseUpdateFile, KeycloakSession session,
|
private void export(Connection connection, String schema, File databaseUpdateFile, KeycloakSession session,
|
||||||
JpaUpdaterProvider updater) {
|
JpaUpdaterProvider updater) {
|
||||||
GlobalLockProvider globalLock = session.getProvider(GlobalLockProvider.class);
|
DBLockManager dbLockManager = new DBLockManager(session);
|
||||||
globalLock.withLock(DBLockGlobalLockProvider.DATABASE, innerSession -> {
|
DBLockProvider dbLock2 = dbLockManager.getDBLock();
|
||||||
|
dbLock2.waitForLock(DBLockProvider.Namespace.DATABASE);
|
||||||
|
try {
|
||||||
updater.export(connection, schema, databaseUpdateFile);
|
updater.export(connection, schema, databaseUpdateFile);
|
||||||
return null;
|
} finally {
|
||||||
});
|
dbLock2.releaseLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,97 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2022 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.locking;
|
|
||||||
|
|
||||||
import org.keycloak.models.KeycloakSessionTaskWithResult;
|
|
||||||
import org.keycloak.provider.Provider;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
|
|
||||||
public interface GlobalLockProvider extends Provider {
|
|
||||||
|
|
||||||
class Constants {
|
|
||||||
public static final String KEYCLOAK_BOOT = "keycloak-boot";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Acquires a new non-reentrant global lock that is visible to all Keycloak nodes.
|
|
||||||
* Effectively the same as {@code withLock(lockName, null, task)}
|
|
||||||
*
|
|
||||||
* @param lockName Identifier used for acquiring lock. Can be any non-null string.
|
|
||||||
* @param task The task that will be executed under the acquired lock
|
|
||||||
* @param <V> Type of object returned by the {@code task}
|
|
||||||
* @return Value returned by the {@code task}
|
|
||||||
* @throws LockAcquiringTimeoutException When acquiring the global lock times out
|
|
||||||
* (see Javadoc of {@link #withLock(String, Duration, KeycloakSessionTaskWithResult)} for more details on how the time
|
|
||||||
* duration is determined)
|
|
||||||
* @throws NullPointerException When lockName is {@code null}.
|
|
||||||
*/
|
|
||||||
default <V> V withLock(String lockName, KeycloakSessionTaskWithResult<V> task) throws LockAcquiringTimeoutException {
|
|
||||||
return withLock(lockName, null, task);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Acquires a new non-reentrant global lock that is visible to all Keycloak nodes. If the lock was successfully
|
|
||||||
* acquired the method runs the {@code task} in a new transaction to ensure all data modified in {@code task}
|
|
||||||
* is committed to the stores before releasing the lock and returning to the caller.
|
|
||||||
* <p/>
|
|
||||||
* If there is another global lock with the same identifier ({@code lockName}) already acquired, this method waits
|
|
||||||
* until the lock is released, however, not more than {@code timeToWaitForLock} duration. If the lock is not
|
|
||||||
* acquired after {@code timeToWaitForLock} duration, the method throws {@link LockAcquiringTimeoutException}.
|
|
||||||
* <p/>
|
|
||||||
* When the execution of the {@code task} finishes, the acquired lock must be released regardless of the result.
|
|
||||||
* <p/>
|
|
||||||
* <b>A note to implementors of the interface:</b>
|
|
||||||
* <p/>
|
|
||||||
* To make sure acquiring/releasing the lock is visible to all Keycloak nodes it may be needed to run the code that
|
|
||||||
* acquires/releases the lock in a separate transactions. This means together the method can use 3 separate
|
|
||||||
* transactions, for example:
|
|
||||||
* <pre>
|
|
||||||
* try {
|
|
||||||
* KeycloakModelUtils.runJobInTransaction(factory,
|
|
||||||
* innerSession -> /* run code that acquires the lock *\/)
|
|
||||||
*
|
|
||||||
* KeycloakModelUtils.runJobInTransactionWithResult(factory, task)
|
|
||||||
* } finally {
|
|
||||||
* KeycloakModelUtils.runJobInTransaction(factory,
|
|
||||||
* innerSession -> /* run code that releases the lock *\/)
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* @param lockName Identifier used for acquiring lock. Can be any non-null string.
|
|
||||||
* @param task The task that will be executed under the acquired lock
|
|
||||||
* @param <V> Type of object returned by the {@code task}
|
|
||||||
* @param timeToWaitForLock Duration this method waits until it gives up acquiring the lock. If {@code null},
|
|
||||||
* each implementation should provide some default duration, for example, using
|
|
||||||
* a configuration option.
|
|
||||||
* @return Value returned by the {@code task}
|
|
||||||
*
|
|
||||||
* @throws LockAcquiringTimeoutException When the method waits for {@code timeToWaitForLock} duration and the lock is still
|
|
||||||
* not available to acquire.
|
|
||||||
* @throws NullPointerException When {@code lockName} is {@code null}.
|
|
||||||
*/
|
|
||||||
<V> V withLock(String lockName, Duration timeToWaitForLock, KeycloakSessionTaskWithResult<V> task) throws LockAcquiringTimeoutException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Releases all locks acquired by this GlobalLockProvider.
|
|
||||||
* <p />
|
|
||||||
* This method unlocks all existing locks acquired by this provider regardless of the thread
|
|
||||||
* or Keycloak instance that originally acquired them.
|
|
||||||
*/
|
|
||||||
void forceReleaseAllLocks();
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2022 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.locking;
|
|
||||||
|
|
||||||
import org.keycloak.provider.ProviderFactory;
|
|
||||||
|
|
||||||
public interface GlobalLockProviderFactory extends ProviderFactory<GlobalLockProvider> {
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2022 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.locking;
|
|
||||||
|
|
||||||
import org.keycloak.provider.Provider;
|
|
||||||
import org.keycloak.provider.ProviderFactory;
|
|
||||||
import org.keycloak.provider.Spi;
|
|
||||||
|
|
||||||
public class GlobalLockProviderSpi implements Spi {
|
|
||||||
|
|
||||||
public static final String GLOBAL_LOCK = "globalLock";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isInternal() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return GLOBAL_LOCK;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Class<? extends Provider> getProviderClass() {
|
|
||||||
return GlobalLockProvider.class;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
|
||||||
return GlobalLockProviderFactory.class;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2022 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.locking;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This exception is thrown when acquiring a lock times out.
|
|
||||||
*/
|
|
||||||
public final class LockAcquiringTimeoutException extends RuntimeException {
|
|
||||||
|
|
||||||
private final String lockName;
|
|
||||||
private final String keycloakInstanceIdentifier;
|
|
||||||
private final Instant timeWhenAcquired;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param lockName Identifier of a lock whose acquiring was unsuccessful.
|
|
||||||
* @param keycloakInstanceIdentifier Identifier of a Keycloak instance that is currently holding the lock.
|
|
||||||
* @param timeWhenAcquired Time instant when the lock held by {@code keycloakInstanceIdentifier} was acquired.
|
|
||||||
*/
|
|
||||||
public LockAcquiringTimeoutException(String lockName, String keycloakInstanceIdentifier, Instant timeWhenAcquired) {
|
|
||||||
super(String.format("Lock [%s] already acquired by keycloak instance [%s] at the time [%s]", lockName, keycloakInstanceIdentifier, timeWhenAcquired));
|
|
||||||
this.lockName = lockName;
|
|
||||||
this.keycloakInstanceIdentifier = keycloakInstanceIdentifier;
|
|
||||||
this.timeWhenAcquired = timeWhenAcquired;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param lockName Identifier of a lock whose acquiring was unsuccessful.
|
|
||||||
* @param keycloakInstanceIdentifier Identifier of a Keycloak instance that is currently holding the lock.
|
|
||||||
* @param timeWhenAcquired Time instant when the lock held by {@code keycloakInstanceIdentifier} was acquired.
|
|
||||||
* @param cause The cause.
|
|
||||||
*/
|
|
||||||
public LockAcquiringTimeoutException(String lockName, String keycloakInstanceIdentifier, Instant timeWhenAcquired, Throwable cause) {
|
|
||||||
super(String.format("Lock [%s] already acquired by keycloak instance [%s] at the time [%s]", lockName, keycloakInstanceIdentifier, timeWhenAcquired), cause);
|
|
||||||
this.lockName = lockName;
|
|
||||||
this.keycloakInstanceIdentifier = keycloakInstanceIdentifier;
|
|
||||||
this.timeWhenAcquired = timeWhenAcquired;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getLockName() {
|
|
||||||
return lockName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getKeycloakInstanceIdentifier() {
|
|
||||||
return keycloakInstanceIdentifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Instant getTimeWhenAcquired() {
|
|
||||||
return timeWhenAcquired;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -28,7 +28,6 @@ org.keycloak.models.SingleUseObjectSpi
|
||||||
org.keycloak.models.UserSessionSpi
|
org.keycloak.models.UserSessionSpi
|
||||||
org.keycloak.models.UserLoginFailureSpi
|
org.keycloak.models.UserLoginFailureSpi
|
||||||
org.keycloak.models.UserSpi
|
org.keycloak.models.UserSpi
|
||||||
org.keycloak.models.locking.GlobalLockProviderSpi
|
|
||||||
org.keycloak.migration.MigrationSpi
|
org.keycloak.migration.MigrationSpi
|
||||||
org.keycloak.events.EventListenerSpi
|
org.keycloak.events.EventListenerSpi
|
||||||
org.keycloak.events.EventStoreSpi
|
org.keycloak.events.EventStoreSpi
|
||||||
|
|
|
@ -205,6 +205,10 @@
|
||||||
<groupId>io.smallrye.common</groupId>
|
<groupId>io.smallrye.common</groupId>
|
||||||
<artifactId>smallrye-common-annotation</artifactId>
|
<artifactId>smallrye-common-annotation</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-model-legacy-private</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
|
|
@ -33,7 +33,8 @@ import org.keycloak.models.ModelDuplicateException;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserProvider;
|
import org.keycloak.models.UserProvider;
|
||||||
import org.keycloak.models.locking.GlobalLockProvider;
|
import org.keycloak.models.dblock.DBLockManager;
|
||||||
|
import org.keycloak.models.dblock.DBLockProvider;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.models.utils.PostMigrationEvent;
|
import org.keycloak.models.utils.PostMigrationEvent;
|
||||||
import org.keycloak.models.utils.RepresentationToModel;
|
import org.keycloak.models.utils.RepresentationToModel;
|
||||||
|
@ -131,24 +132,18 @@ public class KeycloakApplication extends Application {
|
||||||
|
|
||||||
ExportImportManager[] exportImportManager = new ExportImportManager[1];
|
ExportImportManager[] exportImportManager = new ExportImportManager[1];
|
||||||
|
|
||||||
// Release all locks acquired by currently used GlobalLockProvider if keycloak.globalLock.forceUnlock is equal
|
|
||||||
// to true. This can be used to recover from a state where there are some stale locks that were not correctly
|
|
||||||
// unlocked
|
|
||||||
if (Boolean.getBoolean("keycloak.globalLock.forceUnlock")) {
|
|
||||||
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
|
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
|
||||||
@Override
|
@Override
|
||||||
public void run(KeycloakSession session) {
|
public void run(KeycloakSession session) {
|
||||||
GlobalLockProvider locks = session.getProvider(GlobalLockProvider.class);
|
DBLockManager dbLockManager = new DBLockManager(session);
|
||||||
locks.forceReleaseAllLocks();
|
dbLockManager.checkForcedUnlock();
|
||||||
|
DBLockProvider dbLock = dbLockManager.getDBLock();
|
||||||
|
dbLock.waitForLock(DBLockProvider.Namespace.KEYCLOAK_BOOT);
|
||||||
|
try {
|
||||||
|
exportImportManager[0] = bootstrap();
|
||||||
|
} finally {
|
||||||
|
dbLock.releaseLock();
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
|
|
||||||
@Override
|
|
||||||
public void run(KeycloakSession session) {
|
|
||||||
GlobalLockProvider locks = session.getProvider(GlobalLockProvider.class);
|
|
||||||
exportImportManager[0] = locks.withLock(GlobalLockProvider.Constants.KEYCLOAK_BOOT, innerSession -> bootstrap());
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -40,8 +40,8 @@
|
||||||
"provider": "${keycloak.deploymentState.provider:jpa}"
|
"provider": "${keycloak.deploymentState.provider:jpa}"
|
||||||
},
|
},
|
||||||
|
|
||||||
"globalLock": {
|
"dblock": {
|
||||||
"provider": "${keycloak.globalLock.provider:dblock}"
|
"provider": "${keycloak.dblock.provider:jpa}"
|
||||||
},
|
},
|
||||||
|
|
||||||
"realm": {
|
"realm": {
|
||||||
|
|
|
@ -45,7 +45,6 @@ import org.keycloak.models.DeploymentStateSpi;
|
||||||
import org.keycloak.models.UserLoginFailureSpi;
|
import org.keycloak.models.UserLoginFailureSpi;
|
||||||
import org.keycloak.models.UserSessionSpi;
|
import org.keycloak.models.UserSessionSpi;
|
||||||
import org.keycloak.models.UserSpi;
|
import org.keycloak.models.UserSpi;
|
||||||
import org.keycloak.models.locking.GlobalLockProviderSpi;
|
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.models.utils.PostMigrationEvent;
|
import org.keycloak.models.utils.PostMigrationEvent;
|
||||||
import org.keycloak.provider.Provider;
|
import org.keycloak.provider.Provider;
|
||||||
|
@ -233,7 +232,6 @@ public abstract class KeycloakModelTest {
|
||||||
.add(ClientSpi.class)
|
.add(ClientSpi.class)
|
||||||
.add(ComponentFactorySpi.class)
|
.add(ComponentFactorySpi.class)
|
||||||
.add(ClusterSpi.class)
|
.add(ClusterSpi.class)
|
||||||
.add(GlobalLockProviderSpi.class)
|
|
||||||
.add(EventStoreSpi.class)
|
.add(EventStoreSpi.class)
|
||||||
.add(ExecutorsSpi.class)
|
.add(ExecutorsSpi.class)
|
||||||
.add(GroupSpi.class)
|
.add(GroupSpi.class)
|
||||||
|
|
|
@ -1,218 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2022 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.globalLock;
|
|
||||||
|
|
||||||
import org.hamcrest.Matchers;
|
|
||||||
import org.jboss.logging.Logger;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.keycloak.models.dblock.DBLockGlobalLockProviderFactory;
|
|
||||||
import org.keycloak.models.locking.GlobalLockProvider;
|
|
||||||
import org.keycloak.models.locking.LockAcquiringTimeoutException;
|
|
||||||
import org.keycloak.testsuite.model.KeycloakModelTest;
|
|
||||||
import org.keycloak.testsuite.model.RequireProvider;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Random;
|
|
||||||
import java.util.concurrent.CountDownLatch;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.IntStream;
|
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.containsString;
|
|
||||||
import static org.hamcrest.CoreMatchers.equalTo;
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
|
||||||
import static org.hamcrest.Matchers.hasSize;
|
|
||||||
|
|
||||||
@RequireProvider(value = GlobalLockProvider.class,
|
|
||||||
exclude = { DBLockGlobalLockProviderFactory.PROVIDER_ID }
|
|
||||||
)
|
|
||||||
public class GlobalLocksTest extends KeycloakModelTest {
|
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getLogger(GlobalLocksTest.class);
|
|
||||||
@Override
|
|
||||||
protected boolean isUseSameKeycloakSessionFactoryForAllThreads() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void concurrentLockingTest() {
|
|
||||||
final String LOCK_NAME = "simpleLockTestLockName";
|
|
||||||
|
|
||||||
AtomicInteger counter = new AtomicInteger();
|
|
||||||
int numIterations = 50;
|
|
||||||
Random rand = new Random();
|
|
||||||
List<Integer> resultingList = new LinkedList<>();
|
|
||||||
|
|
||||||
IntStream.range(0, numIterations).parallel().forEach(index -> inComittedTransaction(s -> {
|
|
||||||
GlobalLockProvider lockProvider = s.getProvider(GlobalLockProvider.class);
|
|
||||||
LOG.infof("Iteration %d entered session", index);
|
|
||||||
|
|
||||||
lockProvider.withLock(LOCK_NAME, Duration.ofSeconds(60), innerSession -> {
|
|
||||||
LOG.infof("Iteration %d entered locked block", index);
|
|
||||||
|
|
||||||
// Locked block
|
|
||||||
int c = counter.getAndIncrement();
|
|
||||||
|
|
||||||
try {
|
|
||||||
Thread.sleep(rand.nextInt(100));
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
resultingList.add(c);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
assertThat(resultingList, hasSize(numIterations));
|
|
||||||
assertThat(resultingList, equalTo(IntStream.range(0, 50).boxed().collect(Collectors.toList())));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void lockTimeoutExceptionTest() {
|
|
||||||
final String LOCK_NAME = "lockTimeoutExceptionTestLock";
|
|
||||||
AtomicInteger counter = new AtomicInteger();
|
|
||||||
CountDownLatch waitForTheOtherThreadToFail = new CountDownLatch(1);
|
|
||||||
|
|
||||||
IntStream.range(0, 2).parallel().forEach(index -> inComittedTransaction(s -> {
|
|
||||||
GlobalLockProvider lockProvider = s.getProvider(GlobalLockProvider.class);
|
|
||||||
|
|
||||||
try {
|
|
||||||
lockProvider.withLock(LOCK_NAME, Duration.ofSeconds(2), innerSession -> {
|
|
||||||
int c = counter.incrementAndGet();
|
|
||||||
if (c == 1) {
|
|
||||||
try {
|
|
||||||
waitForTheOtherThreadToFail.await();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOG.infof("Lock acquired by thread %s with counter: %d", Thread.currentThread().getName(), c);
|
|
||||||
throw new RuntimeException("Lock acquired by more than one thread.");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
} catch (LockAcquiringTimeoutException e) {
|
|
||||||
int c = counter.incrementAndGet();
|
|
||||||
LOG.infof("Exception when acquiring lock by thread %s with counter: %d", Thread.currentThread().getName(), c);
|
|
||||||
if (c != 2) {
|
|
||||||
throw new RuntimeException("Acquiring lock failed by different thread than second.");
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThat(e.getMessage(), containsString("Lock [" + LOCK_NAME + "] already acquired by keycloak instance"));
|
|
||||||
waitForTheOtherThreadToFail.countDown();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testReleaseAllLocksMethod() throws InterruptedException {
|
|
||||||
final int numberOfThreads = 4;
|
|
||||||
ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);
|
|
||||||
|
|
||||||
CountDownLatch locksAcquired = new CountDownLatch(numberOfThreads);
|
|
||||||
CountDownLatch testFinished = new CountDownLatch(1);
|
|
||||||
|
|
||||||
LOG.info("Initial locks acquiring phase.");
|
|
||||||
try {
|
|
||||||
// Acquire locks and let the threads wait until the end of this test method
|
|
||||||
for (int index = 0; index < numberOfThreads; index++) {
|
|
||||||
final int i = index;
|
|
||||||
executor.submit(() ->
|
|
||||||
inComittedTransaction(s -> {
|
|
||||||
GlobalLockProvider lockProvider = s.getProvider(GlobalLockProvider.class);
|
|
||||||
LOG.infof("Acquiring LOCK_%d", i);
|
|
||||||
lockProvider.withLock("LOCK_" + i, session -> {
|
|
||||||
LOG.infof("Lock LOCK_%d acquired.", i);
|
|
||||||
locksAcquired.countDown();
|
|
||||||
try {
|
|
||||||
testFinished.await();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
LOG.infof("Initial acquiring tx finished for lock LOCK_%d", i);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!locksAcquired.await(5, TimeUnit.MINUTES)) {
|
|
||||||
throw new RuntimeException("Acquiring locks phase took too long.");
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG.info("Expecting timeouts for each lock.");
|
|
||||||
// Test no lock can be acquired because all are still hold by the executor above
|
|
||||||
AtomicInteger counter = new AtomicInteger();
|
|
||||||
for (int index = 0; index < numberOfThreads; index++) {
|
|
||||||
final int i = index;
|
|
||||||
inComittedTransaction(s -> {
|
|
||||||
GlobalLockProvider lockProvider = s.getProvider(GlobalLockProvider.class);
|
|
||||||
try {
|
|
||||||
LOG.infof("Attempt to acquire LOCK_%d.", i);
|
|
||||||
lockProvider.withLock("LOCK_" + i, Duration.ofSeconds(1), is -> {
|
|
||||||
throw new RuntimeException("Acquiring lock should not succeed as it was acquired in the first transaction");
|
|
||||||
});
|
|
||||||
} catch (LockAcquiringTimeoutException e) {
|
|
||||||
LOG.infof("Timeout was successfully received for LOCK_%d", i);
|
|
||||||
counter.incrementAndGet();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThat(counter.get(), Matchers.equalTo(numberOfThreads));
|
|
||||||
|
|
||||||
// Unlock all locks forcefully
|
|
||||||
inComittedTransaction(s -> {
|
|
||||||
GlobalLockProvider lockProvider = s.getProvider(GlobalLockProvider.class);
|
|
||||||
LOG.infof("Releasing all locks", Thread.currentThread().getName());
|
|
||||||
lockProvider.forceReleaseAllLocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test all locks can be acquired again
|
|
||||||
counter.set(0);
|
|
||||||
for (int index = 0; index < numberOfThreads; index++) {
|
|
||||||
final int i = index;
|
|
||||||
inComittedTransaction(s -> {
|
|
||||||
GlobalLockProvider lockProvider = s.getProvider(GlobalLockProvider.class);
|
|
||||||
try {
|
|
||||||
lockProvider.withLock("LOCK_" + i, Duration.ofSeconds(1), is -> {
|
|
||||||
LOG.infof("Lock LOCK_%d acquired again.", i);
|
|
||||||
counter.incrementAndGet();
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
} catch (LockAcquiringTimeoutException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThat(counter.get(), Matchers.equalTo(numberOfThreads));
|
|
||||||
} finally {
|
|
||||||
testFinished.countDown();
|
|
||||||
executor.shutdown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -25,10 +25,8 @@ import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionPr
|
||||||
import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionSpi;
|
import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionSpi;
|
||||||
import org.keycloak.connections.jpa.updater.liquibase.lock.LiquibaseDBLockProviderFactory;
|
import org.keycloak.connections.jpa.updater.liquibase.lock.LiquibaseDBLockProviderFactory;
|
||||||
import org.keycloak.events.jpa.JpaEventStoreProviderFactory;
|
import org.keycloak.events.jpa.JpaEventStoreProviderFactory;
|
||||||
import org.keycloak.models.dblock.DBLockGlobalLockProviderFactory;
|
|
||||||
import org.keycloak.models.dblock.DBLockSpi;
|
import org.keycloak.models.dblock.DBLockSpi;
|
||||||
import org.keycloak.models.jpa.session.JpaUserSessionPersisterProviderFactory;
|
import org.keycloak.models.jpa.session.JpaUserSessionPersisterProviderFactory;
|
||||||
import org.keycloak.models.locking.GlobalLockProviderSpi;
|
|
||||||
import org.keycloak.models.session.UserSessionPersisterSpi;
|
import org.keycloak.models.session.UserSessionPersisterSpi;
|
||||||
import org.keycloak.migration.MigrationProviderFactory;
|
import org.keycloak.migration.MigrationProviderFactory;
|
||||||
import org.keycloak.migration.MigrationSpi;
|
import org.keycloak.migration.MigrationSpi;
|
||||||
|
@ -88,7 +86,6 @@ public class LegacyJpa extends KeycloakModelParameters {
|
||||||
.add(JpaUserProviderFactory.class)
|
.add(JpaUserProviderFactory.class)
|
||||||
.add(LiquibaseConnectionProviderFactory.class)
|
.add(LiquibaseConnectionProviderFactory.class)
|
||||||
.add(LiquibaseDBLockProviderFactory.class)
|
.add(LiquibaseDBLockProviderFactory.class)
|
||||||
.add(DBLockGlobalLockProviderFactory.class)
|
|
||||||
.add(JpaUserSessionPersisterProviderFactory.class)
|
.add(JpaUserSessionPersisterProviderFactory.class)
|
||||||
|
|
||||||
//required for migrateModel
|
//required for migrateModel
|
||||||
|
@ -116,7 +113,6 @@ public class LegacyJpa extends KeycloakModelParameters {
|
||||||
.spi("realm").defaultProvider("jpa")
|
.spi("realm").defaultProvider("jpa")
|
||||||
.spi("deploymentState").defaultProvider("jpa")
|
.spi("deploymentState").defaultProvider("jpa")
|
||||||
.spi("dblock").defaultProvider("jpa")
|
.spi("dblock").defaultProvider("jpa")
|
||||||
.spi(GlobalLockProviderSpi.GLOBAL_LOCK).defaultProvider(DBLockGlobalLockProviderFactory.PROVIDER_ID)
|
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,8 @@
|
||||||
"provider": "${keycloak.deploymentState.provider:jpa}"
|
"provider": "${keycloak.deploymentState.provider:jpa}"
|
||||||
},
|
},
|
||||||
|
|
||||||
"globalLock": {
|
"dblock": {
|
||||||
"provider": "${keycloak.globalLock.provider:dblock}"
|
"provider": "${keycloak.dblock.provider:jpa}"
|
||||||
},
|
},
|
||||||
|
|
||||||
"realm": {
|
"realm": {
|
||||||
|
|
Loading…
Reference in a new issue