bf6813e71d
生产网关在 /api 前丢掉 Upgrade 头,wss://…/chat/{userId} 到不了后端,
端上一直卡在「正在建立 WebSocket 连接…」。
- 新增 ApiChatWebSocketServer(/api/chat/{userId}),与老端点共用会话表;
会话表改存 Session 而不是端点实例,两条路径都能收到推送
- 登记会话时关闭空闲超时(Tomcat 默认 20s,AI 首字慢时会被掐断),
并补 @OnMessage 丢弃心跳(缺了它容器会因不支持的文本帧直接断连)
- send() 找不到 WS 会话时改投 ChatStreamRegistry,把帧送到 HTTP SSE 连接,
推送侧仍只有 sendMessage 一个入口,三千多次调用点不用改
- prod 放开 com.gxwebsoft.ai.config 到 INFO:只保留连接/断开,推送路径仍静默
- 补 ChatStreamRegistryTest / WebSocketEndpointPathTest / AiControllerSseFrameTest
顺带补上 31eb75c 漏加的 ChatStreamRegistry.java:AiController 已经 import 它,
缺文件时 HEAD 编译不过。
101 lines
3.5 KiB
Java
101 lines
3.5 KiB
Java
package com.gxwebsoft.ai.config;
|
|
|
|
import org.junit.jupiter.api.Test;
|
|
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.List;
|
|
import java.util.UUID;
|
|
import java.util.function.Consumer;
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
|
/**
|
|
* HTTP 流式通道(SSE)是生产上唯一能用的推送路径:网关在 {@code /api} 前丢掉
|
|
* WebSocket 的 {@code Upgrade} 头,WS 握手永远到不了 Tomcat。
|
|
*
|
|
* <p>因此这张登记表的几个行为是硬约束:没人接时静默、有人接时逐字转发、
|
|
* 重复登记后者覆盖前者、写失败要把死掉的 sink 摘掉
|
|
* (否则后续消息会一直往已经断开的长连接里写)。
|
|
*
|
|
* <p>每个用例用独立 userId,避免静态表互相串味,也就不需要清理。
|
|
*/
|
|
class ChatStreamRegistryTest {
|
|
|
|
private static String newUser() {
|
|
return "test-" + UUID.randomUUID();
|
|
}
|
|
|
|
@Test
|
|
void 没有登记通道时返回false() {
|
|
assertFalse(ChatStreamRegistry.emit(newUser(), "{}"));
|
|
}
|
|
|
|
@Test
|
|
void 登记后能收到原文() {
|
|
String user = newUser();
|
|
List<String> received = new ArrayList<>();
|
|
ChatStreamRegistry.bind(user, received::add);
|
|
|
|
assertTrue(ChatStreamRegistry.emit(user, "{\"answer\":\"你好\"}"));
|
|
assertEquals(Collections.singletonList("{\"answer\":\"你好\"}"), received);
|
|
}
|
|
|
|
@Test
|
|
void 重复登记时后者覆盖前者() {
|
|
String user = newUser();
|
|
List<String> first = new ArrayList<>();
|
|
List<String> second = new ArrayList<>();
|
|
ChatStreamRegistry.bind(user, first::add);
|
|
ChatStreamRegistry.bind(user, second::add);
|
|
|
|
ChatStreamRegistry.emit(user, "x");
|
|
|
|
assertTrue(first.isEmpty(), "被覆盖的旧连接不该再收到消息");
|
|
assertEquals(Collections.singletonList("x"), second);
|
|
}
|
|
|
|
@Test
|
|
void 解绑只摘自己的那个通道() {
|
|
String user = newUser();
|
|
List<String> first = new ArrayList<>();
|
|
List<String> second = new ArrayList<>();
|
|
Consumer<String> firstSink = first::add;
|
|
ChatStreamRegistry.bind(user, firstSink);
|
|
ChatStreamRegistry.bind(user, second::add);
|
|
|
|
// 旧连接晚到的 finally 块:不能把新连接顶掉
|
|
ChatStreamRegistry.unbind(user, firstSink);
|
|
|
|
assertTrue(ChatStreamRegistry.emit(user, "still-alive"));
|
|
assertEquals(Collections.singletonList("still-alive"), second);
|
|
}
|
|
|
|
@Test
|
|
void 写入失败后摘掉通道且不向上抛() {
|
|
String user = newUser();
|
|
ChatStreamRegistry.bind(user, payload -> {
|
|
throw new IllegalStateException("客户端已断开");
|
|
});
|
|
|
|
assertFalse(ChatStreamRegistry.emit(user, "boom"));
|
|
// 失败过的 sink 已被摘掉,下一次 emit 直接返回 false
|
|
assertFalse(ChatStreamRegistry.emit(user, "boom"));
|
|
}
|
|
|
|
@Test
|
|
void 没有WebSocket会话时推送落到流式通道() throws IOException {
|
|
String user = newUser();
|
|
List<String> received = new ArrayList<>();
|
|
ChatStreamRegistry.bind(user, received::add);
|
|
|
|
// 生产就是这样:SESSIONS 里没有这个用户,send 必须改投 SSE
|
|
WebSocketServer.send(user, "{\"answer\":\"fallback\"}");
|
|
|
|
assertEquals(Collections.singletonList("{\"answer\":\"fallback\"}"), received);
|
|
}
|
|
}
|