feat(W46): 发送路由带上原因,并加一行 CyreneRoute 诊断日志
为什么要把「原因」也带出来 - 结果只有 Ws / Rest / Queue 三档,而「为什么是这一档」在设备上完全看不见。W45 那轮真机回归 就卡在这里:那条消息进了补发队列、网络侧一条请求都没有,而「零请求 + 进队列」在判据里只可能 来自 Queue 那一档;可顶栏同时显示「重连中…」。两者都只是结论,事后无法分辨「判据读到 Offline」与「判据读了别的值」。 - 于是把判据抽成 sendRouteDecisionFor(wsDispatched, connection) -> (route, reason):reason 就是 原来那三条规则各自的分支标识(FrameDispatched / ConnectionOffline / ConnectionNotReady)。 sendRouteFor 现在读它的 route,所以两处不可能分叉。判据、优先级、行为逐字未改。 那一行日志(tag = CyreneRoute,Log.d,只在 DEBUG 构建打) - 打点:ChatViewModel.sendThroughLadder 一进入就一笔,覆盖阶梯的三个分支(帧交出去 / 试 REST / 直接进队列),一行 key=value、空格分隔: wsDispatched=... socket=<ChatSocket.state.value,权威值> mirror=<顶栏读的那个镜像> wsObject=<present|absent> route=... reason=... outcome=... fromQueue=... - socket 与 mirror 必须同时打出来:mirror 是 collect 过来的一格,只会滞后、不会超前,两者不一致 就说明顶栏那一刻显示的是上一格 —— 这正是「顶栏重连中…」与「这条进了队列」能同时出现的形态。 - 这是开发期诊断仪,不是产品行为:字段增删不构成兼容问题、不为它写测试(要钉的是纯函数那张全表)。 不打 token / 请求体 / 正文 / localId。 顺带(同一轮的可观测性) - ChatSocket 新增只读属性 hasLiveSocket(webSocket != null),只喂日志,不是产品逻辑的输入。 - 复核 WsState.Offline 的写点:ChatSocket 里只有两处(runConnectionLoop 的 ConfigMissing,以及 endWithAuthFailure),close() 写的是 Idle;非 401 的握手失败(代理回 503)走 Closed -> Reconnecting,进不了这一档。 测试 - SendFallbackTest 新增两例:五档连接状态 × wsDispatched 真假的全表逐个钉住结果 + 原因,并钉住 sendRouteFor 与判定同值;另一个钉住三个 reason 都可达、没有第四种组合。全量 1155 例, 0 失败 0 错误。
This commit is contained in:
1 parent
b8399b5949
commit
d546f31755
4 files changed
+189
-9
No files matched your search
@@ -26,7 +26,7 @@ import top.yeij.cyrene.domain.chat.SendChannel
|
||||
* 现在的判据里**没有任何探测输入**:路由只看「帧交出去了没有 + 这一刻的连接状态」两个事实,
|
||||
* REST 那一次的产物由 [restAttemptLanding] 判落点。
|
||||
*
|
||||
* ## 本测试钉住六件事
|
||||
* ## 本测试钉住七件事
|
||||
* 1. **优先级**([sendRouteFor]):帧交出去了 ⇒ 走 WS(连接状态不再参与);未就绪且不是
|
||||
* [WsState.Offline] ⇒ 直接试 REST;[WsState.Offline] ⇒ 进队列、**一个请求都不发**。
|
||||
* 2. **收下了就不进队列**:受理 / 续接排队 / 幂等重复([ChatAck.isAccepted] 那一族)⇒
|
||||
@@ -36,6 +36,9 @@ import top.yeij.cyrene.domain.chat.SendChannel
|
||||
* 4. **服务端给过裁决 ⇒ 回滚**:拒绝的回执(`409` / `500` / `503` / `400`)与 401 / 配置类异常。
|
||||
* 5. **两个超时**:连接超时落在 2~3 秒(无网时的等待上界),整通上限另设且不许拉长。
|
||||
* 6. **这条阶梯到不了「REST 专属」的消息**(有图 / 有附件一律在进入阶梯之前就被判成 REST)。
|
||||
* 7. **「为什么是这一档」**([sendRouteDecisionFor],W46):五档连接状态 × `wsDispatched` 真假那
|
||||
* 十个格子逐个钉住**结果 + 原因**。结果是给代码看的,原因是给**设备上的日志**看的——真机上
|
||||
* 看不见判据读到的是哪个连接状态,而那正是排查「发送为什么进了队列」时唯一要分的东西。
|
||||
*/
|
||||
class SendFallbackTest {
|
||||
|
||||
@@ -124,6 +127,69 @@ class SendFallbackTest {
|
||||
}
|
||||
}
|
||||
|
||||
// —— 一之二、判据的「原因」(W46 可观测性) ——
|
||||
|
||||
@Test
|
||||
fun `the whole table carries which rule decided it`() {
|
||||
// 结果只有三档,而「**为什么**是这一档」在真机上看不见:W45 那轮真机回归里,那条消息进了
|
||||
// 队列、网络侧一条请求都没有,而「零请求 + 进队列」在判据里只可能来自 Queue;可顶栏同时
|
||||
// 显示「重连中…」。只看结果分不出「判据读到 Offline」与「判据读了别的值」,所以要有个地方
|
||||
// 能把依据本身取出来——同时打给 logcat(tag `CyreneRoute`)与这条全表。
|
||||
//
|
||||
// 表还是五档 × 帧交出去了没有那十个格子,只是每一格多钉一个原因;并顺手钉住
|
||||
// `sendRouteFor` 就是它的 `.route`(两处**不可能**分叉,日志里那一档与真正走的那一档必然
|
||||
// 是同一次判定的结果)。
|
||||
val table = listOf(
|
||||
Triple(true, WsState.Idle, SendRouteDecision(SendRoute.Ws, SendRouteReason.FrameDispatched)),
|
||||
Triple(true, WsState.Connecting, SendRouteDecision(SendRoute.Ws, SendRouteReason.FrameDispatched)),
|
||||
Triple(true, WsState.Reconnecting, SendRouteDecision(SendRoute.Ws, SendRouteReason.FrameDispatched)),
|
||||
Triple(true, WsState.Connected, SendRouteDecision(SendRoute.Ws, SendRouteReason.FrameDispatched)),
|
||||
Triple(true, WsState.Offline, SendRouteDecision(SendRoute.Ws, SendRouteReason.FrameDispatched)),
|
||||
Triple(false, WsState.Idle, SendRouteDecision(SendRoute.Rest, SendRouteReason.ConnectionNotReady)),
|
||||
Triple(false, WsState.Connecting, SendRouteDecision(SendRoute.Rest, SendRouteReason.ConnectionNotReady)),
|
||||
Triple(false, WsState.Reconnecting, SendRouteDecision(SendRoute.Rest, SendRouteReason.ConnectionNotReady)),
|
||||
Triple(false, WsState.Connected, SendRouteDecision(SendRoute.Rest, SendRouteReason.ConnectionNotReady)),
|
||||
// 唯一能在**一个请求都不发**的前提下进队列的那一格(也正是排查时要认出来的那一格)。
|
||||
Triple(false, WsState.Offline, SendRouteDecision(SendRoute.Queue, SendRouteReason.ConnectionOffline)),
|
||||
)
|
||||
table.forEach { (dispatched, connection, expected) ->
|
||||
val decision = sendRouteDecisionFor(wsDispatched = dispatched, connection = connection)
|
||||
assertEquals(
|
||||
"dispatched=$dispatched / $connection 的结果与依据",
|
||||
expected,
|
||||
decision,
|
||||
)
|
||||
assertEquals(
|
||||
"dispatched=$dispatched / $connection 上 sendRouteFor 必须与判定同值",
|
||||
expected.route,
|
||||
sendRouteFor(wsDispatched = dispatched, connection = connection),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `every reason is reachable and each rule owns exactly one`() {
|
||||
// 原因这一栏与三条规则**一一对应**:每一个取值都必须能在全表里被取到(将来加了规则却没人
|
||||
// 标出来,这条会红),而且「结果 + 原因」不会出现两种组合指向同一个结果这种糊法。
|
||||
val reachable = listOf(true, false).flatMap { dispatched ->
|
||||
WsState.entries.map { sendRouteDecisionFor(dispatched, it).reason }
|
||||
}.toSet()
|
||||
assertEquals(SendRouteReason.entries.toSet(), reachable)
|
||||
|
||||
val combos = listOf(true, false).flatMap { dispatched ->
|
||||
WsState.entries.map { sendRouteDecisionFor(dispatched, it) }
|
||||
}.toSet()
|
||||
// 十格只有三组(结果,原因):`Ws` 一格、`Rest` 一格、`Queue` 一格,没有第四种组合。
|
||||
assertEquals(
|
||||
setOf(
|
||||
SendRouteDecision(SendRoute.Ws, SendRouteReason.FrameDispatched),
|
||||
SendRouteDecision(SendRoute.Rest, SendRouteReason.ConnectionNotReady),
|
||||
SendRouteDecision(SendRoute.Queue, SendRouteReason.ConnectionOffline),
|
||||
),
|
||||
combos,
|
||||
)
|
||||
}
|
||||
|
||||
// —— 二、直接试 REST 的落点 ——
|
||||
|
||||
@Test
|
||||
|
||||
Reference in new issue
Block a user