diff --git a/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/PaymentLoop.java b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/PaymentLoop.java index 21e768d9..53254aba 100644 --- a/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/PaymentLoop.java +++ b/pickhardt-payments/src/main/java/de/cotto/lndmanagej/pickhardtpayments/PaymentLoop.java @@ -27,6 +27,7 @@ import static de.cotto.lndmanagej.configuration.TopUpConfigurationSettings.SLEEP public class PaymentLoop { private static final int DEFAULT_MAX_RETRIES = 5; private static final int DEFAULT_SLEEP_MILLISECONDS = 500; + private static final int MAX_NUMBER_OF_LOOPS = 100; private static final Duration TIMEOUT = Duration.ofMinutes(5); private static final String TIMEOUT_MESSAGE = @@ -93,7 +94,7 @@ public class PaymentLoop { private void start() { int loopIterationCounter = 0; int failureCounter = 0; - while (shouldContinue()) { + while (shouldContinue(loopIterationCounter)) { loopIterationCounter++; Coins residualAmount = totalAmountToSend.subtract(inFlight); if (Coins.NONE.equals(residualAmount)) { @@ -156,7 +157,11 @@ public class PaymentLoop { ); } - private boolean shouldContinue() { + private boolean shouldContinue(int loopIterationCounter) { + if (loopIterationCounter >= MAX_NUMBER_OF_LOOPS) { + paymentStatus.failed("Failing after " + MAX_NUMBER_OF_LOOPS + " loop iterations."); + return false; + } updateInformation(); if (!paymentStatus.isPending()) { return false; diff --git a/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/PaymentLoopTest.java b/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/PaymentLoopTest.java index 8bc61dfe..3d986ab3 100644 --- a/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/PaymentLoopTest.java +++ b/pickhardt-payments/src/test/java/de/cotto/lndmanagej/pickhardtpayments/PaymentLoopTest.java @@ -106,6 +106,20 @@ class PaymentLoopTest { verifyNoInteractions(grpcInvoices); } + @Test + void fails_after_one_hundred_loop_iterations() { + when(multiPathPaymentSplitter.getMultiPathPaymentTo(any(), any(), any())) + .thenReturn(MULTI_PATH_PAYMENT); + paymentLoop.start(DECODED_PAYMENT_REQUEST, PAYMENT_OPTIONS, paymentStatus); + + verify(multiPathPaymentSplitter, times(100)).getMultiPathPaymentTo(any(), any(), any()); + SoftAssertions softly = new SoftAssertions(); + softly.assertThat(paymentStatus.isFailure()).isTrue(); + softly.assertThat(paymentStatus.getMessages().stream().map(InstantWithString::string)) + .contains("Failing after 100 loop iterations."); + softly.assertAll(); + } + @Test void cancels_invoice_from_own_node() { when(grpcGetInfo.getPubkey()).thenReturn(DECODED_PAYMENT_REQUEST.destination()); @@ -139,7 +153,7 @@ class PaymentLoopTest { } @Test - void fails_after_configured_number_of_attempts() { + void fails_after_configured_number_of_retry_attempts() { when(configurationService.getIntegerValue(MAX_RETRIES_AFTER_FAILURE)).thenReturn(Optional.of(1)); when(configurationService.getIntegerValue(SLEEP_AFTER_FAILURE_MILLISECONDS)).thenReturn(Optional.of(1)); when(multiPathPaymentSplitter.getMultiPathPaymentTo(any(), any(), any())).thenReturn(MultiPathPayment.FAILURE);