From 29bc5cfceaad37e0656b1efd3c0bb43460a206dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Thu, 23 Oct 2025 10:01:30 +0200 Subject: [PATCH 1/4] draft MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- gateway-service/build.gradle | 5 ++ .../acceptance/RetryPerServiceTest.java | 49 ++++++++++++++++++- .../org/zowe/apiml/gateway/MockService.java | 47 ++++++++++++++++-- 3 files changed, 95 insertions(+), 6 deletions(-) diff --git a/gateway-service/build.gradle b/gateway-service/build.gradle index d56b1fa759..7d8faa819e 100644 --- a/gateway-service/build.gradle +++ b/gateway-service/build.gradle @@ -158,6 +158,11 @@ bootRun { systemProperties = System.properties } +test { + useJUnitPlatform() + jvmArgs '--add-opens=jdk.httpserver/sun.net.httpserver=ALL-UNNAMED' +} + publishing { publications { mavenJavaFat(MavenPublication) { diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/RetryPerServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/RetryPerServiceTest.java index e6bfc64cd3..73b1bc5d3b 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/RetryPerServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/RetryPerServiceTest.java @@ -14,14 +14,17 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.springframework.test.context.TestPropertySource; import org.zowe.apiml.gateway.MockService; import org.zowe.apiml.gateway.acceptance.common.AcceptanceTestWithMockServices; import org.zowe.apiml.gateway.acceptance.common.MicroservicesAcceptanceTest; +import java.io.IOException; + import static io.restassured.RestAssured.given; -import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE; -import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; +import static org.apache.http.HttpStatus.*; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -44,6 +47,8 @@ void startMockService() { .addEndpoint("/503").responseCode(503) .and() .addEndpoint("/401").responseCode(401) + .and() + .addEndpoint("/200").responseCode(200) .and().start(); mockNoRetryService = mockService("no-retry-service").scope(MockService.Scope.CLASS) @@ -119,4 +124,44 @@ void whenRetryForServiceIsDisabled_andGetReturnsUnavailable_onMixedCaseServiceId } + @Nested + class ConnectionReset { + + @ParameterizedTest(name = "givenConnectionInPool_whenServerWasRestarted_thenRetry({0}, {1})") + @CsvSource({ + "GET,CLOSE", + "GET,CLOSE_CHANNEL", + "GET,KILL_CHANNEL", + "GET,MARK_CHANNEL_AS_CLOSED", + "POST,CLOSE", + "POST,CLOSE_CHANNEL", + "POST,KILL_CHANNEL", + "POST,MARK_CHANNEL_AS_CLOSED" + }) + void givenConnectionInPool_whenServerWasRestarted_thenRetry(String method, MockService.ConnectionCleanupType cleanupType) { + var port = mockService.getPort(); + + given() + .header(HEADER_X_FORWARD_TO, "serviceid1") + .when() + .get(basePath + "/200") + .then() + .statusCode(is(SC_OK)); + + mockService.cleanConnections(cleanupType); + mockService.zombie(); + + var port2 = mockService.getPort(); + assertEquals(port, port2); + + given() + .header(HEADER_X_FORWARD_TO, "serviceid1") + .when() + .request(method, basePath + "/200") + .then() + .statusCode(is(SC_OK)); + } + + } + } diff --git a/gateway-service/src/testFixtures/java/org/zowe/apiml/gateway/MockService.java b/gateway-service/src/testFixtures/java/org/zowe/apiml/gateway/MockService.java index 0c5ca60f21..782b33a735 100644 --- a/gateway-service/src/testFixtures/java/org/zowe/apiml/gateway/MockService.java +++ b/gateway-service/src/testFixtures/java/org/zowe/apiml/gateway/MockService.java @@ -24,16 +24,14 @@ import org.assertj.core.error.MultipleAssertionsError; import org.springframework.cloud.netflix.eureka.EurekaServiceInstance; import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; import org.zowe.apiml.auth.AuthenticationScheme; import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; @@ -223,6 +221,38 @@ public void stop() { setStatus(Status.STOPPED); } + + /** + * To close and clean all open connection on the server + * @throws IOException in case of error during closing a connection + */ + public void cleanConnections(ConnectionCleanupType cleanupType) { + Object serverImpl = ReflectionTestUtils.getField(server, "server"); + Set allConnections = (Set) ReflectionTestUtils.getField(serverImpl, "allConnections"); + Set idleConnections = (Set) ReflectionTestUtils.getField(serverImpl, "idleConnections"); + synchronized (allConnections) { + for (Object connection : allConnections) { + var channel = ReflectionTestUtils.invokeMethod(connection, "getChannel"); + switch (cleanupType) { + case CLOSE: + ReflectionTestUtils.invokeMethod(connection, "close"); + break; + case CLOSE_CHANNEL: + ReflectionTestUtils.invokeMethod(channel, "close"); + break; + case KILL_CHANNEL: + ReflectionTestUtils.invokeMethod(channel, "kill"); + break; + case MARK_CHANNEL_AS_CLOSED: + ReflectionTestUtils.setField(channel, "closed", false); + break; + } + } + } + allConnections.clear(); + idleConnections.clear(); + } + /** * To stop service without any notification (to be still in the registry). In the case service is down, just notify * to be in the registry. @@ -541,4 +571,13 @@ public boolean isUp() { } + public enum ConnectionCleanupType { + + CLOSE, + CLOSE_CHANNEL, + KILL_CHANNEL, + MARK_CHANNEL_AS_CLOSED + + } + } From ebec0351a1b78f2ee7bf1a427c3ecc233f42bf8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Thu, 23 Oct 2025 10:04:21 +0200 Subject: [PATCH 2/4] fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../org/zowe/apiml/gateway/acceptance/RetryPerServiceTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/RetryPerServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/RetryPerServiceTest.java index 73b1bc5d3b..aeab5cd620 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/RetryPerServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/RetryPerServiceTest.java @@ -149,7 +149,6 @@ void givenConnectionInPool_whenServerWasRestarted_thenRetry(String method, MockS .statusCode(is(SC_OK)); mockService.cleanConnections(cleanupType); - mockService.zombie(); var port2 = mockService.getPort(); assertEquals(port, port2); From 2653c8490297299de1c1d38950484874fd20dc53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Thu, 23 Oct 2025 12:45:39 +0200 Subject: [PATCH 3/4] fix test RetryPerServiceTest (with cover current state about not retrying POST - reactor.netty.http.client.PrematureCloseException) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../acceptance/RetryPerServiceTest.java | 88 ++++++++++--------- 1 file changed, 47 insertions(+), 41 deletions(-) diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/RetryPerServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/RetryPerServiceTest.java index aeab5cd620..72f75c2cff 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/RetryPerServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/RetryPerServiceTest.java @@ -21,48 +21,43 @@ import org.zowe.apiml.gateway.acceptance.common.AcceptanceTestWithMockServices; import org.zowe.apiml.gateway.acceptance.common.MicroservicesAcceptanceTest; -import java.io.IOException; - import static io.restassured.RestAssured.given; import static org.apache.http.HttpStatus.*; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; -@MicroservicesAcceptanceTest -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -@TestPropertySource(properties = { - "apiml.gateway.servicesToDisableRetry=no-retry-service,no-RETRY-Service-2" -}) -class RetryPerServiceTest extends AcceptanceTestWithMockServices { +class RetryPerServiceTest { private static final String HEADER_X_FORWARD_TO = "X-Forward-To"; - private MockService mockService; - private MockService mockNoRetryService; - private MockService mockNoRetryService2; - - @BeforeAll - void startMockService() { - mockService = mockService("serviceid1").scope(MockService.Scope.CLASS) + @Nested + @MicroservicesAcceptanceTest + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + @TestPropertySource(properties = { + "apiml.gateway.servicesToDisableRetry=no-retry-service,no-RETRY-Service-2" + }) + class GivenRetryOnAllOperationsIsDisabled extends AcceptanceTestWithMockServices { + + private MockService mockService; + private MockService mockNoRetryService; + private MockService mockNoRetryService2; + + @BeforeAll + void startMockService() { + mockService = mockService("serviceid1").scope(MockService.Scope.CLASS) .addEndpoint("/503").responseCode(503) - .and() + .and() .addEndpoint("/401").responseCode(401) - .and() - .addEndpoint("/200").responseCode(200) - .and().start(); + .and().start(); - mockNoRetryService = mockService("no-retry-service").scope(MockService.Scope.CLASS) - .addEndpoint("/503").responseCode(503) - .and().start(); - - mockNoRetryService2 = mockService("No-Retry-Service-2").scope(MockService.Scope.CLASS) - .addEndpoint("/503").responseCode(503) - .and().start(); - } + mockNoRetryService = mockService("no-retry-service").scope(MockService.Scope.CLASS) + .addEndpoint("/503").responseCode(503) + .and().start(); - @Nested - class GivenRetryOnAllOperationsIsDisabled { - //Only default GET method remains active + mockNoRetryService2 = mockService("No-Retry-Service-2").scope(MockService.Scope.CLASS) + .addEndpoint("/503").responseCode(503) + .and().start(); + } @Test void whenGetReturnsUnavailable_thenRetry() { @@ -125,20 +120,31 @@ void whenRetryForServiceIsDisabled_andGetReturnsUnavailable_onMixedCaseServiceId } @Nested - class ConnectionReset { + @MicroservicesAcceptanceTest + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class ConnectionReset extends AcceptanceTestWithMockServices { + + private MockService mockService; + + @BeforeAll + void startMockService() { + mockService = mockService("serviceid1").scope(MockService.Scope.CLASS) + .addEndpoint("/200").responseCode(200) + .and().start(); + } @ParameterizedTest(name = "givenConnectionInPool_whenServerWasRestarted_thenRetry({0}, {1})") @CsvSource({ - "GET,CLOSE", - "GET,CLOSE_CHANNEL", - "GET,KILL_CHANNEL", - "GET,MARK_CHANNEL_AS_CLOSED", - "POST,CLOSE", - "POST,CLOSE_CHANNEL", - "POST,KILL_CHANNEL", - "POST,MARK_CHANNEL_AS_CLOSED" + "GET,CLOSE,200", + "GET,CLOSE_CHANNEL,200", + "GET,KILL_CHANNEL,200", + "GET,MARK_CHANNEL_AS_CLOSED,200", + "POST,CLOSE,200", + "POST,CLOSE_CHANNEL,200", + "POST,KILL_CHANNEL,500", + "POST,MARK_CHANNEL_AS_CLOSED,500" }) - void givenConnectionInPool_whenServerWasRestarted_thenRetry(String method, MockService.ConnectionCleanupType cleanupType) { + void givenConnectionInPool_whenServerWasRestarted_thenRetry(String method, MockService.ConnectionCleanupType cleanupType, int responseStatus) { var port = mockService.getPort(); given() @@ -158,7 +164,7 @@ void givenConnectionInPool_whenServerWasRestarted_thenRetry(String method, MockS .when() .request(method, basePath + "/200") .then() - .statusCode(is(SC_OK)); + .statusCode(is(responseStatus)); } } From 752ee367559985a8a9473e1ea3567fcfa674e955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Thu, 22 Jan 2026 16:34:52 +0100 Subject: [PATCH 4/4] attempt to fix an issue on GA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../apiml/gateway/acceptance/RetryPerServiceTest.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/RetryPerServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/RetryPerServiceTest.java index 72f75c2cff..185e4bbcdb 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/RetryPerServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/RetryPerServiceTest.java @@ -10,12 +10,10 @@ package org.zowe.apiml.gateway.acceptance; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.TestPropertySource; import org.zowe.apiml.gateway.MockService; import org.zowe.apiml.gateway.acceptance.common.AcceptanceTestWithMockServices; @@ -122,13 +120,14 @@ void whenRetryForServiceIsDisabled_andGetReturnsUnavailable_onMixedCaseServiceId @Nested @MicroservicesAcceptanceTest @TestInstance(TestInstance.Lifecycle.PER_CLASS) + @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) class ConnectionReset extends AcceptanceTestWithMockServices { private MockService mockService; - @BeforeAll + @BeforeEach void startMockService() { - mockService = mockService("serviceid1").scope(MockService.Scope.CLASS) + mockService = mockService("serviceid1").scope(MockService.Scope.TEST) .addEndpoint("/200").responseCode(200) .and().start(); }