Skip to content

poc: java sdk support multiple ip#37

Draft
Sczlog wants to merge 1 commit into
masterfrom
multiple-ip
Draft

poc: java sdk support multiple ip#37
Sczlog wants to merge 1 commit into
masterfrom
multiple-ip

Conversation

@Sczlog
Copy link
Copy Markdown
Collaborator

@Sczlog Sczlog commented May 8, 2026

  • introduce active passivce client
  • fix imcompatibile surefire plugin for testng 7.4.0

测试用代码:

package com.smartx.tower.manual;

import com.smartx.tower.ActivePassiveApiClient;
import com.smartx.tower.ActivePassiveFailoverStrategy;
import com.smartx.tower.ApiException;
import com.smartx.tower.ClientUtil;
import com.smartx.tower.api.UserApi;
import com.smartx.tower.api.VmApi;
import com.smartx.tower.model.GetVmsRequestBody;
import com.smartx.tower.model.LoginInput;
import com.smartx.tower.model.UserSource;
import com.smartx.tower.model.Vm;
import com.smartx.tower.model.WithTaskLoginResponse;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;

public class ActivePassiveApiClientMainSanitized {
    private static final String BASE_PATHS_ENV = "CLOUDTOWER_ACTIVE_PASSIVE_BASE_PATHS";
    private static final String USERNAME_ENV = "CLOUDTOWER_USERNAME";
    private static final String PASSWORD_ENV = "CLOUDTOWER_PASSWORD";
    private static final String USER_SOURCE_ENV = "CLOUDTOWER_USER_SOURCE";
    private static final ActivePassiveFailoverStrategy FAILOVER_STRATEGY =
            ActivePassiveFailoverStrategy.DEFAULT;
    private static final int PROBE_TIMEOUT_MILLIS = 10_000;
    private static final int REQUEST_INTERVAL_SECONDS = 10;
    private static final int VM_FIRST = 10;

    public static void main(String[] args) throws Exception {
        List<String> basePaths = readBasePaths();
        ActivePassiveApiClient client =
                new ActivePassiveApiClient(FAILOVER_STRATEGY, basePaths)
                        .setProbeTimeoutMillis(PROBE_TIMEOUT_MILLIS)
                        .setVerifyingSsl(false);

        login(client, readRequiredEnv(USERNAME_ENV), readRequiredEnv(PASSWORD_ENV), readUserSource());

        while (true) {
            runGetVms(client, VM_FIRST);
            Thread.sleep(REQUEST_INTERVAL_SECONDS * 1000L);
        }
    }

    private static void login(
            ActivePassiveApiClient client, String username, String password, UserSource userSource)
            throws ApiException {
        WithTaskLoginResponse response =
                new UserApi(client)
                        .login(
                                new LoginInput()
                                        .username(username)
                                        .password(password)
                                        .source(userSource));
        if (response == null || response.getData() == null || response.getData().getToken() == null) {
            throw new ApiException("Login response does not contain token");
        }
        ClientUtil.login(response.getData().getToken(), client);
    }

    private static void runGetVms(ActivePassiveApiClient client, int first) {
        try {
            List<Vm> vms = new VmApi(client).getVms(new GetVmsRequestBody().first(first));
            int count = vms == null ? 0 : vms.size();
            System.out.printf(
                    "%s request success active_base_path=%s vm_count=%d%n",
                    OffsetDateTime.now(), client.getCurrentActiveBaseUrl(), count);
        } catch (ApiException e) {
            System.out.printf(
                    "%s request failed active_base_path=%s code=%d error=%s%n",
                    OffsetDateTime.now(),
                    client.getCurrentActiveBaseUrl(),
                    e.getCode(),
                    e.getMessage());
            if (e.getResponseBody() != null) {
                System.out.println(e.getResponseBody());
            }
        }
    }

    private static List<String> readBasePaths() {
        String value = readRequiredEnv(BASE_PATHS_ENV);
        String[] rawBasePaths = value.split(",");
        List<String> basePaths = new ArrayList<String>();
        for (String rawBasePath : rawBasePaths) {
            String basePath = rawBasePath.trim();
            if (!basePath.isEmpty()) {
                basePaths.add(basePath);
            }
        }
        if (basePaths.isEmpty()) {
            throw new IllegalArgumentException(
                    BASE_PATHS_ENV + " must contain at least one endpoint base path");
        }
        return basePaths;
    }

