Files
ykxmz/_build/add_fail_threshold.py
xmz7891 25e7780272 单次请求失败不再立刻判离线,改成连续 3 次
实测单个请求有 2~4% 的偶发失败(4G 丢包打在一条隧道连接上,
或者那条连接正在重连)。原逻辑是一失败就 online = false,
于是界面上会闪一下已断开 —— 用户点按钮时正好撞上那一闪,
看起来就像又连不上了。

而链路其实 1.5~15 秒就自己恢复(客户端本来就有退避重试)。

改成连续失败 3 次才判离线(手表至少 4.5 秒、电脑至少 6 秒):
  * 单次抖动完全不影响界面
  * 真的断了仍然如实显示

这不是掩盖问题,而是把是否可用的判断从一次采样改成连续采样 ——
单点采样在有丢包的链路上噪声太大。

两边同步改:
  android .../RelayTransport.java   failStreak + OFFLINE_AFTER
  windows/lanremote/relay.py        _fail_streak + OFFLINE_AFTER

顺带记录 PC 端基准(真实用户视角,之前都是在设备上量的,
那等于把设备 4G 算了两遍):
  公网可用率 98%   耗时 中位 911  P90 1889  P99 23868 ms
  长轮询     5/6   耗时 60.8~62.1 秒
对比设备端(中位 1344 / P90 2563)—— 用户实际体验比设备端量出来的好。
2026-10-07 16:16:50 +08:00

