KEYCLOAK-10262 DBLockTest.testLockConcurrently fails with MariaDB Galera 10.1

This commit is contained in:
mposolda 2019-05-31 12:34:54 +02:00 committed by Marek Posolda
parent 7654793713
commit c124aec586
3 changed files with 60 additions and 25 deletions

View file

@ -76,12 +76,22 @@ public class Retry {
* @return Index of the first successful invocation, starting from 0.
*/
public static int executeWithBackoff(AdvancedRunnable runnable, int attemptsCount, int intervalBaseMillis) {
return executeWithBackoff(runnable, null, attemptsCount, intervalBaseMillis);
}
public static int executeWithBackoff(AdvancedRunnable runnable, ThrowableCallback throwableCallback, int attemptsCount, int intervalBaseMillis) {
int iteration = 0;
while (true) {
try {
runnable.run(iteration);
return iteration;
} catch (RuntimeException | AssertionError e) {
if (throwableCallback != null) {
throwableCallback.handleThrowable(iteration, e);
}
attemptsCount--;
iteration++;
if (attemptsCount > 0) {
@ -150,6 +160,17 @@ public class Retry {
}
/**
* Needed here because:
* - java.util.function.BiConsumer defined from Java 8
* - Adds some additional info (current iteration and called throwable
*/
public interface ThrowableCallback {
void handleThrowable(int iteration, Throwable t);
}
/**
* Needed here because:
* - java.util.function.Supplier defined from Java 8

View file

@ -19,6 +19,7 @@ package org.keycloak.connections.jpa.updater.liquibase.lock;
import liquibase.database.core.DerbyDatabase;
import liquibase.exception.DatabaseException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.executor.Executor;
import liquibase.executor.ExecutorService;
import liquibase.lockservice.StandardLockService;
@ -78,24 +79,24 @@ public class CustomLockService extends StandardLockService {
}
if (!isDatabaseChangeLogLockTableInitialized(createdTable)) {
try {
try {
if (!isDatabaseChangeLogLockTableInitialized(createdTable)) {
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.isTraceEnabled()) {
log.trace(de.getMessage(), de); // Log details at trace level
}
database.rollback();
throw new LockRetryException(de);
log.debug("Initialized record in the database lock table");
}
log.debug("Initialized record in the database lock table");
} catch (DatabaseException de) {
log.warn("Failed to insert first record to the lock table. Maybe other transaction inserted in the meantime. Retrying...");
if (log.isTraceEnabled()) {
log.trace(de.getMessage(), de); // Log details at trace level
}
database.rollback();
throw new LockRetryException(de);
}
@ -112,6 +113,20 @@ public class CustomLockService extends StandardLockService {
}
@Override
public boolean isDatabaseChangeLogLockTableInitialized(boolean tableJustCreated) throws DatabaseException {
try {
return super.isDatabaseChangeLogLockTableInitialized(tableJustCreated);
} catch (UnexpectedLiquibaseException ulie) {
// It can happen with MariaDB Galera 10.1 that UnexpectedLiquibaseException is rethrown due the DB lock. It is sufficient to just rollback transaction and retry in that case.
if (ulie.getCause() != null && ulie.getCause() instanceof DatabaseException) {
throw (DatabaseException) ulie.getCause();
} else {
throw ulie;
}
}
}
@Override
public void waitForLock() {
boolean locked = false;

View file

@ -21,6 +21,7 @@ import liquibase.Liquibase;
import liquibase.exception.DatabaseException;
import liquibase.exception.LiquibaseException;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Retry;
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.connections.jpa.JpaConnectionProviderFactory;
import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProvider;
@ -38,8 +39,8 @@ 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;
// 10 should be sufficient
private int DEFAULT_MAX_ATTEMPTS = 10;
private final LiquibaseDBLockProviderFactory factory;
@ -49,8 +50,6 @@ public class LiquibaseDBLockProvider implements DBLockProvider {
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;
@ -96,23 +95,23 @@ public class LiquibaseDBLockProvider implements DBLockProvider {
lazyInit();
while (maxAttempts > 0) {
try {
lockService.waitForLock();
factory.setHasLock(true);
this.maxAttempts = DEFAULT_MAX_ATTEMPTS;
return;
} catch (LockRetryException le) {
Retry.executeWithBackoff((int iteration) -> {
lockService.waitForLock();
factory.setHasLock(true);
}, (int iteration, Throwable e) -> {
if (e instanceof LockRetryException && iteration < (DEFAULT_MAX_ATTEMPTS - 1)) {
// Indicates we should try to acquire lock again in different transaction
safeRollbackConnection();
restart();
maxAttempts--;
} catch (RuntimeException re) {
} else {
safeRollbackConnection();
safeCloseConnection();
throw re;
}
}
}, DEFAULT_MAX_ATTEMPTS, 10);
});
}