    private static UserSource readUserSource() {
        String value = System.getenv(USER_SOURCE_ENV);
        if (value == null || value.trim().isEmpty()) {
            return UserSource.LOCAL;
        }
        return UserSource.fromValue(value.trim());
    }

    private static String readRequiredEnv(String name) {
        String value = System.getenv(name);
        if (value == null || value.trim().isEmpty()) {
            throw new IllegalArgumentException("Missing required environment variable: " + name);
        }
        return value.trim();
    }
}

@Sczlog
Copy link
Copy Markdown
Collaborator Author

Sczlog commented May 8, 2026

自测结果:

2026-05-08T18:25:11.415395+08:00 request success active_base_path=https://172.21.152.95/v2/api vm_count=10
2026-05-08T18:25:21.610065+08:00 request success active_base_path=https://172.21.152.95/v2/api vm_count=10
2026-05-08T18:25:31.772132+08:00 request success active_base_path=https://172.21.152.95/v2/api vm_count=10
2026-05-08T18:25:41.851065+08:00 request failed active_base_path=null code=0 error=active-passive discover found no active endpoint
2026-05-08T18:25:51.900412+08:00 request failed active_base_path=null code=0 error=active-passive discover found no active endpoint
2026-05-08T18:26:01.950912+08:00 request failed active_base_path=null code=0 error=active-passive discover found no active endpoint
2026-05-08T18:26:12.007113+08:00 request failed active_base_path=null code=0 error=active-passive discover found no active endpoint: https://172.21.152.75/v2/api: probe active-passive returned unexpected status
2026-05-08T18:26:23.347032+08:00 request failed active_base_path=https://172.21.152.75/v2/api code=502 error=
<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
<hr><center>openresty</center>
</body>
</html>

2026-05-08T18:26:39.411560+08:00 request failed active_base_path=https://172.21.152.75/v2/api code=502 error=
<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
<hr><center>openresty</center>
</body>
</html>

2026-05-08T18:26:49.479195+08:00 request failed active_base_path=https://172.21.152.75/v2/api code=403 error=
{"path":"/get-vms","operationName":"vmsConnectionExternal","status":403,"message":"Unknown error on operation vmsConnectionExternal","stack":"Error: Unknown error on operation vmsConnectionExternal\n    at /app/lib/libs/graphql-client.js:25:19\n    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n    at async GeneratedQueryController.getVms (/app/lib/modules/generated/query-controller.js:8683:24)\n    at async runAuthenticationMiddleware (/app/lib/generated/routes.js:43678:17)\n    at async /app/lib/server.js:122:9\n    at async cors (/app/node_modules/@koa/cors/index.js:61:32)\n    at async bodyParser (/app/node_modules/koa-bodyparser/index.js:95:5)\n    at async errorHandler (/app/lib/middleware/error-handler.js:78:9)","props":{}}
2026-05-08T18:26:59.522744+08:00 request failed active_base_path=https://172.21.152.75/v2/api code=403 error=
{"path":"/get-vms","operationName":"vmsConnectionExternal","status":403,"message":"Unknown error on operation vmsConnectionExternal","stack":"Error: Unknown error on operation vmsConnectionExternal\n    at /app/lib/libs/graphql-client.js:25:19\n    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n    at async GeneratedQueryController.getVms (/app/lib/modules/generated/query-controller.js:8683:24)\n    at async runAuthenticationMiddleware (/app/lib/generated/routes.js:43678:17)\n    at async /app/lib/server.js:122:9\n    at async cors (/app/node_modules/@koa/cors/index.js:61:32)\n    at async bodyParser (/app/node_modules/koa-bodyparser/index.js:95:5)\n    at async errorHandler (/app/lib/middleware/error-handler.js:78:9)","props":{}}
2026-05-08T18:27:13.096539+08:00 request success active_base_path=https://172.21.152.75/v2/api vm_count=10

@Sczlog Sczlog marked this pull request as draft May 8, 2026 10:28
Comment thread pom.xml
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<version>3.5.5</version>
Copy link
Copy Markdown
Collaborator Author

@Sczlog Sczlog May 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testng 版本和对应版本的 surefire 不兼容,以前都是 ide 插件跑的,这次用 mvn 跑发现了问题,就升级了一下,另外匹配升级了 mvn 的最小版本,这些都是开发时的依赖,不影响构建产物

