fix(socks5): 加 Socks5Server.is_alive() 健康探活, sync_instances 自动重启死实例
背景:
Socks5Server.running 是乐观标记, start() 之后到 stop() 之前一直 True。
但子线程 event loop 崩了/端口被外部抢占/worker 被 gunicorn 杀 等情况,
self.running 仍为 True, DB 里 inst.running=true 是谎言。
上次 40080 实例莫名停止就是这种状态。
变更:
+ Socks5Server.is_alive() thread.is_alive() + server.is_serving()
M InstanceManager.sync_instances 启动前先探活, 死的从 _instances 摘掉
让'启动缺失'循环用 DB 配置重建它
M inst.running = srv.is_alive() 不再用乐观的 srv.running
测试:
+ tests/e2e_socks5_lifecycle.py 完整 SOCKS5 userpass 生命周期, 拦住
_record_stats 里 await 同步方法的回归
+ tests/e2e_health_check.py 模拟 Socks5Server 死了, sync_instances
自动重启; 反向验证: 注释掉健康检查就 fail
验证:
- 正向 (修复在): lifecycle PASS, health PASS, smoke tunnel PASS
- 反向 (回滚修复): 两个 e2e 都正确 FAIL, 证明测试真的能抓回归
This commit is contained in:
+41
-4
@@ -150,6 +150,27 @@ class Socks5Server:
|
||||
with self._lock:
|
||||
return self._active_connections
|
||||
|
||||
def is_alive(self):
|
||||
"""真实健康检查: 线程活 + server socket 在服务。
|
||||
|
||||
self.running 是乐观标记, start() 之后一直为 True 直到 stop() 被显式调用。
|
||||
如果子线程 event loop 崩了 (例如 _record_stats 抛 TypeError 把 worker 卡死,
|
||||
上游 gunicorn timeout kill 进程, 或端口被外部抢占), self.running 还是
|
||||
True 但 server 实际不再 accept。
|
||||
|
||||
调用方应据此重启实例, 否则 DB 里 running=true 是谎言。
|
||||
"""
|
||||
if not self.running:
|
||||
return False
|
||||
if self.thread is None or not self.thread.is_alive():
|
||||
return False
|
||||
if self.server is None:
|
||||
return False
|
||||
# server.is_serving() 反映 socket 是否在 listen, 比检查 fd 状态靠谱
|
||||
if not self.server.is_serving():
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
import time # noqa: E402
|
||||
|
||||
@@ -185,7 +206,12 @@ class InstanceManager:
|
||||
self._config_cache = new_configs
|
||||
|
||||
def sync_instances(self):
|
||||
"""根据数据库配置同步实例运行状态。"""
|
||||
"""根据数据库配置同步实例运行状态。
|
||||
|
||||
健康检查: 对每个 _instances[name], 调用 is_alive() 验证线程 + server
|
||||
socket 都活着。如果死了, 从 _instances 删掉, 用同一个 config 重新启动。
|
||||
避免 40080 那种'DB 写 running=true 但端口没人 listen'的谎言状态。
|
||||
"""
|
||||
from models import Instance
|
||||
from database import db
|
||||
self.reload_config()
|
||||
@@ -193,7 +219,18 @@ class InstanceManager:
|
||||
current_names = set(self._config_cache.keys())
|
||||
running_names = set(self._instances.keys())
|
||||
|
||||
# 启动缺失的实例
|
||||
# 健康检查: 把死的从 _instances 摘掉, 让下面的'启动缺失'逻辑接管。
|
||||
# _config_cache 是从 DB reload 的权威配置, 不能动它, 否则重启循环拿不到 cfg。
|
||||
dead = []
|
||||
for name, srv in self._instances.items():
|
||||
if not srv.is_alive():
|
||||
log.warning("[%s] SOCKS5 进程不健康 (thread/socket dead), 标记待重启", name)
|
||||
dead.append(name)
|
||||
for name in dead:
|
||||
self._instances.pop(name, None)
|
||||
running_names = set(self._instances.keys())
|
||||
|
||||
# 启动缺失的实例 (含刚被健康检查踢出来的)
|
||||
for name in current_names - running_names:
|
||||
cfg = self._config_cache[name]
|
||||
srv = Socks5Server(cfg, self.user_service, self.flask_app)
|
||||
@@ -210,11 +247,11 @@ class InstanceManager:
|
||||
srv = self._instances.pop(name)
|
||||
srv.stop()
|
||||
|
||||
# 更新实例运行状态到数据库
|
||||
# 更新实例运行状态到数据库 (用 is_alive() 而不是乐观 running 标记)
|
||||
with db.app.app_context():
|
||||
for inst in Instance.query.all():
|
||||
srv = self._instances.get(inst.name)
|
||||
inst.running = srv is not None and srv.running
|
||||
inst.running = srv is not None and srv.is_alive()
|
||||
if srv:
|
||||
inst.active_connections = srv.active_connections
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user