xiaolongmr commited on
Commit ·
9340872
1
Parent(s): d12cef2
houtai xintiao jiang pin dao 50s, fangkuan zaixian chuangkou dao 120s
Browse filespresence xintiao shi wei yi "qie dao houtai ye zhao da" de quanju lunxun,
xian shang shi ce dan zhe yi xiang jiu you 300+ req/s, er rongqi zhi you
2 ge gongxiang vCPU。
- qian tai: 25s (bu bian, hu ying bu shou ying xiang)
- hou tai: 50s (jiang yi ban)
- fuwu duan zaixian chuang kou: 60s -> 120s
50s x 2 = 100s < 120s, lian diu liang ci xintiao reng suan zaixian,
bi yuan lai de 25s/60s geng bu rongyi wu pan li xian。
qie hui qian tai shi li ke bu yi ci, zaixian renshu dangchang bian zhun。
zhen ji: artifacts/_visibility_gate_test.py 10/10; tsc / eslint jun lv。
services/presence_service.py
CHANGED
|
@@ -4,8 +4,11 @@ import threading
|
|
| 4 |
import time
|
| 5 |
from typing import Any
|
| 6 |
|
| 7 |
-
# 在线判定窗口
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
# 站内角色 → 展示分组。「user」是内部用户(不消耗积分、走模型每日限额)。
|
| 11 |
_ROLE_GROUP = {"normal": "normal", "user": "internal", "admin": "admin"}
|
|
|
|
| 4 |
import time
|
| 5 |
from typing import Any
|
| 6 |
|
| 7 |
+
# 在线判定窗口。
|
| 8 |
+
# ⚠️ 取 120s 而不是 60s:客户端前台心跳 25s、**后台降到 50s**(后台也照打是最贵的轮询,
|
| 9 |
+
# 线上单这一项就有 300+ req/s,而容器只有 2 个共享 vCPU)。
|
| 10 |
+
# 50s × 2 = 100s < 120s ⇒ 连丢两次心跳仍算在线,比原来的 25s/60s 更不容易误判离线。
|
| 11 |
+
DEFAULT_ONLINE_WINDOW_SECS = 120
|
| 12 |
|
| 13 |
# 站内角色 → 展示分组。「user」是内部用户(不消耗积分、走模型每日限额)。
|
| 14 |
_ROLE_GROUP = {"normal": "normal", "user": "internal", "admin": "admin"}
|
web/src/components/presence-provider.tsx
CHANGED
|
@@ -4,9 +4,21 @@ import { createContext, useContext, useEffect, useRef, useState, type ReactNode
|
|
| 4 |
|
| 5 |
import { leavePresence, loadPresenceAuthKey, pingPresence } from "@/lib/api";
|
| 6 |
|
| 7 |
-
// 心跳间隔要显著小于服务端
|
| 8 |
const HEARTBEAT_INTERVAL_MS = 25_000;
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
const PresenceContext = createContext<number | null>(null);
|
| 11 |
|
| 12 |
/**
|
|
@@ -30,6 +42,7 @@ export function PresenceProvider({ children }: { children: ReactNode }) {
|
|
| 30 |
|
| 31 |
useEffect(() => {
|
| 32 |
let disposed = false;
|
|
|
|
| 33 |
|
| 34 |
const beat = async () => {
|
| 35 |
try {
|
|
@@ -42,6 +55,19 @@ export function PresenceProvider({ children }: { children: ReactNode }) {
|
|
| 42 |
}
|
| 43 |
};
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
void (async () => {
|
| 46 |
const key = await loadPresenceAuthKey();
|
| 47 |
if (!disposed) {
|
|
@@ -50,9 +76,16 @@ export function PresenceProvider({ children }: { children: ReactNode }) {
|
|
| 50 |
void beat();
|
| 51 |
})();
|
| 52 |
|
| 53 |
-
const
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
// pagehide 覆盖关闭标签页 / 前进后退 / 移动端切走;刷新时也会触发,
|
| 58 |
// 但新页面挂载后立刻就会重新 ping,不会造成空窗。
|
|
@@ -63,7 +96,10 @@ export function PresenceProvider({ children }: { children: ReactNode }) {
|
|
| 63 |
|
| 64 |
return () => {
|
| 65 |
disposed = true;
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
| 67 |
window.removeEventListener("pagehide", handlePageHide);
|
| 68 |
};
|
| 69 |
}, []);
|
|
|
|
| 4 |
|
| 5 |
import { leavePresence, loadPresenceAuthKey, pingPresence } from "@/lib/api";
|
| 6 |
|
| 7 |
+
// 心跳间隔要显著小于服务端在线窗口,容忍一次丢包 / 网络抖动后仍算在线。
|
| 8 |
const HEARTBEAT_INTERVAL_MS = 25_000;
|
| 9 |
|
| 10 |
+
/**
|
| 11 |
+
* 后台标签页的心跳间隔。
|
| 12 |
+
*
|
| 13 |
+
* ⚠️ 这是**唯一「切到后台也照打」的全局轮询**,线上实测它单独就有 300+ req/s,
|
| 14 |
+
* 而容器只有 2 个共享 vCPU。后台标签页不需要 25s 的精度 —— 但也不能直接停:
|
| 15 |
+
* 停了「在线人数」就会漏掉开着页面去干别的事的用户,那正是当初把它放全局的原因。
|
| 16 |
+
*
|
| 17 |
+
* 折中:后台降到 50s。配合**服务端窗口从 60s 放宽到 120s**,仍然容忍连丢两次心跳,
|
| 18 |
+
* 比改之前更不容易误判离线(改之前 25s 间隔 + 60s 窗口,丢一次就濒临掉线)。
|
| 19 |
+
*/
|
| 20 |
+
const HEARTBEAT_INTERVAL_MS_HIDDEN = 50_000;
|
| 21 |
+
|
| 22 |
const PresenceContext = createContext<number | null>(null);
|
| 23 |
|
| 24 |
/**
|
|
|
|
| 42 |
|
| 43 |
useEffect(() => {
|
| 44 |
let disposed = false;
|
| 45 |
+
let timer: number | undefined;
|
| 46 |
|
| 47 |
const beat = async () => {
|
| 48 |
try {
|
|
|
|
| 55 |
}
|
| 56 |
};
|
| 57 |
|
| 58 |
+
/** 按当前可见性选间隔重排定时器(切前后台时都要重排一次)。 */
|
| 59 |
+
const schedule = () => {
|
| 60 |
+
if (timer !== undefined) {
|
| 61 |
+
window.clearInterval(timer);
|
| 62 |
+
}
|
| 63 |
+
const interval = document.visibilityState === "visible"
|
| 64 |
+
? HEARTBEAT_INTERVAL_MS
|
| 65 |
+
: HEARTBEAT_INTERVAL_MS_HIDDEN;
|
| 66 |
+
timer = window.setInterval(() => {
|
| 67 |
+
void beat();
|
| 68 |
+
}, interval);
|
| 69 |
+
};
|
| 70 |
+
|
| 71 |
void (async () => {
|
| 72 |
const key = await loadPresenceAuthKey();
|
| 73 |
if (!disposed) {
|
|
|
|
| 76 |
void beat();
|
| 77 |
})();
|
| 78 |
|
| 79 |
+
const onVisibilityChange = () => {
|
| 80 |
+
schedule();
|
| 81 |
+
// 切回前台立刻补一次:在线状态和人数当场变准,不用等下一个周期。
|
| 82 |
+
if (document.visibilityState === "visible") {
|
| 83 |
+
void beat();
|
| 84 |
+
}
|
| 85 |
+
};
|
| 86 |
+
|
| 87 |
+
schedule();
|
| 88 |
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
| 89 |
|
| 90 |
// pagehide 覆盖关闭标签页 / 前进后退 / 移动端切走;刷新时也会触发,
|
| 91 |
// 但新页面挂载后立刻就会重新 ping,不会造成空窗。
|
|
|
|
| 96 |
|
| 97 |
return () => {
|
| 98 |
disposed = true;
|
| 99 |
+
if (timer !== undefined) {
|
| 100 |
+
window.clearInterval(timer);
|
| 101 |
+
}
|
| 102 |
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
| 103 |
window.removeEventListener("pagehide", handlePageHide);
|
| 104 |
};
|
| 105 |
}, []);
|