207 lines
7.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
r'''单次请求失败不再立刻判"离线",改成连续失败 3 次才算。
== 为什么 ==
实测单个请求会有 2~4% 的偶发失败(4G 丢包打在一条隧道连接上,
或者那条连接正在重连)。原来的逻辑是**一失败就 online = false**,
于是:
手表/电脑的界面上会闪一下"已断开"
用户点按钮时正好撞上那一闪 -> 看起来像"又连不上了"
而实际上链路 1.5~15 秒后就自己恢复了(客户端本来就有退避重试)。
改成"连续失败 3 次才判离线",效果是:
* 单次抖动完全不影响界面显示
* 真的断了(连续 3 次,至少 4.5 秒都没通)仍然会如实显示离线
这不是掩盖问题,而是把"是否可用"的判断从"一次采样"改成
"连续采样"—— 单点采样在有丢包的链路上噪声太大。
== 两边都要改 ==
1. android/src/com/lanremote/watch/RelayTransport.java (手表轮询)
2. windows/lanremote/relay.py (电脑轮询)
'''
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
BOM = b"\xef\xbb\xbf"
# ---------------------------------------------------------------- 手表
WATCH = ROOT / "android/src/com/lanremote/watch/RelayTransport.java"
WATCH_CONST_OLD = " private static final int RETRY_MAX_MS = 15000;"
WATCH_CONST_NEW = """ private static final int RETRY_MAX_MS = 15000;
/**
* 连续失败几次才判定"离线"。
*
* <p>单次失败就报离线太敏感:4G 链路上随手就有 2~4% 的偶发失败,
* 而链路通常一两秒就自己恢复,界面却已经闪了一下"已断开"。
* 改成连续 3 次(至少 4.5 秒都不通)才判离线 ——
* 真的断了仍然会如实显示。
*/
private static final int OFFLINE_AFTER = 3;"""
WATCH_FIELD_OLD = " private Thread pollThread;"
WATCH_FIELD_NEW = """ private Thread pollThread;
/** 连续失败次数(成功一次就清零)。 */
private volatile int failStreak;"""
# 401/403 分支
W_401_OLD = """ if (result.code == 401 || result.code == 403) {
// 会话过期(中转重启等)—— 这种可以自动重连
online = false;
lastError = "会话已失效,正在重新接入";"""
W_401_NEW = """ if (result.code == 401 || result.code == 403) {
// 会话过期(中转重启等)—— 这种可以自动重连。
// 会话失效是明确的状态,直接标离线。
online = false;
lastError = "会话已失效,正在重新接入";"""
# 非 200 分支
W_OTHER_OLD = """ if (result.code != 200) {
online = false;
lastError = "轮询返回 " + result.code;
sleep(backoff);
backoff = Math.min(RETRY_MAX_MS, backoff * 2);
continue;
}
online = true;
lastError = "";
backoff = RETRY_MIN_MS;"""
W_OTHER_NEW = """ if (result.code != 200) {
// 偶发的 502/504 不算断线,连续几次才算
failStreak++;
if (failStreak >= OFFLINE_AFTER) {
online = false;
}
lastError = "轮询返回 " + result.code;
sleep(backoff);
backoff = Math.min(RETRY_MAX_MS, backoff * 2);
continue;
}
online = true;
failStreak = 0;
lastError = "";
backoff = RETRY_MIN_MS;"""
# 异常分支
W_EXC_OLD = """ } catch (Exception exc) {
if (closed) {
return;
}
online = false;
lastError = "轮询出错:" + exc.getClass().getSimpleName();
sleep(backoff);
backoff = Math.min(RETRY_MAX_MS, backoff * 2);
}"""
W_EXC_NEW = """ } catch (Exception exc) {
if (closed) {
return;
}
// 连接超时/重置很常见(隧道在重连),连续几次才算真断
failStreak++;
if (failStreak >= OFFLINE_AFTER) {
online = false;
}
lastError = "轮询出错:" + exc.getClass().getSimpleName();
sleep(backoff);
backoff = Math.min(RETRY_MAX_MS, backoff * 2);
}"""
# ---------------------------------------------------------------- 电脑
AGENT = ROOT / "windows/lanremote/relay.py"
A_CONST_OLD = "RETRY_MAX = 20.0"
A_CONST_NEW = """RETRY_MAX = 20.0
# 连续失败几次才判定"离线"。
# 单次失败就报离线太敏感:4G 链路上有 2~4% 的偶发失败,
# 而链路通常一两秒就自己恢复,界面却已经闪了一下"已断开"。
# 连续 3 次(至少 6 秒都不通)才判离线,真的断了仍然如实显示。
OFFLINE_AFTER = 3"""
A_FAIL_OLD = """ except Exception as exc: # noqa: BLE001
self.online = False
self.last_error = f"{type(exc).__name__}: {exc}"
self._log(f"中转连接出问题:{self.last_error}")"""
A_FAIL_NEW = """ except Exception as exc: # noqa: BLE001
# 偶发的连接超时/重置不算断线,连续几次才算
self._fail_streak = getattr(self, "_fail_streak", 0) + 1
if self._fail_streak >= OFFLINE_AFTER:
self.online = False
self.last_error = f"{type(exc).__name__}: {exc}"
self._log(f"中转连接出问题(第 {self._fail_streak} 次):"
f"{self.last_error}")"""
A_OK_OLD = """ self.online = True
self.last_ok = time.time()
self.last_error = \"\""""
A_OK_NEW = """ self.online = True
self._fail_streak = 0
self.last_ok = time.time()
self.last_error = \"\""""
def patch(text: str, old: str, new: str, label: str) -> tuple:
if old not in text:
if new.strip() and new.splitlines()[-1].strip() in text:
print(" • %s 已经是新值" % label)
return text, True
print(" ✗ 没找到:%s" % label)
return text, False
if text.count(old) != 1:
print(" ! 出现 %d 次:%s" % (text.count(old), label))
return text, False
print(" ✓ %s" % label)
return text.replace(old, new, 1), True
def main() -> int:
ok_all = True
print(" 手表 RelayTransport.java:")
raw = WATCH.read_bytes()
text = raw.decode("utf-8-sig")
for old, new, label in (
(WATCH_CONST_OLD, WATCH_CONST_NEW, "加 OFFLINE_AFTER 常量"),
(WATCH_FIELD_OLD, WATCH_FIELD_NEW, "加 failStreak 字段"),
(W_OTHER_OLD, W_OTHER_NEW, "非 200 用连续计数"),
(W_EXC_OLD, W_EXC_NEW, "异常用连续计数"),
):
text, ok = patch(text, old, new, label)
ok_all = ok_all and ok
if ok_all:
WATCH.write_text(text, encoding="utf-8", newline="\n")
print()
print(" 电脑 relay.py:")
araw = AGENT.read_bytes()
atext = araw.decode("utf-8-sig")
for old, new, label in (
(A_CONST_OLD, A_CONST_NEW, "加 OFFLINE_AFTER 常量"),
(A_FAIL_OLD, A_FAIL_NEW, "异常用连续计数"),
(A_OK_OLD, A_OK_NEW, "成功时清零"),
):
atext, ok = patch(atext, old, new, label)
ok_all = ok_all and ok
if not ok_all:
print()
print(" 有失败项,没写回电脑端")
return 1
AGENT.write_text(atext, encoding="utf-8", newline="\n")
print()
print(" 两边都改好了")
return 0
if __name__ == "__main__":
sys.exit(main())