| = Introduction Resilience |
| |
| Fineract had handcrafted retry loops in place for the longest time. A typical retry code would have looked like this: |
| |
| .Legacy retry code |
| [source,java] |
| ---- |
| @Override |
| @SuppressWarnings("AvoidHidingCauseException") |
| @SuppressFBWarnings(value = { |
| "DMI_RANDOM_USED_ONLY_ONCE" }, justification = "False positive for random object created and used only once") |
| public CommandProcessingResult logCommandSource(final CommandWrapper wrapper) { |
| |
| boolean isApprovedByChecker = false; |
| // check if is update of own account details |
| if (wrapper.isUpdateOfOwnUserDetails(this.context.authenticatedUser(wrapper).getId())) { |
| // then allow this operation to proceed. |
| // maker checker doesn't mean anything here. |
| isApprovedByChecker = true; // set to true in case permissions have |
| // been maker-checker enabled by |
| // accident. |
| } else { |
| // if not user changing their own details - check user has |
| // permission to perform specific task. |
| this.context.authenticatedUser(wrapper).validateHasPermissionTo(wrapper.getTaskPermissionName()); |
| } |
| validateIsUpdateAllowed(); |
| |
| final String json = wrapper.getJson(); |
| CommandProcessingResult result = null; |
| JsonCommand command; |
| int numberOfRetries = 0; // <1> |
| int maxNumberOfRetries = ThreadLocalContextUtil.getTenant().getConnection().getMaxRetriesOnDeadlock(); |
| int maxIntervalBetweenRetries = ThreadLocalContextUtil.getTenant().getConnection().getMaxIntervalBetweenRetries(); |
| final JsonElement parsedCommand = this.fromApiJsonHelper.parse(json); |
| command = JsonCommand.from(json, parsedCommand, this.fromApiJsonHelper, wrapper.getEntityName(), wrapper.getEntityId(), |
| wrapper.getSubentityId(), wrapper.getGroupId(), wrapper.getClientId(), wrapper.getLoanId(), wrapper.getSavingsId(), |
| wrapper.getTransactionId(), wrapper.getHref(), wrapper.getProductId(), wrapper.getCreditBureauId(), |
| wrapper.getOrganisationCreditBureauId(), wrapper.getJobName()); |
| while (numberOfRetries <= maxNumberOfRetries) { // <2> |
| try { |
| result = this.processAndLogCommandService.executeCommand(wrapper, command, isApprovedByChecker); |
| numberOfRetries = maxNumberOfRetries + 1; // <3> |
| } catch (CannotAcquireLockException | ObjectOptimisticLockingFailureException exception) { |
| log.debug("The following command {} has been retried {} time(s)", command.json(), numberOfRetries); |
| /*** |
| * Fail if the transaction has been retired for maxNumberOfRetries |
| **/ |
| if (numberOfRetries >= maxNumberOfRetries) { |
| log.warn("The following command {} has been retried for the max allowed attempts of {} and will be rolled back", |
| command.json(), numberOfRetries); |
| throw exception; |
| } |
| /*** |
| * Else sleep for a random time (between 1 to 10 seconds) and continue |
| **/ |
| try { |
| int randomNum = RANDOM.nextInt(maxIntervalBetweenRetries + 1); |
| Thread.sleep(1000 + (randomNum * 1000)); |
| numberOfRetries = numberOfRetries + 1; // <4> |
| } catch (InterruptedException e) { |
| throw exception; |
| } |
| } catch (final RollbackTransactionAsCommandIsNotApprovedByCheckerException e) { |
| numberOfRetries = maxNumberOfRetries + 1; // <3> |
| result = this.processAndLogCommandService.logCommand(e.getCommandSourceResult()); |
| } |
| } |
| |
| return result; |
| } |
| ---- |
| <1> counter |
| <2> `while` loop |
| <3> increment to abort |
| <4> increment |
| |
| For better code quality and readability we introduced https://resilience4j.readme.io/docs[Resilience4j]: |
| |
| .Annotation based retry |
| [source,java] |
| ---- |
| include::{rootdir}/fineract-core/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java[lines=49..76] |
| ---- |