/*
* 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.Liquibase;
import liquibase.exception.DatabaseException;
import liquibase.exception.LiquibaseException;
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;
import org.keycloak.models.utils.KeycloakModelUtils;
import java.sql.Connection;
import java.sql.SQLException;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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 CustomLockService lockService;
private Connection dbConnection;
private boolean initialized = false;
private int maxAttempts = DEFAULT_MAX_ATTEMPTS;
public LiquibaseDBLockProvider(LiquibaseDBLockProviderFactory factory, KeycloakSession session) {
this.factory = factory;
this.session = session;
}
private void lazyInit() {
if (!initialized) {
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.setDatabase(liquibase.getDatabase());
initialized = true;
} 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;
initialized = false;
lazyInit();
}
@Override
public void waitForLock() {
KeycloakModelUtils.suspendJtaTransaction(session.getKeycloakSessionFactory(), () -> {
lazyInit();
while (maxAttempts > 0) {
try {
lockService.waitForLock();
factory.setHasLock(true);
this.maxAttempts = DEFAULT_MAX_ATTEMPTS;
return;
} catch (LockRetryException le) {
// Indicates we should try to acquire lock again in different transaction
safeRollbackConnection();
restart();
maxAttempts--;
} catch (RuntimeException re) {
safeRollbackConnection();
safeCloseConnection();
throw re;
}
}
});
}
@Override
public void releaseLock() {
KeycloakModelUtils.suspendJtaTransaction(session.getKeycloakSessionFactory(), () -> {
lazyInit();
lockService.releaseLock();
lockService.reset();
factory.setHasLock(false);
});
}
@Override
public boolean hasLock() {
return factory.hasLock();
}
@Override
public boolean supportsForcedUnlock() {
// Implementation based on "SELECT FOR UPDATE" can't force unlock as it's locked by other transaction
return false;
}
@Override
public void destroyLockInfo() {
KeycloakModelUtils.suspendJtaTransaction(session.getKeycloakSessionFactory(), () -> {
lazyInit();
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() {
KeycloakModelUtils.suspendJtaTransaction(session.getKeycloakSessionFactory(), () -> {
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 to prevent in-mem databases from closing
if (dbConnection != null) {
try {
dbConnection.close();
} catch (SQLException e) {
logger.warn("Failed to close connection", e);
}
}
}
}