FINERACT-1744: Tiny refactorings as a preparation for idempotency implementation

This commit is contained in:
Arnold Galovics
2022-09-13 13:40:24 +02:00
committed by Arnold Gálovics
parent 622e32e737
commit b5b189d610
8 changed files with 70 additions and 94 deletions
@@ -18,6 +18,11 @@
*/ */
package org.apache.fineract.commands.domain; package org.apache.fineract.commands.domain;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum CommandProcessingResultType { public enum CommandProcessingResultType {
INVALID(0, "commandProcessingResultType.invalid"), // INVALID(0, "commandProcessingResultType.invalid"), //
@@ -27,33 +32,4 @@ public enum CommandProcessingResultType {
private final Integer value; private final Integer value;
private final String code; private final String code;
CommandProcessingResultType(final Integer value, final String code) {
this.value = value;
this.code = code;
}
public Integer getValue() {
return this.value;
}
public String getCode() {
return this.code;
}
public static CommandProcessingResultType fromInt(final Integer typeValue) {
CommandProcessingResultType type = CommandProcessingResultType.INVALID;
switch (typeValue) {
case 1:
type = PROCESSED;
break;
case 2:
type = AWAITING_APPROVAL;
break;
case 3:
type = REJECTED;
break;
}
return type;
}
} }
@@ -28,6 +28,7 @@ import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.exception.UnsupportedCommandException; import org.apache.fineract.commands.exception.UnsupportedCommandException;
import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -47,10 +48,30 @@ import org.springframework.stereotype.Component;
@Component @Component
@NoArgsConstructor @NoArgsConstructor
@Slf4j @Slf4j
public class CommandHandlerProvider implements ApplicationContextAware { public class CommandHandlerProvider implements ApplicationContextAware, InitializingBean {
private final HashMap<String, String> registeredHandlers = new HashMap<>();
private ApplicationContext applicationContext; private ApplicationContext applicationContext;
private HashMap<String, String> registeredHandlers;
@Override
public void afterPropertiesSet() throws Exception {
initializeHandlerRegistry();
}
private void initializeHandlerRegistry() {
final String[] commandHandlerBeans = applicationContext.getBeanNamesForAnnotation(CommandType.class);
if (ArrayUtils.isNotEmpty(commandHandlerBeans)) {
for (final String commandHandlerName : commandHandlerBeans) {
log.debug("Register command handler '{}' ...", commandHandlerName);
final CommandType commandType = applicationContext.findAnnotationOnBean(commandHandlerName, CommandType.class);
try {
registeredHandlers.put(commandType.entity() + "|" + commandType.action(), commandHandlerName);
} catch (final Throwable th) {
log.error("Unable to register command handler '{}'!", commandHandlerName, th);
}
}
}
}
/** /**
* Returns a handler for the given entity and action.<br> * Returns a handler for the given entity and action.<br>
@@ -68,34 +89,14 @@ public class CommandHandlerProvider implements ApplicationContextAware {
Preconditions.checkArgument(StringUtils.isNoneEmpty(action), "An action must be given!"); Preconditions.checkArgument(StringUtils.isNoneEmpty(action), "An action must be given!");
final String key = entity + "|" + action; final String key = entity + "|" + action;
if (!this.registeredHandlers.containsKey(key)) { if (!registeredHandlers.containsKey(key)) {
throw new UnsupportedCommandException(key); throw new UnsupportedCommandException(key);
} }
return (NewCommandSourceHandler) this.applicationContext.getBean(this.registeredHandlers.get(key)); return (NewCommandSourceHandler) applicationContext.getBean(registeredHandlers.get(key));
}
private void initializeHandlerRegistry() {
if (this.registeredHandlers == null) {
this.registeredHandlers = new HashMap<>();
final String[] commandHandlerBeans = this.applicationContext.getBeanNamesForAnnotation(CommandType.class);
if (ArrayUtils.isNotEmpty(commandHandlerBeans)) {
for (final String commandHandlerName : commandHandlerBeans) {
log.debug("Register command handler '{}' ...", commandHandlerName);
final CommandType commandType = this.applicationContext.findAnnotationOnBean(commandHandlerName, CommandType.class);
try {
this.registeredHandlers.put(commandType.entity() + "|" + commandType.action(), commandHandlerName);
} catch (final Throwable th) {
log.error("Unable to register command handler '{}'!", commandHandlerName, th);
}
}
}
}
} }
@Override @Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext; this.applicationContext = applicationContext;
this.initializeHandlerRegistry();
} }
} }
@@ -26,7 +26,7 @@ import org.apache.fineract.useradministration.domain.AppUser;
public interface CommandProcessingService { public interface CommandProcessingService {
CommandProcessingResult processAndLogCommand(CommandWrapper wrapper, JsonCommand command, boolean isApprovedByChecker); CommandProcessingResult executeCommand(CommandWrapper wrapper, JsonCommand command, boolean isApprovedByChecker);
CommandProcessingResult logCommand(CommandSource commandSourceResult); CommandProcessingResult logCommand(CommandSource commandSourceResult);
@@ -77,7 +77,7 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
final String json = wrapper.getJson(); final String json = wrapper.getJson();
CommandProcessingResult result = null; CommandProcessingResult result = null;
JsonCommand command = null; JsonCommand command;
int numberOfRetries = 0; int numberOfRetries = 0;
int maxNumberOfRetries = ThreadLocalContextUtil.getTenant().getConnection().getMaxRetriesOnDeadlock(); int maxNumberOfRetries = ThreadLocalContextUtil.getTenant().getConnection().getMaxRetriesOnDeadlock();
int maxIntervalBetweenRetries = ThreadLocalContextUtil.getTenant().getConnection().getMaxIntervalBetweenRetries(); int maxIntervalBetweenRetries = ThreadLocalContextUtil.getTenant().getConnection().getMaxIntervalBetweenRetries();
@@ -88,7 +88,7 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
wrapper.getOrganisationCreditBureauId()); wrapper.getOrganisationCreditBureauId());
while (numberOfRetries <= maxNumberOfRetries) { while (numberOfRetries <= maxNumberOfRetries) {
try { try {
result = this.processAndLogCommandService.processAndLogCommand(wrapper, command, isApprovedByChecker); result = this.processAndLogCommandService.executeCommand(wrapper, command, isApprovedByChecker);
numberOfRetries = maxNumberOfRetries + 1; numberOfRetries = maxNumberOfRetries + 1;
} catch (CannotAcquireLockException | ObjectOptimisticLockingFailureException exception) { } catch (CannotAcquireLockException | ObjectOptimisticLockingFailureException exception) {
log.info("The following command {} has been retried {} time(s)", command.json(), numberOfRetries); log.info("The following command {} has been retried {} time(s)", command.json(), numberOfRetries);
@@ -140,7 +140,7 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
commandSourceInput.getOrganisationCreditBureauId()); commandSourceInput.getOrganisationCreditBureauId());
final boolean makerCheckerApproval = true; final boolean makerCheckerApproval = true;
return this.processAndLogCommandService.processAndLogCommand(wrapper, command, makerCheckerApproval); return this.processAndLogCommandService.executeCommand(wrapper, command, makerCheckerApproval);
} }
@Transactional @Transactional
@@ -65,10 +65,10 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
@Transactional @Transactional
@Override @Override
public CommandProcessingResult processAndLogCommand(final CommandWrapper wrapper, final JsonCommand command, public CommandProcessingResult executeCommand(final CommandWrapper wrapper, final JsonCommand command,
final boolean isApprovedByChecker) { final boolean isApprovedByChecker) {
final boolean rollbackTransaction = this.configurationDomainService.isMakerCheckerEnabledForTask(wrapper.taskPermissionName()); final boolean rollbackTransaction = configurationDomainService.isMakerCheckerEnabledForTask(wrapper.taskPermissionName());
final NewCommandSourceHandler handler = findCommandHandler(wrapper); final NewCommandSourceHandler handler = findCommandHandler(wrapper);
@@ -76,16 +76,15 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
try { try {
result = handler.processCommand(command); result = handler.processCommand(command);
} catch (Throwable t) { } catch (Throwable t) {
// publish error event publishHookErrorEvent(wrapper, command, t);
publishErrorEvent(wrapper, command, t);
throw t; throw t;
} }
final AppUser maker = this.context.authenticatedUser(wrapper); final AppUser maker = context.authenticatedUser(wrapper);
CommandSource commandSourceResult; CommandSource commandSourceResult;
if (command.commandId() != null) { if (command.commandId() != null) {
commandSourceResult = this.commandSourceRepository.findById(command.commandId()) commandSourceResult = commandSourceRepository.findById(command.commandId())
.orElseThrow(() -> new CommandNotFoundException(command.commandId())); .orElseThrow(() -> new CommandNotFoundException(command.commandId()));
commandSourceResult.markAsChecked(maker); commandSourceResult.markAsChecked(maker);
} else { } else {
@@ -98,7 +97,7 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
String changesOnlyJson; String changesOnlyJson;
boolean rollBack = (rollbackTransaction || result.isRollbackTransaction()) && !isApprovedByChecker; boolean rollBack = (rollbackTransaction || result.isRollbackTransaction()) && !isApprovedByChecker;
if (result.hasChanges() && !rollBack) { if (result.hasChanges() && !rollBack) {
changesOnlyJson = this.toApiJsonSerializer.serializeResult(result.getChanges()); changesOnlyJson = toApiJsonSerializer.serializeResult(result.getChanges());
commandSourceResult.updateJsonTo(changesOnlyJson); commandSourceResult.updateJsonTo(changesOnlyJson);
} }
@@ -107,7 +106,7 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
} }
if (commandSourceResult.hasJson()) { if (commandSourceResult.hasJson()) {
this.commandSourceRepository.save(commandSourceResult); commandSourceRepository.save(commandSourceResult);
} }
if ((rollbackTransaction || result.isRollbackTransaction()) && !isApprovedByChecker) { if ((rollbackTransaction || result.isRollbackTransaction()) && !isApprovedByChecker) {
@@ -125,7 +124,7 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
} }
result.setRollbackTransaction(null); result.setRollbackTransaction(null);
publishEvent(wrapper.entityName(), wrapper.actionName(), command, result); publishHookEvent(wrapper.entityName(), wrapper.actionName(), command, result);
return result; return result;
} }
@@ -135,7 +134,7 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
public CommandProcessingResult logCommand(CommandSource commandSourceResult) { public CommandProcessingResult logCommand(CommandSource commandSourceResult) {
commandSourceResult.markAsAwaitingApproval(); commandSourceResult.markAsAwaitingApproval();
commandSourceResult = this.commandSourceRepository.saveAndFlush(commandSourceResult); commandSourceResult = commandSourceRepository.saveAndFlush(commandSourceResult);
return new CommandProcessingResultBuilder().withCommandId(commandSourceResult.getId()) return new CommandProcessingResultBuilder().withCommandId(commandSourceResult.getId())
.withEntityId(commandSourceResult.getResourceId()).build(); .withEntityId(commandSourceResult.getResourceId()).build();
@@ -146,54 +145,54 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
if (wrapper.isDatatableResource()) { if (wrapper.isDatatableResource()) {
if (wrapper.isCreateDatatable()) { if (wrapper.isCreateDatatable()) {
handler = this.applicationContext.getBean("createDatatableCommandHandler", NewCommandSourceHandler.class); handler = applicationContext.getBean("createDatatableCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isDeleteDatatable()) { } else if (wrapper.isDeleteDatatable()) {
handler = this.applicationContext.getBean("deleteDatatableCommandHandler", NewCommandSourceHandler.class); handler = applicationContext.getBean("deleteDatatableCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isUpdateDatatable()) { } else if (wrapper.isUpdateDatatable()) {
handler = this.applicationContext.getBean("updateDatatableCommandHandler", NewCommandSourceHandler.class); handler = applicationContext.getBean("updateDatatableCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isCreate()) { } else if (wrapper.isCreate()) {
handler = this.applicationContext.getBean("createDatatableEntryCommandHandler", NewCommandSourceHandler.class); handler = applicationContext.getBean("createDatatableEntryCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isUpdateMultiple()) { } else if (wrapper.isUpdateMultiple()) {
handler = this.applicationContext.getBean("updateOneToManyDatatableEntryCommandHandler", NewCommandSourceHandler.class); handler = applicationContext.getBean("updateOneToManyDatatableEntryCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isUpdateOneToOne()) { } else if (wrapper.isUpdateOneToOne()) {
handler = this.applicationContext.getBean("updateOneToOneDatatableEntryCommandHandler", NewCommandSourceHandler.class); handler = applicationContext.getBean("updateOneToOneDatatableEntryCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isDeleteMultiple()) { } else if (wrapper.isDeleteMultiple()) {
handler = this.applicationContext.getBean("deleteOneToManyDatatableEntryCommandHandler", NewCommandSourceHandler.class); handler = applicationContext.getBean("deleteOneToManyDatatableEntryCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isDeleteOneToOne()) { } else if (wrapper.isDeleteOneToOne()) {
handler = this.applicationContext.getBean("deleteOneToOneDatatableEntryCommandHandler", NewCommandSourceHandler.class); handler = applicationContext.getBean("deleteOneToOneDatatableEntryCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isRegisterDatatable()) { } else if (wrapper.isRegisterDatatable()) {
handler = this.applicationContext.getBean("registerDatatableCommandHandler", NewCommandSourceHandler.class); handler = applicationContext.getBean("registerDatatableCommandHandler", NewCommandSourceHandler.class);
} else { } else {
throw new UnsupportedCommandException(wrapper.commandName()); throw new UnsupportedCommandException(wrapper.commandName());
} }
} else if (wrapper.isNoteResource()) { } else if (wrapper.isNoteResource()) {
if (wrapper.isCreate()) { if (wrapper.isCreate()) {
handler = this.applicationContext.getBean("createNoteCommandHandler", NewCommandSourceHandler.class); handler = applicationContext.getBean("createNoteCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isUpdate()) { } else if (wrapper.isUpdate()) {
handler = this.applicationContext.getBean("updateNoteCommandHandler", NewCommandSourceHandler.class); handler = applicationContext.getBean("updateNoteCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isDelete()) { } else if (wrapper.isDelete()) {
handler = this.applicationContext.getBean("deleteNoteCommandHandler", NewCommandSourceHandler.class); handler = applicationContext.getBean("deleteNoteCommandHandler", NewCommandSourceHandler.class);
} else { } else {
throw new UnsupportedCommandException(wrapper.commandName()); throw new UnsupportedCommandException(wrapper.commandName());
} }
} else if (wrapper.isSurveyResource()) { } else if (wrapper.isSurveyResource()) {
if (wrapper.isRegisterSurvey()) { if (wrapper.isRegisterSurvey()) {
handler = this.applicationContext.getBean("registerSurveyCommandHandler", NewCommandSourceHandler.class); handler = applicationContext.getBean("registerSurveyCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.isFullFilSurvey()) { } else if (wrapper.isFullFilSurvey()) {
handler = this.applicationContext.getBean("fullFilSurveyCommandHandler", NewCommandSourceHandler.class); handler = applicationContext.getBean("fullFilSurveyCommandHandler", NewCommandSourceHandler.class);
} else { } else {
throw new UnsupportedCommandException(wrapper.commandName()); throw new UnsupportedCommandException(wrapper.commandName());
} }
} else if (wrapper.isLoanDisburseDetailResource()) { } else if (wrapper.isLoanDisburseDetailResource()) {
if (wrapper.isUpdateDisbursementDate()) { if (wrapper.isUpdateDisbursementDate()) {
handler = this.applicationContext.getBean("updateLoanDisbuseDateCommandHandler", NewCommandSourceHandler.class); handler = applicationContext.getBean("updateLoanDisburseDateCommandHandler", NewCommandSourceHandler.class);
} else if (wrapper.addAndDeleteDisbursementDetails()) { } else if (wrapper.addAndDeleteDisbursementDetails()) {
handler = this.applicationContext.getBean("addAndDeleteLoanDisburseDetailsCommandHandler", NewCommandSourceHandler.class); handler = applicationContext.getBean("addAndDeleteLoanDisburseDetailsCommandHandler", NewCommandSourceHandler.class);
} else { } else {
throw new UnsupportedCommandException(wrapper.commandName()); throw new UnsupportedCommandException(wrapper.commandName());
} }
} else { } else {
handler = this.commandHandlerProvider.getHandler(wrapper.entityName(), wrapper.actionName()); handler = commandHandlerProvider.getHandler(wrapper.entityName(), wrapper.actionName());
} }
return handler; return handler;
@@ -201,12 +200,12 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
@Override @Override
public boolean validateCommand(final CommandWrapper commandWrapper, final AppUser user) { public boolean validateCommand(final CommandWrapper commandWrapper, final AppUser user) {
boolean rollbackTransaction = this.configurationDomainService.isMakerCheckerEnabledForTask(commandWrapper.taskPermissionName()); boolean rollbackTransaction = configurationDomainService.isMakerCheckerEnabledForTask(commandWrapper.taskPermissionName());
user.validateHasPermissionTo(commandWrapper.getTaskPermissionName()); user.validateHasPermissionTo(commandWrapper.getTaskPermissionName());
return rollbackTransaction; return rollbackTransaction;
} }
private void publishErrorEvent(CommandWrapper wrapper, JsonCommand command, Throwable t) { private void publishHookErrorEvent(CommandWrapper wrapper, JsonCommand command, Throwable t) {
ErrorInfo ex; ErrorInfo ex;
if (t instanceof final RuntimeException e) { if (t instanceof final RuntimeException e) {
@@ -215,13 +214,13 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
ex = new ErrorInfo(500, 9999, "{\"Exception\": " + t.toString() + "}"); ex = new ErrorInfo(500, 9999, "{\"Exception\": " + t.toString() + "}");
} }
publishEvent(wrapper.entityName(), wrapper.actionName(), command, ex); publishHookEvent(wrapper.entityName(), wrapper.actionName(), command, ex);
} }
private void publishEvent(final String entityName, final String actionName, JsonCommand command, final Object result) { private void publishHookEvent(final String entityName, final String actionName, JsonCommand command, final Object result) {
Gson gson = new Gson(); Gson gson = new Gson();
try { try {
final AppUser appUser = this.context.authenticatedUser(CommandWrapper.wrap(actionName, entityName, null, null)); final AppUser appUser = context.authenticatedUser(CommandWrapper.wrap(actionName, entityName, null, null));
final HookEventSource hookEventSource = new HookEventSource(entityName, actionName); final HookEventSource hookEventSource = new HookEventSource(entityName, actionName);
@@ -260,7 +259,7 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
reqmap.put("timestamp", Instant.now().toString()); reqmap.put("timestamp", Instant.now().toString());
final String serializedResult = this.toApiResultJsonSerializer.serialize(reqmap); final String serializedResult = toApiResultJsonSerializer.serialize(reqmap);
final HookEvent applicationEvent = new HookEvent(hookEventSource, serializedResult, appUser, final HookEvent applicationEvent = new HookEvent(hookEventSource, serializedResult, appUser,
ThreadLocalContextUtil.getContext()); ThreadLocalContextUtil.getContext());
@@ -52,11 +52,11 @@ public class FineractHookListener implements HookListener {
final String actionName = hookEventSource.getActionName(); final String actionName = hookEventSource.getActionName();
final String payload = event.getPayload(); final String payload = event.getPayload();
final List<Hook> hooks = this.hookReadPlatformService.retrieveHooksByEvent(hookEventSource.getEntityName(), final List<Hook> hooks = hookReadPlatformService.retrieveHooksByEvent(hookEventSource.getEntityName(),
hookEventSource.getActionName()); hookEventSource.getActionName());
for (final Hook hook : hooks) { for (final Hook hook : hooks) {
final HookProcessor processor = this.hookProcessorProvider.getProcessor(hook); final HookProcessor processor = hookProcessorProvider.getProcessor(hook);
try { try {
processor.process(hook, payload, entityName, actionName, fineractContext); processor.process(hook, payload, entityName, actionName, fineractContext);
} catch (Throwable e) { } catch (Throwable e) {
@@ -125,7 +125,7 @@ public class SchedulerJobRunnerReadServiceImpl implements SchedulerJobRunnerRead
public boolean isUpdatesAllowed() { public boolean isUpdatesAllowed() {
final String sql = "select job.display_name from job job where job.currently_running=true and job.updates_allowed=false"; final String sql = "select job.display_name from job job where job.currently_running=true and job.updates_allowed=false";
final List<String> names = this.jdbcTemplate.queryForList(sql, String.class); final List<String> names = this.jdbcTemplate.queryForList(sql, String.class);
if (names != null && names.size() > 0) { if (names.size() > 0) {
final String listVals = names.toString(); final String listVals = names.toString();
final String jobNames = listVals.substring(listVals.indexOf("[") + 1, listVals.indexOf("]")); final String jobNames = listVals.substring(listVals.indexOf("[") + 1, listVals.indexOf("]"));
throw new OperationNotAllowedException(jobNames); throw new OperationNotAllowedException(jobNames);
@@ -28,7 +28,7 @@ import org.springframework.transaction.annotation.Transactional;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class UpdateLoanDisbuseDateCommandHandler implements NewCommandSourceHandler { public class UpdateLoanDisburseDateCommandHandler implements NewCommandSourceHandler {
private final LoanWritePlatformService writePlatformService; private final LoanWritePlatformService writePlatformService;