diff --git a/.github/actions/scenarios/xds-service/xds-router-lb/action.yml b/.github/actions/scenarios/xds-service/xds-router-lb/action.yml index e2982e2055..06b2d8d449 100644 --- a/.github/actions/scenarios/xds-service/xds-router-lb/action.yml +++ b/.github/actions/scenarios/xds-service/xds-router-lb/action.yml @@ -9,7 +9,7 @@ runs: cp -r sermant-integration-tests/xds-service-test/product/sermant-agent-*/agent sermant-integration-tests/xds-service-test/product/spring-client/ cp -r sermant-integration-tests/xds-service-test/product/sermant-agent-*/agent sermant-integration-tests/xds-service-test/product/spring-cloud-client/ cp -r sermant-integration-tests/xds-service-test/product/sermant-agent-*/agent sermant-integration-tests/xds-service-test/product/spring-server/ - mvn package -Dspringboot.version=${{ matrix.springBootVersion }} -Dsnakeyaml.version=${{ matrix.snakeyamlVersion }} -Dspringcloud.version=${{ matrix.springCloudVersion }} -Dhttpclient.version=${{ matrix.httpClientVersion }} -Dokhttp2.version=${{ matrix.okHttp2Version }} -Dhttpclient.async.version=${{ matrix.httpAsyncClientVersion }} -Dokhttp3.version=${{ matrix.okHttp3Version }} -DskipTests -pl spring-client,spring-cloud-client,spring-server -Pxds-router-lb --file \ + mvn package -Dspringboot.version=${{ matrix.springBootVersion }} -Dsnakeyaml.version=${{ matrix.snakeyamlVersion }} -Dspringcloud.version=${{ matrix.springCloudVersion }} -Dhttpclient.version=${{ matrix.httpClientVersion }} -Dokhttp2.version=${{ matrix.okHttp2Version }} -Dhttpclient.async.version=${{ matrix.httpAsyncClientVersion }} -Dokhttp3.version=${{ matrix.okHttp3Version }} -DskipTests -pl spring-common,spring-client,spring-cloud-client,spring-server -Pxds-router-lb --file \ sermant-integration-tests/xds-service-test/pom.xml - name: build docker image shell: bash diff --git a/.github/actions/scenarios/xds-service/xds-service-discovery/client-envoy/action.yml b/.github/actions/scenarios/xds-service/xds-service-discovery/client-envoy/action.yml index 9cb0187ffb..6367fe7784 100644 --- a/.github/actions/scenarios/xds-service/xds-service-discovery/client-envoy/action.yml +++ b/.github/actions/scenarios/xds-service/xds-service-discovery/client-envoy/action.yml @@ -8,7 +8,7 @@ runs: run: | cp -r sermant-integration-tests/xds-service-test/product/sermant-agent-*/agent sermant-integration-tests/xds-service-test/product/spring-client/ cp -r sermant-integration-tests/xds-service-test/product/sermant-agent-*/agent sermant-integration-tests/xds-service-test/product/spring-server/ - mvn package -DskipTests -pl spring-client,spring-server -Pxds-discovery --file \ + mvn package -DskipTests -pl spring-common,spring-client,spring-server -Pxds-discovery --file \ sermant-integration-tests/xds-service-test/pom.xml - name: build docker image shell: bash diff --git a/.github/actions/scenarios/xds-service/xds-service-discovery/sermant-only/action.yml b/.github/actions/scenarios/xds-service/xds-service-discovery/sermant-only/action.yml index 9879780c4a..5352c76101 100644 --- a/.github/actions/scenarios/xds-service/xds-service-discovery/sermant-only/action.yml +++ b/.github/actions/scenarios/xds-service/xds-service-discovery/sermant-only/action.yml @@ -8,7 +8,7 @@ runs: run: | cp -r sermant-integration-tests/xds-service-test/product/sermant-agent-*/agent sermant-integration-tests/xds-service-test/product/spring-client/ cp -r sermant-integration-tests/xds-service-test/product/sermant-agent-*/agent sermant-integration-tests/xds-service-test/product/spring-server/ - mvn package -DskipTests -pl spring-client,spring-server -Pxds-discovery --file \ + mvn package -DskipTests -pl spring-common,spring-client,spring-server -Pxds-discovery --file \ sermant-integration-tests/xds-service-test/pom.xml - name: build docker image shell: bash diff --git a/.github/actions/scenarios/xds-service/xds-service-discovery/server-envoy/action.yml b/.github/actions/scenarios/xds-service/xds-service-discovery/server-envoy/action.yml index 416706fbbc..f2bb1aae7a 100644 --- a/.github/actions/scenarios/xds-service/xds-service-discovery/server-envoy/action.yml +++ b/.github/actions/scenarios/xds-service/xds-service-discovery/server-envoy/action.yml @@ -8,7 +8,7 @@ runs: run: | cp -r sermant-integration-tests/xds-service-test/product/sermant-agent-*/agent sermant-integration-tests/xds-service-test/product/spring-client/ cp -r sermant-integration-tests/xds-service-test/product/sermant-agent-*/agent sermant-integration-tests/xds-service-test/product/spring-server/ - mvn package -DskipTests -pl spring-client,spring-server -Pxds-discovery --file \ + mvn package -DskipTests -pl spring-common,spring-client,spring-server -Pxds-discovery --file \ sermant-integration-tests/xds-service-test/pom.xml - name: build docker image shell: bash diff --git a/sermant-integration-tests/xds-service-test/pom.xml b/sermant-integration-tests/xds-service-test/pom.xml index f1a0732cef..6cd557c3ee 100644 --- a/sermant-integration-tests/xds-service-test/pom.xml +++ b/sermant-integration-tests/xds-service-test/pom.xml @@ -59,6 +59,7 @@ xds-discovery + spring-common spring-client spring-server @@ -66,6 +67,16 @@ xds-router-lb + spring-common + spring-client + spring-server + spring-cloud-client + + + + xds-flowcontrol + + spring-common spring-client spring-server spring-cloud-client diff --git a/sermant-integration-tests/xds-service-test/spring-client/pom.xml b/sermant-integration-tests/xds-service-test/spring-client/pom.xml index 1fa1009061..7b77a4a8f5 100644 --- a/sermant-integration-tests/xds-service-test/spring-client/pom.xml +++ b/sermant-integration-tests/xds-service-test/spring-client/pom.xml @@ -14,6 +14,7 @@ 8 8 + 1.0.0 @@ -26,6 +27,11 @@ httpclient 4.5.13 + + io.sermant.integration + spring-common + ${spring.common.version} + org.apache.httpcomponents httpasyncclient diff --git a/sermant-integration-tests/xds-service-test/spring-client/src/main/java/io/sermant/demo/spring/client/flowcontrol/FlowControlController.java b/sermant-integration-tests/xds-service-test/spring-client/src/main/java/io/sermant/demo/spring/client/flowcontrol/FlowControlController.java new file mode 100644 index 0000000000..39da113c07 --- /dev/null +++ b/sermant-integration-tests/xds-service-test/spring-client/src/main/java/io/sermant/demo/spring/client/flowcontrol/FlowControlController.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2025-2025 Sermant Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.sermant.demo.spring.client.flowcontrol; + +import io.sermant.demo.spring.common.HttpClientType; +import io.sermant.demo.spring.client.util.HttpUtil; +import io.sermant.demo.spring.common.Constants; +import io.sermant.demo.spring.common.entity.Result; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.Map; + +/** + * flow control + * + * @author zhp + * @since 2025-01-13 + **/ +@RequestMapping("flowControl") +@RestController +public class FlowControlController { + /** + * Test the flow control functionality of the HttpClient client + * + * @param host the service address of upstream service + * @param path request path + * @return result + */ + @GetMapping("testHttpClient") + public Result testHttpClient(String host, String path, String version) { + Map headers = new HashMap<>(); + headers.put("version", version); + return HttpUtil.sendGetRequest(Constants.HTTP_PROTOCOL + host + Constants.SLASH + path, + HttpClientType.HTTP_CLIENT, headers); + } + + /** + * Test the flow control functionality of the OkHttp2 + * + * @param host the service address of upstream service + * @param path request path + * @return result + */ + @GetMapping("testOkHttp2") + public Result testOkHttp2(String host, String path, String version) { + Map headers = new HashMap<>(); + headers.put("version", version); + return HttpUtil.sendGetRequest(Constants.HTTP_PROTOCOL + host + Constants.SLASH + path, + HttpClientType.OK_HTTP2, headers); + } + + /** + * Test the flow control functionality of the HttpUrlConnection + * + * @param host the service address of upstream service + * @param path request path + * @return result + */ + @GetMapping("testHttpUrlConnection") + public Result testHttpUrlConnection(String host, String path, String version) { + Map headers = new HashMap<>(); + headers.put("version", version); + return HttpUtil.sendGetRequest(Constants.HTTP_PROTOCOL + host + Constants.SLASH + path, + HttpClientType.HTTP_URL_CONNECTION, headers); + } +} diff --git a/sermant-integration-tests/xds-service-test/spring-client/src/main/java/io/sermant/demo/spring/client/util/HttpUtil.java b/sermant-integration-tests/xds-service-test/spring-client/src/main/java/io/sermant/demo/spring/client/util/HttpUtil.java new file mode 100644 index 0000000000..4dcc8a2050 --- /dev/null +++ b/sermant-integration-tests/xds-service-test/spring-client/src/main/java/io/sermant/demo/spring/client/util/HttpUtil.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2025-2025 Sermant Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.sermant.demo.spring.client.util; + +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.RequestBody; +import com.squareup.okhttp.Response; +import com.squareup.okhttp.ResponseBody; +import io.sermant.demo.spring.common.HttpClientType; +import io.sermant.demo.spring.common.entity.Result; +import org.apache.http.HttpEntity; +import org.apache.http.HttpStatus; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.util.EntityUtils; +import org.springframework.http.HttpMethod; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * controller + * + * @author zhp + * @since 2025-01-13 + **/ +public class HttpUtil { + private static final int MAX_TOTAL = 200; + + private static final int MAX_PER_ROUTE = 20; + + private static final int CONNECT_TIMEOUT = 2000; + + private static final int SOCKET_TIMEOUT = 2000; + + private static final int RETRY_COUNT = 3; + + private static final String EMPTY_STR = ""; + + private static final CloseableHttpClient httpClient; + + private static final OkHttpClient client; + + static { + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); + connectionManager.setMaxTotal(MAX_TOTAL); + connectionManager.setDefaultMaxPerRoute(MAX_PER_ROUTE); + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(CONNECT_TIMEOUT) + .setSocketTimeout(SOCKET_TIMEOUT) + .build(); + httpClient = HttpClients.custom() + .setConnectionManager(connectionManager) + .setDefaultRequestConfig(requestConfig) + .setRetryHandler(new DefaultHttpRequestRetryHandler(RETRY_COUNT, true)) + .build(); + client = new OkHttpClient(); + client.setConnectTimeout(CONNECT_TIMEOUT, TimeUnit.MILLISECONDS); + client.setWriteTimeout(SOCKET_TIMEOUT, TimeUnit.MILLISECONDS); + client.setReadTimeout(SOCKET_TIMEOUT, TimeUnit.MILLISECONDS); + } + + /** + * send Http get request + * + * @param url request url + * @param headerMap request header information + * @param httpClientType Client type for sending HTTP requests + * @return response + */ + public static Result sendGetRequest(String url, HttpClientType httpClientType, Map headerMap) { + if (httpClientType == HttpClientType.HTTP_CLIENT) { + return sendGetRequestWithHttpClient(url, headerMap); + } + if (httpClientType == HttpClientType.OK_HTTP2) { + return sendGetRequestWithOkHttp(url, headerMap); + } + if (httpClientType == HttpClientType.HTTP_URL_CONNECTION) { + return sendRequestWithHttpUrlConnection(url, null, headerMap, HttpMethod.GET.name()); + } + return new Result(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Unsupported HttpClient", null); + } + + /** + * send Http Post request + * + * @param url request url + * @param body request body + * @param httpClientType Client type for sending HTTP requests + * @return response + */ + public static Result sendPostRequest(String url, Object body, HttpClientType httpClientType, + Map headerMap) { + if (httpClientType == HttpClientType.HTTP_CLIENT) { + return sendPostRequestWithHttpClient(url, body, headerMap); + } + if (httpClientType == HttpClientType.OK_HTTP2) { + return sendPostRequestWithOkHttp(url, body, headerMap); + } + if (httpClientType == HttpClientType.HTTP_URL_CONNECTION) { + return sendRequestWithHttpUrlConnection(url, body, headerMap, HttpMethod.POST.name()); + } + return new Result(HttpStatus.SC_INTERNAL_SERVER_ERROR, "Unsupported HttpClient", null); + } + + private static Result sendGetRequestWithHttpClient(String url, Map headerMap) { + HttpGet httpGet = new HttpGet(url); + return sendRequestWithHttpClient(headerMap, httpGet); + } + + private static Result sendRequestWithHttpClient(Map headerMap, HttpRequestBase httpRequestBase) { + for (Map.Entry entry : headerMap.entrySet()) { + httpRequestBase.addHeader(entry.getKey(), entry.getValue()); + } + try (CloseableHttpResponse response = httpClient.execute(httpRequestBase)) { + return new Result(response.getStatusLine().getStatusCode(), EMPTY_STR, + EntityUtils.toString(response.getEntity())); + } catch (IOException e) { + return new Result(HttpStatus.SC_INTERNAL_SERVER_ERROR, e.getMessage(), null); + } + } + + private static Result sendPostRequestWithHttpClient(String url, Object body, Map headerMap) { + HttpPost httpPost = new HttpPost(url); + if (body instanceof HttpEntity) { + httpPost.setEntity((HttpEntity) body); + } + return sendRequestWithHttpClient(headerMap, httpPost); + } + + private static Result sendGetRequestWithOkHttp(String url, Map headerMap) { + Request.Builder builder = new Request.Builder(); + for (Map.Entry entry : headerMap.entrySet()) { + builder.addHeader(entry.getKey(), entry.getValue()); + } + Request request = builder.url(url).build(); + return sendRequestWithOkHttp(request); + } + + private static Result sendPostRequestWithOkHttp(String url, Object body, Map headerMap) { + Request.Builder builder = new Request.Builder(); + for (Map.Entry entry : headerMap.entrySet()) { + builder.addHeader(entry.getKey(), entry.getValue()); + } + if (body instanceof RequestBody) { + builder.post((RequestBody) body); + } + Request request = builder.url(url).build(); + return sendRequestWithOkHttp(request); + } + + private static Result sendRequestWithOkHttp(Request request) { + try { + Response response = client.newCall(request).execute(); + try (ResponseBody responseBody = response.body()) { + return new Result(response.code(), response.message(), new String(responseBody.bytes())); + } + } catch (IOException e) { + return new Result(HttpStatus.SC_INTERNAL_SERVER_ERROR, e.getMessage(), null); + } + } + + private static Result sendRequestWithHttpUrlConnection(String urlStr, Object body, + Map headerMap, String httpMethod) { + HttpURLConnection connection = null; + BufferedReader in = null; + try { + connection = buildConnection(urlStr, headerMap, httpMethod); + if (body != null) { + connection.setDoOutput(true); + try (OutputStream os = connection.getOutputStream()) { + byte[] input = body.toString().getBytes(StandardCharsets.UTF_8); + os.write(input, 0, input.length); + } + } + in = new BufferedReader(new InputStreamReader(connection.getInputStream())); + String inputLine; + StringBuilder response = new StringBuilder(); + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); + } + return new Result(getResponseCode(connection), EMPTY_STR, response); + } catch (IOException e) { + return new Result(getResponseCode(connection), e.getMessage(), null); + } finally { + if (connection != null) { + connection.disconnect(); + } + if (in != null) { + try { + in.close(); + } catch (IOException e) { + // ignore + } + } + } + } + + private static int getResponseCode(HttpURLConnection connection) { + if (connection == null) { + return HttpStatus.SC_INTERNAL_SERVER_ERROR; + } + try { + return connection.getResponseCode(); + } catch (IOException e) { + return HttpStatus.SC_INTERNAL_SERVER_ERROR; + } + } + + private static HttpURLConnection buildConnection(String urlStr, Map headerMap, + String methodType) throws IOException { + URL url = new URL(urlStr); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod(methodType); + connection.setConnectTimeout(CONNECT_TIMEOUT); + connection.setReadTimeout(SOCKET_TIMEOUT); + for (Map.Entry entry : headerMap.entrySet()) { + connection.addRequestProperty(entry.getKey(), entry.getValue()); + } + return connection; + } +} diff --git a/sermant-integration-tests/xds-service-test/spring-cloud-client/pom.xml b/sermant-integration-tests/xds-service-test/spring-cloud-client/pom.xml index 7f9b803b6a..897bce88fd 100644 --- a/sermant-integration-tests/xds-service-test/spring-cloud-client/pom.xml +++ b/sermant-integration-tests/xds-service-test/spring-cloud-client/pom.xml @@ -14,6 +14,7 @@ 8 8 + 1.0.0 @@ -39,6 +40,11 @@ okhttp ${okhttp3.version} + + io.sermant.integration + spring-common + ${spring.common.version} + diff --git a/sermant-integration-tests/xds-service-test/spring-cloud-client/src/main/java/io/sermant/demo/springcloud/client/SpringFlowControlController.java b/sermant-integration-tests/xds-service-test/spring-cloud-client/src/main/java/io/sermant/demo/springcloud/client/SpringFlowControlController.java new file mode 100644 index 0000000000..c95bb7dfe8 --- /dev/null +++ b/sermant-integration-tests/xds-service-test/spring-cloud-client/src/main/java/io/sermant/demo/springcloud/client/SpringFlowControlController.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024-2024 Sermant Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.sermant.demo.springcloud.client; + +import io.sermant.demo.spring.common.Constants; +import io.sermant.demo.spring.common.entity.Result; +import okhttp3.OkHttpClient; +import org.apache.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +/** + * SpringRouterController + * + * @author daizhenyu + * @since 2024-09-23 + **/ +@RequestMapping("flowControl") +@RestController +public class SpringFlowControlController { + /** + * Test the flow control functionality of the HttpClient client + * + * @param host the service address of upstream service + * @param path request path + * @return result + */ + @GetMapping("testOkHttp3") + public Result testOkHttp3(String host, String path, String version) { + String url = Constants.HTTP_PROTOCOL + host + Constants.SLASH + path; + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + builder.readTimeout(3000, TimeUnit.MILLISECONDS).writeTimeout(3000, TimeUnit.MILLISECONDS) + .connectTimeout(3000, TimeUnit.MILLISECONDS); + OkHttpClient client = builder.build(); + okhttp3.Request request = new okhttp3.Request.Builder() + .url(url) + .addHeader("version", version) + .build(); + try (okhttp3.Response response = client.newCall(request).execute()) { + if (response.body() != null) { + return new Result(response.code(), "", new String(response.body().bytes())); + } + return new Result(response.code(), "", ""); + } catch (IOException e) { + return new Result(HttpStatus.SC_INTERNAL_SERVER_ERROR, e.getMessage(), null); + } + } +} diff --git a/sermant-integration-tests/xds-service-test/spring-common/pom.xml b/sermant-integration-tests/xds-service-test/spring-common/pom.xml new file mode 100644 index 0000000000..74a2130088 --- /dev/null +++ b/sermant-integration-tests/xds-service-test/spring-common/pom.xml @@ -0,0 +1,19 @@ + + + + xds-service-test + io.sermant.integration + 1.0.0 + + 4.0.0 + + spring-common + + + 8 + 8 + + + diff --git a/sermant-integration-tests/xds-service-test/spring-common/src/main/java/io/sermant/demo.spring.common/Constants.java b/sermant-integration-tests/xds-service-test/spring-common/src/main/java/io/sermant/demo.spring.common/Constants.java new file mode 100644 index 0000000000..3296e92b9a --- /dev/null +++ b/sermant-integration-tests/xds-service-test/spring-common/src/main/java/io/sermant/demo.spring.common/Constants.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025-2025 Sermant Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.sermant.demo.spring.common; + +/** + * Constants + * + * @author zhp + * @since 2025-01-13 + **/ +public class Constants { + /** + * http protocol + */ + public static final String HTTP_PROTOCOL = "http://"; + + /** + * slash + */ + public static final String SLASH = "/"; +} diff --git a/sermant-integration-tests/xds-service-test/spring-common/src/main/java/io/sermant/demo.spring.common/HttpClientType.java b/sermant-integration-tests/xds-service-test/spring-common/src/main/java/io/sermant/demo.spring.common/HttpClientType.java new file mode 100644 index 0000000000..e8f1be2fc5 --- /dev/null +++ b/sermant-integration-tests/xds-service-test/spring-common/src/main/java/io/sermant/demo.spring.common/HttpClientType.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2025-2025 Sermant Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.sermant.demo.spring.common; + +/** + * HTTP client types + * + * @author zhp + * @since 2025-01-13 + **/ +public enum HttpClientType { + /** + * httpClient + */ + HTTP_CLIENT("HTTP_CLIENT"), + + /** + * okHttp2 + */ + OK_HTTP2("OK_HTTP2"), + + /** + * okHttp3 + */ + OK_HTTP3("OK_HTTP3"), + + /** + * httpUrlConnection + */ + HTTP_URL_CONNECTION("HTTP_URL_CONNECTION"); + + private final String clientName; + + public String getClientName() { + return clientName; + } + + /** + * Constructor + * + * @param clientName clent name + */ + HttpClientType(String clientName) { + this.clientName = clientName; + } + +} diff --git a/sermant-integration-tests/xds-service-test/spring-common/src/main/java/io/sermant/demo.spring.common/entity/Result.java b/sermant-integration-tests/xds-service-test/spring-common/src/main/java/io/sermant/demo.spring.common/entity/Result.java new file mode 100644 index 0000000000..921e06cd61 --- /dev/null +++ b/sermant-integration-tests/xds-service-test/spring-common/src/main/java/io/sermant/demo.spring.common/entity/Result.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024-2024 Huawei Technologies Co., Ltd. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.sermant.demo.spring.common.entity; + +/** + * Result Information + * + * @author zhp + * @since 2025-01-13 + */ +public class Result { + /** + * result code + */ + private int code; + + /** + * result message + */ + private String message; + + /** + * result data + */ + private Object data; + + /** + * Constructor + * + * @param code result code + * @param message result message + * @param data result data + */ + public Result(int code, String message, Object data) { + this.code = code; + this.message = message; + this.data = data; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Object getData() { + return data; + } + + public void setData(Object data) { + this.data = data; + } +} diff --git a/sermant-integration-tests/xds-service-test/spring-server/src/main/java/io/sermant/demo/spring/server/flowcontrol/CircuitBreakerController.java b/sermant-integration-tests/xds-service-test/spring-server/src/main/java/io/sermant/demo/spring/server/flowcontrol/CircuitBreakerController.java new file mode 100644 index 0000000000..50b9a8c6fd --- /dev/null +++ b/sermant-integration-tests/xds-service-test/spring-server/src/main/java/io/sermant/demo/spring/server/flowcontrol/CircuitBreakerController.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2025-2025 Sermant Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.sermant.demo.spring.server.flowcontrol; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * CircuitBreakerController + * + * @author zhp + * @since 2025-01-13 + **/ +@RestController +public class CircuitBreakerController { + @Value("${connectTimeout}") + private int timeout; + + @Value("${statusCode}") + private int statusCode; + + @GetMapping("/testRequestCircuitBreaker") + public ResponseEntity testRequestCircuitBreaker() { + try { + Thread.sleep(timeout); + } catch (InterruptedException e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("testRequestCircuitBreaker"); + } + return ResponseEntity.ok().body("testRequestCircuitBreaker"); + } + + @GetMapping("/testInstanceCircuitBreaker") + public ResponseEntity testInstanceCircuitBreaker() { + System.out.println(statusCode); + return ResponseEntity.status(statusCode).body(String.valueOf(statusCode)); + } +} diff --git a/sermant-integration-tests/xds-service-test/spring-server/src/main/java/io/sermant/demo/spring/server/flowcontrol/FaultController.java b/sermant-integration-tests/xds-service-test/spring-server/src/main/java/io/sermant/demo/spring/server/flowcontrol/FaultController.java new file mode 100644 index 0000000000..8a41e612d8 --- /dev/null +++ b/sermant-integration-tests/xds-service-test/spring-server/src/main/java/io/sermant/demo/spring/server/flowcontrol/FaultController.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025-2025 Sermant Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.sermant.demo.spring.server.flowcontrol; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * FaultController + * + * @author zhp + * @since 2025-01-13 + **/ +@RestController +public class FaultController { + @RequestMapping("testFault") + public ResponseEntity testFault() { + return ResponseEntity.ok().body("testFault"); + } +} diff --git a/sermant-integration-tests/xds-service-test/spring-server/src/main/java/io/sermant/demo/spring/server/flowcontrol/RateLimitController.java b/sermant-integration-tests/xds-service-test/spring-server/src/main/java/io/sermant/demo/spring/server/flowcontrol/RateLimitController.java new file mode 100644 index 0000000000..3de98542f5 --- /dev/null +++ b/sermant-integration-tests/xds-service-test/spring-server/src/main/java/io/sermant/demo/spring/server/flowcontrol/RateLimitController.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025-2025 Sermant Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.sermant.demo.spring.server.flowcontrol; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * RateLimitController + * + * @author zhp + * @since 2025-01-13 + **/ +@RestController +public class RateLimitController { + @RequestMapping("testRateLimit") + public ResponseEntity testRateLimit() { + return ResponseEntity.ok().body("testRateLimit"); + } +} diff --git a/sermant-integration-tests/xds-service-test/spring-server/src/main/java/io/sermant/demo/spring/server/flowcontrol/RetryController.java b/sermant-integration-tests/xds-service-test/spring-server/src/main/java/io/sermant/demo/spring/server/flowcontrol/RetryController.java new file mode 100644 index 0000000000..654f8f8d40 --- /dev/null +++ b/sermant-integration-tests/xds-service-test/spring-server/src/main/java/io/sermant/demo/spring/server/flowcontrol/RetryController.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2025-2025 Sermant Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.sermant.demo.spring.server.flowcontrol; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * RetryController + * + * @author zhp + * @since 2025-01-13 + **/ +@RestController +public class RetryController { + @Value("${connectTimeout}") + private int timeout; + + private int requestCount = 0; + + @RequestMapping("testGateWayError") + public ResponseEntity testGateWayError() { + if (requestCount > 0) { + requestCount++; + return ResponseEntity.ok().body(String.valueOf(requestCount)); + } + requestCount++; + return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(String.valueOf(requestCount)); + } + + @GetMapping("/testRetryOnHeader") + public ResponseEntity testRetryOnHeader() { + System.out.println("testRetryOnHeader " + requestCount); + if (requestCount > 0) { + requestCount++; + return ResponseEntity.ok().body(String.valueOf(requestCount)); + } + requestCount++; + HttpHeaders headers = new HttpHeaders(); + headers.add("needRetry", "true"); + return ResponseEntity.status(HttpStatus.BAD_GATEWAY).headers(headers).body(String.valueOf(requestCount)); + } + + @GetMapping("/testRetryOnStatusCode") + public ResponseEntity testRetryOnStatusCode() { + System.out.println("testRetryOnStatusCode " + requestCount); + if (requestCount > 0) { + requestCount++; + return ResponseEntity.ok().body(String.valueOf(requestCount)); + } + requestCount++; + return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(String.valueOf(requestCount)); + } + + @GetMapping("/testConnectError") + public ResponseEntity testConnectError() { + System.out.println("testConnectError " + requestCount); + if (requestCount > 0) { + requestCount++; + return ResponseEntity.ok().body(String.valueOf(requestCount)); + } + requestCount++; + try{ + Thread.sleep(timeout); + } catch (InterruptedException e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(String.valueOf(requestCount)); + } + return ResponseEntity.ok().body(String.valueOf(requestCount)); + } + + @GetMapping("/test4xxError") + public ResponseEntity test4xxError() { + if (requestCount > 0) { + requestCount++; + return ResponseEntity.ok().body(String.valueOf(requestCount)); + } + requestCount++; + return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).body(String.valueOf(requestCount)); + } + + @GetMapping("/test5xxError") + public ResponseEntity test5xxError() { + if (requestCount > 0) { + requestCount++; + return ResponseEntity.ok().body(String.valueOf(requestCount)); + } + requestCount++; + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(String.valueOf(requestCount)); + } + + @GetMapping("/getRequestCount") + public ResponseEntity getRequestCount() { + return ResponseEntity.ok().body(String.valueOf(requestCount)); + } + + @GetMapping("/reset") + public ResponseEntity reset() { + requestCount = 0; + return ResponseEntity.ok().body(String.valueOf(requestCount)); + } +} diff --git a/sermant-integration-tests/xds-service-test/spring-server/src/main/resources/application.yml b/sermant-integration-tests/xds-service-test/spring-server/src/main/resources/application.yml index 2d4efb462b..5d662001ff 100644 --- a/sermant-integration-tests/xds-service-test/spring-server/src/main/resources/application.yml +++ b/sermant-integration-tests/xds-service-test/spring-server/src/main/resources/application.yml @@ -7,3 +7,5 @@ spring: enabled: ${ZOOKEEPER_ENABLED:false} server: port: 8081 +connectTimeout: 1500 +statusCode: 200 diff --git a/sermant-integration-tests/xds-service-test/xds-service-integration-test/pom.xml b/sermant-integration-tests/xds-service-test/xds-service-integration-test/pom.xml index 54df56a05b..a54a698b49 100644 --- a/sermant-integration-tests/xds-service-test/xds-service-integration-test/pom.xml +++ b/sermant-integration-tests/xds-service-test/xds-service-integration-test/pom.xml @@ -20,6 +20,7 @@ 1.7.35 5.8.1 3.0.0 + 1.0.0 @@ -59,6 +60,11 @@ ${commons-log.version} test + + com.alibaba + fastjson + ${fastjson.version} + diff --git a/sermant-integration-tests/xds-service-test/xds-service-integration-test/src/test/java/io/sermant/xds/service/entity/HttpClientType.java b/sermant-integration-tests/xds-service-test/xds-service-integration-test/src/test/java/io/sermant/xds/service/entity/HttpClientType.java new file mode 100644 index 0000000000..28423b6ad0 --- /dev/null +++ b/sermant-integration-tests/xds-service-test/xds-service-integration-test/src/test/java/io/sermant/xds/service/entity/HttpClientType.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2025-2025 Sermant Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.sermant.xds.service.entity; + +/** + * HTTP client types + * + * @author zhp + * @since 2025-01-13 + **/ +public enum HttpClientType { + /** + * httpClient + */ + HTTP_CLIENT("HTTP_CLIENT"), + + /** + * okHttp2 + */ + OK_HTTP2("OK_HTTP2"), + + /** + * okHttp3 + */ + OK_HTTP3("OK_HTTP3"), + + /** + * httpUrlConnection + */ + HTTP_URL_CONNECTION("HTTP_URL_CONNECTION"); + + private final String clientName; + + public String getClientName() { + return clientName; + } + + /** + * Constructor + * + * @param clientName clent name + */ + HttpClientType(String clientName) { + this.clientName = clientName; + } + +} diff --git a/sermant-integration-tests/xds-service-test/xds-service-integration-test/src/test/java/io/sermant/xds/service/entity/Result.java b/sermant-integration-tests/xds-service-test/xds-service-integration-test/src/test/java/io/sermant/xds/service/entity/Result.java new file mode 100644 index 0000000000..b34a102924 --- /dev/null +++ b/sermant-integration-tests/xds-service-test/xds-service-integration-test/src/test/java/io/sermant/xds/service/entity/Result.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024-2024 Huawei Technologies Co., Ltd. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package io.sermant.xds.service.entity; + +/** + * Result Information + * + * @author zhp + * @since 2025-01-13 + */ +public class Result { + /** + * result code + */ + private int code; + + /** + * result message + */ + private String message; + + /** + * result data + */ + private Object data; + + /** + * Constructor + * + * @param code result code + * @param message result message + * @param data result data + */ + public Result(int code, String message, Object data) { + this.code = code; + this.message = message; + this.data = data; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Object getData() { + return data; + } + + public void setData(Object data) { + this.data = data; + } +} diff --git a/sermant-integration-tests/xds-service-test/xds-service-integration-test/src/test/java/io/sermant/xds/service/flowcontrol/XdsFlowControlTest.java b/sermant-integration-tests/xds-service-test/xds-service-integration-test/src/test/java/io/sermant/xds/service/flowcontrol/XdsFlowControlTest.java new file mode 100644 index 0000000000..5ebc87eec7 --- /dev/null +++ b/sermant-integration-tests/xds-service-test/xds-service-integration-test/src/test/java/io/sermant/xds/service/flowcontrol/XdsFlowControlTest.java @@ -0,0 +1,374 @@ +/* + * Copyright (C) 2025-2025 Sermant Authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.sermant.xds.service.flowcontrol; + +import com.alibaba.fastjson.JSONObject; +import io.sermant.xds.service.entity.HttpClientType; +import io.sermant.xds.service.entity.Result; +import org.apache.http.HttpStatus; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; + +import java.io.IOException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * xDS flow control test + * + * @author zhp + * @since 2025-01-13 + **/ +public class XdsFlowControlTest { + private static final int CONNECT_TIMEOUT = 30000; + + private static final int SOCKET_TIMEOUT = 30000; + + private static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(25); + + private static final String OKHTTP_URL_PREFIX = + "http://127.0.0.1:8080/flowControl/testOkHttp2?host=spring-server&version="; + + private static final String OKHTTP3_URL_PREFIX = + "http://127.0.0.1:8082/flowControl/testOkHttp3?host=spring-server&version="; + + private static final String HTTP_CLIENT_URL_PREFIX = + "http://127.0.0.1:8080/flowControl/testHttpClient?host=spring-server&version="; + + private static final String HTTP_URL_CONNECTION_URL_PREFIX = + "http://127.0.0.1:8080/flowControl/testHttpUrlConnection?host=spring-server&version="; + + /** + * test fault + */ + @Test + @EnabledIfSystemProperty(named = "xds.service.integration.test.type", matches = "FLOW_CONTROL_FAULT") + public void testFault() throws InterruptedException { + // Test the case where the request delay probability is 0 + testFaultProbability(buildFaultUrl(HttpClientType.HTTP_CLIENT, "v1"), 0, 0, 1000); + testFaultProbability(buildFaultUrl(HttpClientType.OK_HTTP2, "v1"), 0, 0, 1000); + testFaultProbability(buildFaultUrl(HttpClientType.HTTP_URL_CONNECTION, "v1"), 0, 0, 1000); + testFaultProbability(buildFaultUrl(HttpClientType.OK_HTTP3, "v1"), 0, 0, 1000); + + // Test the case where the request delay probability is 50% + testFaultProbability(buildFaultUrl(HttpClientType.HTTP_CLIENT, "v2"), 10, 40, 10000); + testFaultProbability(buildFaultUrl(HttpClientType.OK_HTTP2, "v2"), 10, 40, 10000); + testFaultProbability(buildFaultUrl(HttpClientType.HTTP_URL_CONNECTION, "v2"), 10, 40, 10000); + testFaultProbability(buildFaultUrl(HttpClientType.OK_HTTP3, "v2"), 10, 40, 10000); + + // Test the case where the request delay probability is 100% + testFaultProbability(buildFaultUrl(HttpClientType.HTTP_CLIENT, "v3"), 50, 50, 10000); + testFaultProbability(buildFaultUrl(HttpClientType.OK_HTTP2, "v3"), 50, 50, 10000); + testFaultProbability(buildFaultUrl(HttpClientType.HTTP_URL_CONNECTION, "v3"), 50, 50, 10000); + testFaultProbability(buildFaultUrl(HttpClientType.OK_HTTP3, "v3"), 50, 50, 10000); + + // Test the case where the request abort probability is 0 + testFaultProbability(buildFaultUrl(HttpClientType.HTTP_CLIENT, "v4"), 0, 0, 1000); + testFaultProbability(buildFaultUrl(HttpClientType.OK_HTTP2, "v4"), 0, 0, 1000); + testFaultProbability(buildFaultUrl(HttpClientType.HTTP_URL_CONNECTION, "v4"), 0, 0, 1000); + testFaultProbability(buildFaultUrl(HttpClientType.OK_HTTP3, "v4"), 0, 0, 1000); + + // Test the case where the request abort probability is 50% + testFaultProbability(buildFaultUrl(HttpClientType.HTTP_CLIENT, "v5"), 10, 40, 1000); + testFaultProbability(buildFaultUrl(HttpClientType.OK_HTTP2, "v5"), 10, 40, 1000); + testFaultProbability(buildFaultUrl(HttpClientType.HTTP_URL_CONNECTION, "v5"), 10, 40, 1000); + testFaultProbability(buildFaultUrl(HttpClientType.OK_HTTP3, "v5"), 10, 40, 1000); + + // Test the case where the request abort probability is 100% + testFaultProbability(buildFaultUrl(HttpClientType.HTTP_CLIENT, "v5"), 50, 50, 1000); + testFaultProbability(buildFaultUrl(HttpClientType.OK_HTTP2, "v5"), 50, 50, 1000); + testFaultProbability(buildFaultUrl(HttpClientType.HTTP_URL_CONNECTION, "v5"), 50, 50, 1000); + testFaultProbability(buildFaultUrl(HttpClientType.OK_HTTP3, "v5"), 50, 50, 1000); + } + + /** + * test retry + */ + @Test + @EnabledIfSystemProperty(named = "xds.service.integration.test.type", matches = "FLOW_CONTROL_RETRY") + public void testRetry() { + // Test does not meet the matching rules, retry will not be triggered + resetRequestCount(); + Result result = doGet(buildGateWayErrorUrl(HttpClientType.HTTP_CLIENT, "v2")); + Assertions.assertEquals(HttpStatus.SC_BAD_GATEWAY, result.getCode()); + resetRequestCount(); + result = doGet(buildGateWayErrorUrl(HttpClientType.OK_HTTP2, "v2")); + Assertions.assertEquals(HttpStatus.SC_BAD_GATEWAY, result.getCode()); + resetRequestCount(); + result = doGet(buildGateWayErrorUrl(HttpClientType.OK_HTTP3, "v2")); + Assertions.assertEquals(HttpStatus.SC_BAD_GATEWAY, result.getCode()); + resetRequestCount(); + result = doGet(buildGateWayErrorUrl(HttpClientType.HTTP_URL_CONNECTION, "v2")); + Assertions.assertEquals(HttpStatus.SC_BAD_GATEWAY, result.getCode()); + resetRequestCount(); + + doGet(buildGateWayErrorUrl(HttpClientType.HTTP_CLIENT, "v1")); + doGet(buildGateWayErrorUrl(HttpClientType.HTTP_CLIENT, "v1")); + + // Test meet the matching rules, retry will not be triggered + result = doGet(buildGateWayErrorUrl(HttpClientType.HTTP_CLIENT, "v1")); + Assertions.assertEquals("2", result.getData()); + result = doGet(buildGateWayErrorUrl(HttpClientType.OK_HTTP2, "v1")); + Assertions.assertEquals("3", result.getData()); + result = doGet(buildGateWayErrorUrl(HttpClientType.HTTP_URL_CONNECTION, "v1")); + Assertions.assertEquals("3", result.getData()); + result = doGet(buildGateWayErrorUrl(HttpClientType.OK_HTTP3, "v1")); + Assertions.assertEquals("4", result.getData()); + resetRequestCount(); + + // Test the retry be triggered + testAllRetryCondition("http://127.0.0.1:8080/flowControl/testHttpClient"); + testAllRetryCondition("http://127.0.0.1:8080/flowControl/testOkHttp2"); + testAllRetryCondition("http://127.0.0.1:8080/flowControl/testHttpUrlConnection"); + testAllRetryCondition("http://127.0.0.1:8082/flowControl/testOkHttp3"); + } + + private static void testAllRetryCondition(String urlPrefix) { + // Test the case of retries when a gateway error occurs + testRequestFailureCount(urlPrefix + "?host=spring-server&version=v1&path=testGateWayError"); + // Test the case of retries when the response contains the specified response header + testRequestFailureCount(urlPrefix + "?host=spring-server&version=v1&path=testRetryOnHeader"); + // Test the case of retries when the response contains the specified response code + testRequestFailureCount(urlPrefix + "?host=spring-server&version=v1&path=testRetryOnStatusCode"); + // Test the case of retries when a connect failure occurs + testRequestFailureCount(urlPrefix + "?host=spring-server&version=v1&path=testConnectError"); + // Test the case of retries when the response code is 4xx + testRequestFailureCount(urlPrefix + "?host=spring-server&version=v1&path=test4xxError"); + // Test the case of retries when the response code is 5xx + testRequestFailureCount(urlPrefix + "?host=spring-server&version=v1&path=test5xxError"); + } + + /** + * test Circuit Breaker + */ + @Test + @EnabledIfSystemProperty(named = "xds.service.integration.test.type", matches = "FLOW_CONTROL_CIRCUIT_BREAKER") + public void testCircuitBreaker() throws InterruptedException { + // Test Circuit Breaker Function Based on Active Request Count + EXECUTOR_SERVICE.execute(() -> doGet(buildRequestCircuitBreakerUrl(HttpClientType.HTTP_CLIENT, "v1"))); + EXECUTOR_SERVICE.execute(() -> doGet(buildRequestCircuitBreakerUrl(HttpClientType.OK_HTTP3, "v1"))); + Thread.sleep(500); + EXECUTOR_SERVICE.execute(() -> { + Result result = doGet(buildRequestCircuitBreakerUrl(HttpClientType.HTTP_CLIENT, "v1")); + Assertions.assertNotEquals(HttpStatus.SC_OK, result.getCode()); + }); + EXECUTOR_SERVICE.execute(() -> { + Result result = doGet(buildRequestCircuitBreakerUrl(HttpClientType.OK_HTTP2, "v1")); + Assertions.assertNotEquals(HttpStatus.SC_OK, result.getCode()); + }); + EXECUTOR_SERVICE.execute(() -> { + Result result = doGet(buildRequestCircuitBreakerUrl(HttpClientType.OK_HTTP3, "v1")); + Assertions.assertNotEquals(HttpStatus.SC_OK, result.getCode()); + }); + EXECUTOR_SERVICE.execute(() -> { + Result result = doGet(buildRequestCircuitBreakerUrl(HttpClientType.HTTP_URL_CONNECTION, "v1")); + Assertions.assertNotEquals(HttpStatus.SC_OK, result.getCode()); + }); + + // Test Instance Circuit Breaker Base on GateWayError + testRemovedCircuitBreakerInstance("v2"); + + // Test Instance Circuit Breaker Base on 5XX error + testRemovedCircuitBreakerInstance("v3"); + Thread.sleep(10000); + + // Test instance recovery + Result result = doGet(buildInstanceCircuitBreakerUrl(HttpClientType.HTTP_CLIENT, "v2")); + Result result1 = doGet(buildInstanceCircuitBreakerUrl(HttpClientType.HTTP_CLIENT, "v2")); + Assertions.assertTrue(HttpStatus.SC_OK != result.getCode() || result1.getCode() != HttpStatus.SC_OK); + result = doGet(buildInstanceCircuitBreakerUrl(HttpClientType.OK_HTTP2, "v2")); + result1 = doGet(buildInstanceCircuitBreakerUrl(HttpClientType.OK_HTTP2, "v2")); + Assertions.assertTrue(HttpStatus.SC_OK != result.getCode() || result1.getCode() != HttpStatus.SC_OK); + result = doGet(buildInstanceCircuitBreakerUrl(HttpClientType.OK_HTTP3, "v2")); + result1 = doGet(buildInstanceCircuitBreakerUrl(HttpClientType.OK_HTTP3, "v2")); + Assertions.assertTrue(HttpStatus.SC_OK != result.getCode() || result1.getCode() != HttpStatus.SC_OK); + result = doGet(buildInstanceCircuitBreakerUrl(HttpClientType.HTTP_URL_CONNECTION, "v2")); + result1 = doGet(buildInstanceCircuitBreakerUrl(HttpClientType.HTTP_URL_CONNECTION, "v2")); + Assertions.assertTrue(HttpStatus.SC_OK != result.getCode() || result1.getCode() != HttpStatus.SC_OK); + } + + private static void testRemovedCircuitBreakerInstance(String version) { + for (int i = 0; i < 40; i++) { + doGet(buildInstanceCircuitBreakerUrl(HttpClientType.HTTP_CLIENT, version)); + doGet(buildInstanceCircuitBreakerUrl(HttpClientType.OK_HTTP3, version)); + } + for (int i = 0; i < 40; i++) { + Result result = doGet(buildInstanceCircuitBreakerUrl(HttpClientType.HTTP_CLIENT, version)); + Assertions.assertEquals(HttpStatus.SC_OK, result.getCode()); + result = doGet(buildInstanceCircuitBreakerUrl(HttpClientType.OK_HTTP2, version)); + Assertions.assertEquals(HttpStatus.SC_OK, result.getCode()); + result = doGet(buildInstanceCircuitBreakerUrl(HttpClientType.OK_HTTP3, version)); + Assertions.assertEquals(HttpStatus.SC_OK, result.getCode()); + result = doGet(buildInstanceCircuitBreakerUrl(HttpClientType.HTTP_URL_CONNECTION, version)); + Assertions.assertEquals(HttpStatus.SC_OK, result.getCode()); + } + } + + /** + * test Circuit Breaker + */ + @Test + @EnabledIfSystemProperty(named = "xds.service.integration.test.type", matches = "FLOW_CONTROL_RATE_LIMIT") + public void testRateLimit() throws InterruptedException { + // Test the case where the request delay probability is 0 + for (int i = 0; i < 10; i++) { + Result result = doGet(buildRateLimitUrl(HttpClientType.HTTP_CLIENT, "v1")); + Assertions.assertEquals(HttpStatus.SC_OK, result.getCode()); + result = doGet(buildRateLimitUrl(HttpClientType.OK_HTTP2, "v1")); + Assertions.assertEquals(HttpStatus.SC_OK, result.getCode()); + result = doGet(buildRateLimitUrl(HttpClientType.OK_HTTP3, "v1")); + Assertions.assertEquals(HttpStatus.SC_OK, result.getCode()); + result = doGet(buildRateLimitUrl(HttpClientType.HTTP_URL_CONNECTION, "v1")); + Assertions.assertEquals(HttpStatus.SC_OK, result.getCode()); + } + + // Test the case where the probability is 50% + testRateLimitProbability(buildRateLimitUrl(HttpClientType.HTTP_CLIENT, "v2"), 30, 70); + testRateLimitProbability(buildRateLimitUrl(HttpClientType.OK_HTTP2, "v2"), 30, 70); + testRateLimitProbability(buildRateLimitUrl(HttpClientType.OK_HTTP3, "v2"), 30, 70); + testRateLimitProbability(buildRateLimitUrl(HttpClientType.HTTP_URL_CONNECTION, "v2"), 30, 70); + + // Test the case where the probability is 100% + testRateLimitProbability(buildRateLimitUrl(HttpClientType.HTTP_CLIENT, "v3"), 90, 104); + testRateLimitProbability(buildRateLimitUrl(HttpClientType.OK_HTTP2, "v2"), 90, 104); + testRateLimitProbability(buildRateLimitUrl(HttpClientType.OK_HTTP3, "v2"), 90, 104); + testRateLimitProbability(buildRateLimitUrl(HttpClientType.HTTP_URL_CONNECTION, "v2"), 90, 104); + + // Test token filling + Thread.sleep(10000); + Result result = doGet(buildRateLimitUrl(HttpClientType.HTTP_CLIENT, "v2")); + Assertions.assertEquals(HttpStatus.SC_OK, result.getCode()); + result = doGet(buildRateLimitUrl(HttpClientType.HTTP_URL_CONNECTION, "v2")); + Assertions.assertEquals(HttpStatus.SC_OK, result.getCode()); + result = doGet(buildRateLimitUrl(HttpClientType.OK_HTTP2, "v2")); + Assertions.assertEquals(HttpStatus.SC_OK, result.getCode()); + result = doGet(buildRateLimitUrl(HttpClientType.OK_HTTP3, "v2")); + Assertions.assertEquals(HttpStatus.SC_OK, result.getCode()); + } + + private static void testRateLimitProbability(String url, int minFailureCount, int maxFailureCount) { + Result result; + int requestFailureCount = 0; + for (int i = 0; i < 104; i++) { + result = doGet(url); + if (result.getCode() != HttpStatus.SC_OK) { + requestFailureCount++; + } + System.out.println(result.getData()); + } + System.out.println(requestFailureCount); + Assertions.assertTrue(requestFailureCount >= minFailureCount && requestFailureCount <= maxFailureCount); + } + + private static void testRequestFailureCount(String url) { + long start = System.currentTimeMillis(); + Result result = doGet(url); + System.out.println(System.currentTimeMillis() - start); + Assertions.assertEquals(HttpStatus.SC_OK, result.getCode()); + Assertions.assertEquals(3, getRequestCount()); + resetRequestCount(); + } + + private static void testFaultProbability(String url, int minFailureCount, int maxFailureCount, int sleepTime) + throws InterruptedException { + final AtomicInteger requestFailureCount = new AtomicInteger(); + for (int i = 0; i < 50; i++) { + EXECUTOR_SERVICE.execute(() -> { + Result result = doGet(url); + if (result.getCode() != HttpStatus.SC_OK) { + requestFailureCount.incrementAndGet(); + } + }); + } + Thread.sleep(sleepTime); + int count = requestFailureCount.get(); + Assertions.assertTrue(count >= minFailureCount && count <= maxFailureCount); + } + + private static void resetRequestCount() { + // The server has two instances, and it needs to be called twice + doGet("http://127.0.0.1:8080/flowControl/testHttpClient?host=spring-server&version=v1&path=reset"); + doGet("http://127.0.0.1:8080/flowControl/testHttpClient?host=spring-server&version=v1&path=reset"); + } + + private static int getRequestCount() { + // The server has two instances, and it needs to be called twice + Result result = doGet("http://127.0.0.1:8080/flowControl/testHttpClient?host=spring-server&version=v1&path=getRequestCount"); + int requestFailureCount = Integer.parseInt(result.getData().toString()); + result = doGet("http://127.0.0.1:8080/flowControl/testHttpClient?host=spring-server&version=v1&path=getRequestCount"); + return requestFailureCount + Integer.parseInt(result.getData().toString()); + } + + + private static Result doGet(String url) { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(CONNECT_TIMEOUT) + .setSocketTimeout(SOCKET_TIMEOUT).build(); + HttpGet httpGet = new HttpGet(url); + httpGet.setConfig(requestConfig); + try (CloseableHttpResponse response = httpClient.execute(httpGet)) { + return JSONObject.parseObject(EntityUtils.toString(response.getEntity()), Result.class); + } + } catch (IOException e) { + return new Result(HttpStatus.SC_INTERNAL_SERVER_ERROR, e.getMessage(), null); + } + } + + private static String buildRateLimitUrl(HttpClientType clientType, String version) { + return buildUrl(clientType, version, "&path=testRateLimit"); + } + + private static String buildInstanceCircuitBreakerUrl(HttpClientType clientType, String version) { + return buildUrl(clientType, version, "&path=testInstanceCircuitBreaker"); + } + + private static String buildRequestCircuitBreakerUrl(HttpClientType clientType, String version) { + return buildUrl(clientType, version, "&path=testRequestCircuitBreaker"); + } + + private static String buildGateWayErrorUrl(HttpClientType clientType, String version) { + return buildUrl(clientType, version, "&path=testGateWayError"); + } + + private static String buildFaultUrl(HttpClientType clientType, String version) { + return buildUrl(clientType, version, "&path=testFault"); + } + + private static String buildUrl(HttpClientType clientType, String version, String path) { + if (clientType == HttpClientType.OK_HTTP2) { + return OKHTTP_URL_PREFIX + version + path; + } + if (clientType == HttpClientType.OK_HTTP3) { + return OKHTTP3_URL_PREFIX + version + path; + } + if (clientType == HttpClientType.HTTP_CLIENT) { + return HTTP_CLIENT_URL_PREFIX + version + path; + } + if (clientType == HttpClientType.HTTP_URL_CONNECTION) { + return HTTP_URL_CONNECTION_URL_PREFIX + version + path; + } + return ""; + } +}