Files
ykxmz/_build/poll180_patch.py
xmz7891 b2bfa6ffbf 长轮询客户端超时 120 -> 180 秒(实测出现 126.6 秒的长尾)
relay-stability 跑了 30 次真实长轮询,第 1 次耗时 126604ms:

    服务端 hold 是 60 秒
    剩下 66 秒全是网络停顿(4G 抖动 + 重传退避)
    而客户端超时写的是 120000ms
    -> 126.6 > 120,那次会被**误判成失败**,尽管服务端已经正常返回

其余 29 次都是 61~62 秒,很正常。不是 hold 太长,是网络尾巴太长。

为什么放宽超时而不是缩短 hold:
  A. hold 缩到 45 秒、超时保持 120 -> 最坏 111 秒,安全
     但请求数多 33%,而每次请求都要吃隧道 1~2 秒开销
  B. hold 保持 60 秒、超时放宽到 180 -> 最坏 180 秒,安全
选 B —— 这条链路的根本问题是丢包抖动,**请求越少越不容易撞上**。
放宽超时只是"别把慢但成功的请求冤枉掉",不增加任何请求。

客户端已有"连续失败 3 次才判离线"(OFFLINE_AFTER=3),
所以偶尔一次长等待不会让界面闪"已断开"。

超时余量的演进(全是实测逼出来的):
  90 秒  -> 实测 93589ms  不够
  120 秒 -> 实测 126604ms 不够
  180 秒 -> 60 hold + 120 余量

两边同步改:
  windows/lanremote/relay.py                     POLL_TIMEOUT 120 -> 180
  android .../RelayTransport.java                POLL_TIMEOUT_MS 120000 -> 180000
(Java 侧确认 setConnectTimeout 另有 15 秒上限、setReadTimeout 吃这个值,
  没有别的超时会先把请求掐掉。)
2026-10-07 17:36:20 +08:00

132 lines
5.2 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'''客户端长轮询超时 120 -> 180 秒。
== 实测证据 ==
relay-stability 跑了 30 次真实长轮询,其中**第 1 次耗时 126604ms**:
服务端 hold 是 60 秒
剩下 66 秒全是网络停顿(4G 抖动 + 重传退避)
而客户端超时写的是 120000ms
-> 126.6 > 120,那一次会被**误判成失败**,即使服务端已经正常返回了
其余 29 次都是 61~62 秒,很正常。所以不是 hold 太长,是网络尾巴太长。
== 为什么把超时放宽到 180 而不是把 hold 缩短 ==
两个选择:
A. hold 缩到 45 秒,超时保持 120 -> 最坏 45+66=111 < 120,安全
但请求数多 33%,而每次请求都要吃隧道那 1~2 秒开销
B. hold 保持 60 秒,超时放宽到 180 -> 最坏 60+120=180,安全
选 B。理由:这条链路的根本问题是丢包和抖动,
**请求越少,撞上抖动窗口的概率越小**。
放宽客户端超时只是"别把慢但成功的请求冤枉掉",不增加任何请求。
而且客户端已经有"连续失败 3 次才判离线"的保护
(OFFLINE_AFTER=3),所以偶尔一次 180 秒的等待不会让界面闪"已断开"。
'''
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
BOM = b"\xef\xbb\xbf"
EDITS = [
(
"windows/lanremote/relay.py",
"""# 长轮询单次请求的超时。
# 必须显著大于服务端的 hold 时间(现在 60 秒),否则会在服务端
# 正常返回之前自己先超时 —— 那就变成"每次轮询都失败"了。
# 120 = 60(hold) + 60(网络余量)。
# 为什么余量给这么大:实测出现过一次 93589ms 的长尾
# (4G 丢包触发 TCP 重传退避),原来设 90 秒会把它误判成失败。
POLL_TIMEOUT = 120.0""",
"""# 长轮询单次请求的超时。
# 必须显著大于服务端的 hold 时间(现在 60 秒),否则会在服务端
# 正常返回之前自己先超时 —— 那就变成"每次轮询都失败"了。
# 180 = 60(hold) + 120(网络余量)。
# 余量一直在加大,因为实测的长尾一直在刷新:
# 90 秒 -> 实测 93589ms 不够
# 120 秒 -> 实测 126604ms 不够(30 次里出现 1 次,其余都是 61~62 秒)
# 4G 丢包触发的重传退避能拖出一分钟以上的停顿,这是链路特性。
# 放宽超时只是"别把慢但成功的请求冤枉掉",不增加请求数。
# 而且客户端有"连续失败 3 次才判离线"的保护,
# 偶尔一次长等待不会让界面闪"已断开"。
POLL_TIMEOUT = 180.0""",
),
(
"android/src/com/lanremote/watch/RelayTransport.java",
""" /**
* 长轮询:中转那边挂 60 秒,客户端超时给宽一点。
*
* <p>必须显著大于服务端的 hold 时间,否则会在服务端正常返回
* 之前自己先超时 —— 那就变成"每次轮询都失败"了。
* 120 秒 = 60(hold) + 60(网络余量)。
*
* <p>余量给这么大是因为实测出现过一次 93589ms 的长尾
* (4G 丢包触发 TCP 重传退避)。设 90 秒会把那次
* 其实成功的请求误判成失败。
*/
private static final int POLL_TIMEOUT_MS = 120000;""",
""" /**
* 长轮询:中转那边挂 60 秒,客户端超时给宽一点。
*
* <p>必须显著大于服务端的 hold 时间,否则会在服务端正常返回
* 之前自己先超时 —— 那就变成"每次轮询都失败"了。
*
* <p>180 秒 = 60(hold) + 120(网络余量)。
* 余量一直在加大,因为实测的长尾一直在刷新:
* 90 秒时实测到 93589ms,120 秒时实测到 126604ms
* (30 次里 1 次,其余都是 61~62 秒)。
* 4G 丢包触发的重传退避能拖出一分钟以上的停顿,这是链路特性。
* 放宽超时只是"别把慢但成功的请求冤枉掉",不增加请求数。
*/
private static final int POLL_TIMEOUT_MS = 180000;""",
),
]
def main() -> int:
ok_all = True
for rel, old, new in EDITS:
path = ROOT / rel
if not path.is_file():
print(" ✗ %s 不存在" % rel)
ok_all = False
continue
raw = path.read_bytes()
had_bom = raw.startswith(BOM)
text = raw.decode("utf-8-sig")
if old not in text:
if "180.0" in text or "180000" in text:
print(" • %-46s 已经是新值" % rel)
continue
print(" ✗ %-46s 找不到锚点" % rel)
ok_all = False
continue
text = text.replace(old, new, 1)
data = text.encode("utf-8")
if had_bom:
data = BOM + data
path.write_bytes(data)
print(" ✓ %-46s 已改" % rel)
print()
if not ok_all:
return 1
for rel, token in (
("windows/lanremote/relay.py", "POLL_TIMEOUT = 180.0"),
("android/src/com/lanremote/watch/RelayTransport.java",
"POLL_TIMEOUT_MS = 180000"),
):
text = (ROOT / rel).read_text(encoding="utf-8-sig")
print(" %s %s" % ("✓" if token in text else "✗", rel))
return 0
if __name__ == "__main__":
sys.exit(main())