Copy link
Copy Markdown

@fanyang89 fanyang89 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这版先别合,重复造轮子太多,而且不是“多写点代码”这么简单,是把 OkHttp/JDK 已经保证好的语义自己重写了一遍,然后还写坏了。

主要问题:

  • src/main/java/com/smartx/tower/ActivePassiveApiClient.java:664 开始手写 okhttp3.Call,这就是在复刻 OkHttp 的 RealCall。SDK 不应该自己实现一套 execute/enqueue/cancel/timeout/clone。这个需求更适合放到 OkHttp Interceptor 或一个很薄的 endpoint resolver 里,最终仍然让 OkHttp 创建真实 Call

  • src/main/java/com/smartx/tower/ActivePassiveApiClient.java:704enqueue() 直接拿 dispatcher().executorService().execute(...) 跑任务,绕开了 OkHttp Dispatcher.enqueue/finished 的请求计数、maxRequestsmaxRequestsPerHost、idle callback 等机制。这不是优化,这是把 OkHttp 的调度器拆了重新糊一个残版。

  • src/main/java/com/smartx/tower/ActivePassiveApiClient.java:731cancel() 只改了一个 AtomicBoolean,已经发出去的 probe/request 根本取消不了。真实 OkHttp Call.cancel() 会取消 socket/stream,这里用户以为取消了,实际请求还在跑。

  • src/main/java/com/smartx/tower/ActivePassiveApiClient.java:747timeout() 返回的是一个新建 Timeout,没有绑定到任何真实 call,也不会约束正在执行的请求。OkHttp 已经有 call timeout,这里返回一个摆设会误导调用方。

  • src/main/java/com/smartx/tower/ActivePassiveApiClient.java:114219 这一大坨 setter 只是 super.xxx(...); return this;,纯样板代码。为了链式调用窄化返回类型复制 20 多个方法不值,保留真正有行为差异的 override 就行。

  • src/main/java/com/smartx/tower/ApiClient.java:810 明确写着 OpenAPI Generator 生成、不要手改。这个 PR 直接往 ApiClient 里塞 active/passive probe,下次重新生成 SDK 就没了。active/passive 逻辑应该放到非生成类里,或者改 generator/template,而不是手改生成产物。

  • src/main/java/com/smartx/tower/ApiClient.java:1134src/main/java/com/smartx/tower/ActivePassiveApiClient.java:524 各写了一份 apiException()closeQuietly() 也重复写了两份。已有 handleResponse()/Response.close() 可以复用的话就复用,真需要 helper 也抽一处,别复制粘贴。

  • src/main/java/com/smartx/tower/ActivePassiveApiClient.java:550 手写 join() 没必要,JDK 早就有 String.join(delimiter, values)src/main/java/com/smartx/tower/ActivePassiveApiClient.java:542trimTrailingSlash() 还带 "http://x" 这种魔法字符串,URL 规范化交给 HttpUrl/URI,别自己抠字符串。

  • src/main/java/com/smartx/tower/ActivePassiveApiClient.java:384390 在 synchronized 里做网络探测。一个慢 endpoint 就能把所有并发请求堵在锁后面,最坏是 endpoint 数量 * probe timeout。锁只应该保护状态读写,不应该包住网络 I/O。

  • src/test/java/com/smartx/tower/ActivePassiveApiClientTest.java:217 还在用字符串字面量 "UTF-8",JDK 有 StandardCharsets.UTF_8。小问题,但和上面的问题是一类:标准库有的东西别手搓。

建议重做:不要加一个 761 行的代理 client 去复刻 OkHttp。把“发现 active endpoint / 收到 307 后清缓存并重试一次”做成小的 resolver/interceptor,尽量让原来的 ApiClient.buildRequest() 和 OkHttp 的真实 Call 继续工作。这样代码量会少很多,也不会破坏取消、超时、异步调度这些基础语义。

@Sczlog
Copy link
Copy Markdown
Collaborator Author

Sczlog commented May 12, 2026

@fanyang89

修改了请求的发送模式,并通过 interceptor 来进行切主选主

override 很多直接调用 super 的代码,主要是为了保证 builder 模式可以正确返回指定的 classname

- introduce active passivce client
- fix imcompatibile surefire plugin for testng 7.4.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants