FINERACT-1909: Retry feature for commands - deadlock

This commit is contained in:
jmarta
2023-11-20 19:49:47 +01:00
committed by Adam Saghy
parent 3acb2b3c50
commit ee79c58e15
11 changed files with 200 additions and 50 deletions
@@ -107,7 +107,7 @@ public class JournalEntry extends AbstractAuditableWithUTCDateTimeCustom {
//
}
public JournalEntry(final Office office, final PaymentDetail paymentDetail, final GLAccount glAccount, final String currencyCode,
protected JournalEntry(final Office office, final PaymentDetail paymentDetail, final GLAccount glAccount, final String currencyCode,
final String transactionId, final boolean manualEntry, final LocalDate transactionDate, final Integer type,
final BigDecimal amount, final String description, final Integer entityType, final Long entityId, final String referenceNumber,
final Long loanTransactionId, final Long savingsTransactionId, final Long clientTransactionId, final Long shareTransactionId) {
@@ -145,6 +145,8 @@ public class BatchApiServiceImpl implements BatchApiService {
} catch (RuntimeException ex) {
status.setRollbackOnly();
return buildErrorResponses(ex, responseList);
} finally {
BatchRequestContextHolder.setEnclosingTransaction(Optional.empty());
}
});
} catch (TransactionException | NonTransientDataAccessException ex) {
@@ -329,7 +331,7 @@ public class BatchApiServiceImpl implements BatchApiService {
String body = null;
Set<Header> headers = new HashSet<>();
if (ex != null) {
ErrorInfo errorInfo = errorHandler.handle(errorHandler.getMappable(ex));
ErrorInfo errorInfo = errorHandler.handle(ErrorHandler.getMappable(ex));
statusCode = errorInfo.getStatusCode();
body = errorInfo.getMessage();
headers = Optional.ofNullable(errorInfo.getHeaders()).orElse(new HashSet<>());
@@ -358,7 +360,7 @@ public class BatchApiServiceImpl implements BatchApiService {
String body = null;
Set<Header> headers = new HashSet<>();
if (ex != null) {
ErrorInfo errorInfo = errorHandler.handle(errorHandler.getMappable(ex));
ErrorInfo errorInfo = errorHandler.handle(ErrorHandler.getMappable(ex));
statusCode = errorInfo.getStatusCode();
body = errorInfo.getMessage();
headers = Optional.ofNullable(errorInfo.getHeaders()).orElse(new HashSet<>());
@@ -62,9 +62,6 @@ public class CommandSourceService {
@NotNull
private CommandSource saveInitial(CommandWrapper wrapper, JsonCommand jsonCommand, AppUser maker, String idempotencyKey) {
CommandSource initialCommandSource = getInitialCommandSource(wrapper, jsonCommand, maker, idempotencyKey);
if (initialCommandSource.getCommandJson() == null) {
initialCommandSource.setCommandJson("{}");
}
return commandSourceRepository.saveAndFlush(initialCommandSource);
}
@@ -84,19 +81,22 @@ public class CommandSourceService {
}
public ErrorInfo generateErrorInfo(Throwable t) {
return errorHandler.handle(errorHandler.getMappable(t));
return errorHandler.handle(ErrorHandler.getMappable(t));
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public CommandSource getCommandSource(Long commandSourceId) {
return commandSourceRepository.findById(commandSourceId).orElseThrow(() -> new CommandNotFoundException(commandSourceId));
}
@Transactional(propagation = Propagation.REQUIRED)
public CommandSource findCommandSource(CommandWrapper wrapper, String idempotencyKey) {
return commandSourceRepository.findByActionNameAndEntityNameAndIdempotencyKey(wrapper.actionName(), wrapper.entityName(),
idempotencyKey);
}
private CommandSource getInitialCommandSource(CommandWrapper wrapper, JsonCommand jsonCommand, AppUser maker, String idempotencyKey) {
@Transactional(propagation = Propagation.REQUIRED)
public CommandSource getInitialCommandSource(CommandWrapper wrapper, JsonCommand jsonCommand, AppUser maker, String idempotencyKey) {
CommandSource commandSourceResult;
if (jsonCommand.commandId() != null) {
commandSourceResult = commandSourceRepository.findById(jsonCommand.commandId())
@@ -105,6 +105,9 @@ public class CommandSourceService {
} else {
commandSourceResult = CommandSource.fullEntryFrom(wrapper, jsonCommand, maker, idempotencyKey, UNDER_PROCESSING.getValue());
}
if (commandSourceResult.getCommandJson() == null) {
commandSourceResult.setCommandJson("{}");
}
return commandSourceResult;
}
}
@@ -106,9 +106,12 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
boolean sameTransaction = BatchRequestContextHolder.getEnclosingTransaction().isPresent();
if (commandSource == null) {
AppUser user = context.authenticatedUser(wrapper);
commandSource = sameTransaction ? commandSourceService.saveInitialSameTransaction(wrapper, command, user, idempotencyKey)
: commandSourceService.saveInitialNewTransaction(wrapper, command, user, idempotencyKey);
storeCommandIdInContext(commandSource); // Store command id as a request attribute
if (sameTransaction) {
commandSource = commandSourceService.getInitialCommandSource(wrapper, command, user, idempotencyKey);
} else {
commandSource = commandSourceService.saveInitialNewTransaction(wrapper, command, user, idempotencyKey);
storeCommandIdInContext(commandSource); // Store command id as a request attribute
}
}
setIdempotencyKeyStoreFlag(true);
@@ -116,19 +119,21 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
try {
result = findCommandHandler(wrapper).processCommand(command);
} catch (Throwable t) { // NOSONAR
ErrorInfo errorInfo = commandSourceService.generateErrorInfo(t);
RuntimeException mappable = ErrorHandler.getMappable(t);
ErrorInfo errorInfo = commandSourceService.generateErrorInfo(mappable);
commandSource.setResultStatusCode(errorInfo.getStatusCode());
commandSource.setResult(errorInfo.getMessage());
commandSource.setStatus(ERROR);
commandSource = sameTransaction ? commandSourceService.saveResultSameTransaction(commandSource)
: commandSourceService.saveResultNewTransaction(commandSource);
publishHookErrorEvent(wrapper, command, errorInfo);
throw t;
if (!sameTransaction) { // TODO: temporary solution
commandSource = commandSourceService.saveResultNewTransaction(commandSource);
}
publishHookErrorEvent(wrapper, command, errorInfo); // TODO must be performed in a new transaction
throw mappable;
}
commandSource.updateForAudit(result);
commandSource.setResult(toApiJsonSerializer.serializeResult(result));
commandSource.setResultStatusCode(SC_OK);
commandSource.setResult(toApiJsonSerializer.serializeResult(result));
commandSource.setStatus(PROCESSED);
boolean isRollback = !isApprovedByChecker && (result.isRollbackTransaction()
@@ -139,6 +144,9 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
}
commandSource = commandSourceService.saveResultSameTransaction(commandSource);
if (sameTransaction) {
storeCommandIdInContext(commandSource); // Store command id as a request attribute
}
if (isRollback) {
/*
@@ -147,14 +155,14 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
* when checker approves the transaction
*/
commandSource.setTransactionId(command.getTransactionId());
// TODO: this should be removed together with lines 133-135
// TODO: this should be removed together with lines 147-149
commandSource.setCommandJson(command.json()); // Set back CommandSource json data
throw new RollbackTransactionAsCommandIsNotApprovedByCheckerException(commandSource);
}
result.setRollbackTransaction(null);
publishHookEvent(wrapper.entityName(), wrapper.actionName(), command, result);
publishHookEvent(wrapper.entityName(), wrapper.actionName(), command, result); // TODO must be performed in a
// new transaction
return result;
}
@@ -211,7 +219,7 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
if (e instanceof RollbackTransactionAsCommandIsNotApprovedByCheckerException ex) {
return logCommand(ex.getCommandSourceResult());
}
throw errorHandler.getMappable(e);
throw ErrorHandler.getMappable(e);
}
private NewCommandSourceHandler findCommandHandler(final CommandWrapper wrapper) {
@@ -117,7 +117,8 @@ public class ApiGlobalErrorResponse {
if (identifier != null) {
details += " [" + identifier + ']';
}
String msg = "The server is currently unable to handle the request due to concurrent modification" + details + ", please try again";
String msg = "The server is currently unable to handle the request due to concurrent modification " + details
+ ", please try again";
return create(SC_LOCKED, "error.msg.platform.service." + type + ".conflict", msg, msg);
}
@@ -26,7 +26,9 @@ import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import java.sql.SQLException;
import java.text.ParseException;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@@ -35,6 +37,7 @@ import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.fortuna.ical4j.validate.ValidationException;
import org.apache.commons.collections4.SetUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.fineract.batch.domain.Header;
import org.apache.fineract.batch.exception.ErrorInfo;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
@@ -45,6 +48,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.core.NestedRuntimeException;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.dao.PessimisticLockingFailureException;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
/**
@@ -60,14 +65,55 @@ import org.springframework.stereotype.Component;
@AllArgsConstructor
public final class ErrorHandler {
private static final Gson JSON_HELPER = GoogleGsonSerializerHelper.createGsonBuilder(true).create();
private enum PessimisticLockingFailureCode {
ROLLBACK("40"), // Transaction rollback
DEADLOCK("60"), // Oracle: deadlock
HY00("HY", "Lock wait timeout exceeded"), // MySql deadlock HY00
;
private final String code;
private final String msg;
PessimisticLockingFailureCode(String code, String msg) {
this.code = code;
this.msg = msg;
}
PessimisticLockingFailureCode(String code) {
this(code, null);
}
private static Throwable match(Throwable t) {
Throwable rootCause = ExceptionUtils.getRootCause(t);
return rootCause instanceof SQLException sqle && Arrays.stream(values()).anyMatch(e -> e.matches(sqle)) ? rootCause : null;
}
private boolean matches(SQLException ex) {
return code.equals(getSqlClassCode(ex)) && (msg == null || ex.getMessage().contains(msg));
}
@Nullable
private static String getSqlClassCode(SQLException ex) {
String sqlState = ex.getSQLState();
if (sqlState == null) {
SQLException nestedEx = ex.getNextException();
if (nestedEx != null) {
sqlState = nestedEx.getSQLState();
}
}
return sqlState != null && sqlState.length() > 2 ? sqlState.substring(0, 2) : sqlState;
}
}
@Autowired
private final ApplicationContext ctx;
@Autowired
private final DefaultExceptionMapper defaultExceptionMapper;
private static final Gson JSON_HELPER = GoogleGsonSerializerHelper.createGsonBuilder(true).create();
@NotNull
public <T extends RuntimeException> ExceptionMapper<T> findMostSpecificExceptionHandler(T exception) {
Class<?> clazz = exception.getClass();
@@ -120,8 +166,13 @@ public final class ErrorHandler {
String msg = defaultMsg == null ? t.getMessage() : defaultMsg;
String codePfx = "error.msg" + (param == null ? "" : ("." + param));
Object[] args = defaultMsgArgs == null ? new Object[] { t } : defaultMsgArgs;
Throwable cause;
if ((cause = PessimisticLockingFailureCode.match(t)) != null) {
return new PessimisticLockingFailureException(msg, cause); // deadlock
}
if (t instanceof NestedRuntimeException nre) {
Throwable cause = nre.getMostSpecificCause();
cause = nre.getMostSpecificCause();
msg = defaultMsg == null ? cause.getMessage() : defaultMsg;
if (nre instanceof NonTransientDataAccessException) {
msgCode = msgCode == null ? codePfx + ".data.integrity.issue" : msgCode;
@@ -150,7 +201,7 @@ public final class ErrorHandler {
return new RuntimeException(msg, t);
}
private <T> Set<T> createSet(T[] array) {
private static <T> Set<T> createSet(T[] array) {
if (array == null) {
return Set.of();
} else {
@@ -44,11 +44,14 @@ public class ConcurrencyFailureExceptionMapper implements FineractExceptionMappe
@Override
public Response toResponse(final ConcurrencyFailureException exception) {
log.warn("Exception: {}, Message: {}", exception.getClass().getName(), exception.getMessage());
String type = "unknown";
String identifier = "unknown";
String type;
String identifier;
if (exception instanceof ObjectOptimisticLockingFailureException olex) {
type = olex.getPersistentClassName();
identifier = olex.getIdentifier() == null ? null : String.valueOf(olex.getIdentifier());
} else {
type = "lock";
identifier = null;
}
final ApiGlobalErrorResponse dataIntegrityError = ApiGlobalErrorResponse.locked(type, identifier);
return Response.status(SC_LOCKED).entity(dataIntegrityError).type(MediaType.APPLICATION_JSON).build();