feat(net): 连接保活 / 设备去重 / 连接状态机(T-WIN-015,契约 websocket-keypoints 0.2.0)
与 Android 端 T-AND-011「两端一起改」:契约先在 opendance-web 修改并提升到
websocket-keypoints 0.2.0(向后兼容:子协议名与二进制帧 version 均不变,
只新增可选控制字段与一种新控制类型),再同步三端快照。
修掉本端的 3 处根因/放大器:
1. 静默超时 5s 过短且未声明,客户端无从得知。OkHttp 的协议层 ping 由底层库
自动应答、**不暴露给应用层**,无法刷新 last_seen_ms,于是"画面里没人"时
每 5 秒必然踢一次连接 -> 客户端重连 -> 再被踢。
- CLIENT_TIMEOUT_MS 5s -> 15s,新增 HEARTBEAT_INTERVAL_MS = 5s;
- welcome_payload() 在 welcome 中声明 silentTimeoutMs / heartbeatIntervalMs,
config 广播同样带上(契约 §1.1:心跳必须 < 静默超时的一半,5000*2 < 15000);
- 处理应用层 ping 并回 pong(回显 sentAtMs + serverAtMs,供对端算 RTT)。
2. 反复重连被计成多台设备。按契约 §5 用 hello.deviceId 去重:同一设备只保留
最新连接,旧连接以 close(1000) 关闭——刻意用 1000,因为对端按契约 §7
收到 1000 不会重连,否则会形成"挤掉-重连-再挤掉"的循环;
新增 device_count() 与 device_replaced 信号,界面一律显示去重后的设备数。
3. 只有 handshaken 布尔量,日志看不出卡在哪一步。按契约 §8 逐客户端维护与
Android 端同名的状态(connecting -> handshaking -> streaming -> degraded,
主动停止/超时/被替换为 stopped),并记录 close code/reason、协商子协议、
连接/握手/首帧时间;degraded 保持时长做成可覆盖字段便于测试。
验收测试(tests/test_pose_server.gd 新增 7 项,总计 720 通过 / 0 失败):
welcome 声明保活参数、ping->pong 回显、同 deviceId 去重(旧连接 1000 关闭且
设备数仍为 1)、8 个状态名与契约一致、状态随握手/首帧推进到 streaming、
丢帧进入 degraded 后恢复、snapshot 暴露设备数与保活参数。
同时更新 README/README.txt/tasks 与 Credits 的契约版本(0.2.0)。
This commit is contained in:
@@ -62,6 +62,25 @@ func connected_client_count() -> int:
|
||||
return server.connected_count() if server != null else 0
|
||||
|
||||
|
||||
## 去重后的设备数(契约 §4.1);界面显示「已连接 N 台」应当用它
|
||||
func connected_device_count() -> int:
|
||||
return server.device_count() if server != null else 0
|
||||
|
||||
|
||||
## 逐客户端状态(契约 §8,用于界面展示与日志对照)
|
||||
func clients_info() -> Array:
|
||||
return server.clients_info() if server != null else []
|
||||
|
||||
|
||||
## 保活参数(契约 §1.1):本端声明,客户端据此定心跳节奏
|
||||
func silent_timeout_ms() -> int:
|
||||
return server.CLIENT_TIMEOUT_MS if server != null else 0
|
||||
|
||||
|
||||
func heartbeat_interval_ms() -> int:
|
||||
return server.HEARTBEAT_INTERVAL_MS if server != null else 0
|
||||
|
||||
|
||||
## 设置要下发给 Android 端的参数(契约 §3.3 config)
|
||||
func apply_remote_config(config: Dictionary) -> void:
|
||||
if server == null:
|
||||
|
||||
+179
-12
@@ -1,18 +1,27 @@
|
||||
class_name PoseServer
|
||||
extends Node
|
||||
|
||||
## T-WIN-003:WebSocket 关键点接收服务
|
||||
## T-WIN-003 / T-AND-011:WebSocket 关键点接收服务
|
||||
##
|
||||
## 契约 contracts/websocket-keypoints.md §1:
|
||||
## 契约 contracts/websocket-keypoints.md v0.2.0 §1:
|
||||
## 端点 ws://<windows-host>:8765/pose
|
||||
## 子协议 opendance.pose.v1
|
||||
## 二进制帧 = 239 字节关键点(见 KeypointFrame)
|
||||
## 文本帧 = JSON 控制消息(hello / welcome / config / stats / bye)
|
||||
## 文本帧 = JSON 控制消息(hello / welcome / config / stats / ping / pong / bye)
|
||||
##
|
||||
## 本端为**服务端**,Android 辅助 App 主动连入。
|
||||
##
|
||||
## 说明:Godot 的 WebSocketPeer 不暴露握手请求路径,因此无法校验客户端
|
||||
## 连接的 path 是否为 /pose;本实现接受任意 path,仅校验子协议。
|
||||
##
|
||||
## T-AND-011 对齐要点(契约 v0.2.0 §1.1 / §4.1 / §8):
|
||||
## 1. 在 welcome / config 中**声明**静默超时与期望心跳间隔(silentTimeoutMs /
|
||||
## heartbeatIntervalMs),且 heartbeatIntervalMs < silentTimeoutMs / 2;
|
||||
## 2. 处理应用层 ping(协议层 Sec-WebSocket-Ping 在 Godot 里对应用层不可见,
|
||||
## 不能用来续命),可选回 pong 供对端算 RTT;
|
||||
## 3. 按 hello.deviceId **去重**:同一设备的重复连接替换旧连接(1000 关闭),
|
||||
## 设备数按去重后计数,避免"反复重连"被显示成多台设备;
|
||||
## 4. 日志与状态面板使用契约 §8 的同名连接状态描述每个客户端。
|
||||
|
||||
signal listening(port: int)
|
||||
signal listen_failed(port: int, reason: String)
|
||||
@@ -21,16 +30,39 @@ signal client_disconnected(client_id: int, reason: String)
|
||||
signal frame_received(frame: KeypointFrame.PoseFrame)
|
||||
signal control_received(client_id: int, message: Dictionary)
|
||||
signal stats_updated(stats: Dictionary)
|
||||
## 同一 deviceId 的旧连接被新连接替换(契约 §4.1)
|
||||
signal device_replaced(device_id: String)
|
||||
|
||||
const KeypointFrame = preload("res://src/net/keypoint_frame.gd")
|
||||
|
||||
const DEFAULT_PORT := 8765
|
||||
const DEFAULT_SUBPROTOCOL := "opendance.pose.v1"
|
||||
|
||||
## 超过该时长(毫秒)没有收到任何帧,判定为超时断开
|
||||
const CLIENT_TIMEOUT_MS := 5000
|
||||
## 静默超时(毫秒):超过该时长没有收到**任何数据帧或文本帧**即断开。
|
||||
## 契约 §1.1 要求本端在 welcome/config 中声明该值,客户端据此定心跳节奏。
|
||||
## 取 15 秒:Android 每秒一次 stats、每帧关键点都算活动,15 秒足以容忍
|
||||
## 相机暂停 / 画面中无人 / 推理短暂卡顿,又不会让死连接长期占用。
|
||||
const CLIENT_TIMEOUT_MS := 15000
|
||||
## 期望心跳间隔(毫秒)——必须 < CLIENT_TIMEOUT_MS / 2(契约 §1.1)
|
||||
const HEARTBEAT_INTERVAL_MS := 5000
|
||||
## 每秒向客户端推送一次 config 的间隔(0 表示不主动推送)
|
||||
const CONFIG_PUSH_INTERVAL_MS := 2000
|
||||
## degraded 状态的保持时长:这段时间内没有新的丢帧就回到 streaming。
|
||||
## 做成可覆盖的实例字段,便于测试在不依赖真实等待的前提下核对状态回收。
|
||||
const DEGRADED_HOLD_MS := 3000
|
||||
## 契约 §8 的连接状态名(本端为接收端,逐客户端维护)
|
||||
enum ClientState { IDLE, RESOLVING, CONNECTING, HANDSHAKING, STREAMING, DEGRADED, RECONNECTING, STOPPED }
|
||||
|
||||
const STATE_NAMES := {
|
||||
ClientState.IDLE: "idle",
|
||||
ClientState.RESOLVING: "resolving",
|
||||
ClientState.CONNECTING: "connecting",
|
||||
ClientState.HANDSHAKING: "handshaking",
|
||||
ClientState.STREAMING: "streaming",
|
||||
ClientState.DEGRADED: "degraded",
|
||||
ClientState.RECONNECTING: "reconnecting",
|
||||
ClientState.STOPPED: "stopped",
|
||||
}
|
||||
|
||||
var subprotocol: String = DEFAULT_SUBPROTOCOL
|
||||
var expected_fps: int = 30
|
||||
@@ -44,6 +76,8 @@ var _next_client_id: int = 1
|
||||
var _port: int = 0
|
||||
var _running: bool = false
|
||||
var _last_config_push_ms: int = 0
|
||||
## 见 DEGRADED_HOLD_MS;运行期可调(测试用)
|
||||
var degraded_hold_ms: int = DEGRADED_HOLD_MS
|
||||
|
||||
## 累计统计
|
||||
var frames_total: int = 0
|
||||
@@ -98,6 +132,7 @@ func stop() -> void:
|
||||
_running = false
|
||||
for entry in _clients:
|
||||
var peer: WebSocketPeer = entry["peer"]
|
||||
entry["state"] = ClientState.STOPPED
|
||||
if peer.get_ready_state() == WebSocketPeer.STATE_OPEN:
|
||||
peer.close(1001, "server shutdown")
|
||||
_tcp.stop()
|
||||
@@ -134,10 +169,19 @@ func _accept_new_connections() -> void:
|
||||
"peer": peer,
|
||||
"handshaken": false,
|
||||
"device": "",
|
||||
"device_id": "",
|
||||
"state": ClientState.CONNECTING,
|
||||
"selected_protocol": "",
|
||||
"connected_at_ms": Time.get_ticks_msec(),
|
||||
"handshake_at_ms": 0,
|
||||
"first_frame_at_ms": 0,
|
||||
"last_seen_ms": Time.get_ticks_msec(),
|
||||
"last_frame_id": -1,
|
||||
"frames": 0,
|
||||
"dropped": 0,
|
||||
"degraded_until_ms": 0,
|
||||
"close_code": 0,
|
||||
"close_reason": "",
|
||||
"fps_window_start_ms": Time.get_ticks_msec(),
|
||||
"fps_window_frames": 0,
|
||||
"fps": 0.0,
|
||||
@@ -155,12 +199,18 @@ func _poll_clients() -> void:
|
||||
|
||||
var state := peer.get_ready_state()
|
||||
if state == WebSocketPeer.STATE_CLOSED:
|
||||
client_disconnected.emit(entry["id"], "连接已关闭")
|
||||
entry["close_code"] = peer.get_close_code()
|
||||
entry["close_reason"] = peer.get_close_reason()
|
||||
entry["state"] = ClientState.STOPPED
|
||||
client_disconnected.emit(entry["id"], _close_reason_text(entry))
|
||||
continue
|
||||
|
||||
if state == WebSocketPeer.STATE_OPEN and not entry["handshaken"]:
|
||||
entry["handshaken"] = true
|
||||
entry["handshake_at_ms"] = now
|
||||
entry["state"] = ClientState.HANDSHAKING
|
||||
var selected := peer.get_selected_protocol()
|
||||
entry["selected_protocol"] = selected
|
||||
if selected != "" and selected != subprotocol:
|
||||
push_warning("[PoseServer] 客户端选择子协议 %s,与期望 %s 不符" % [selected, subprotocol])
|
||||
client_connected.emit(entry["id"], entry["device"])
|
||||
@@ -169,15 +219,62 @@ func _poll_clients() -> void:
|
||||
_handle_packet(entry, peer)
|
||||
entry["last_seen_ms"] = now
|
||||
|
||||
# 超时剔除
|
||||
# 降级状态到期后回到 streaming(契约 §8)
|
||||
if entry["state"] == ClientState.DEGRADED and now >= int(entry["degraded_until_ms"]):
|
||||
entry["state"] = ClientState.STREAMING if int(entry["first_frame_at_ms"]) > 0 else ClientState.HANDSHAKING
|
||||
|
||||
# 超时剔除:契约 §1.1,客户端应按 heartbeatIntervalMs 保活
|
||||
if entry["handshaken"] and now - int(entry["last_seen_ms"]) > CLIENT_TIMEOUT_MS:
|
||||
client_disconnected.emit(entry["id"], "超时未收到数据")
|
||||
var silent_ms := now - int(entry["last_seen_ms"])
|
||||
entry["state"] = ClientState.STOPPED
|
||||
client_disconnected.emit(
|
||||
entry["id"],
|
||||
"静默超时 %d ms(阈值 %d ms,期望心跳 %d ms)" % [silent_ms, CLIENT_TIMEOUT_MS, HEARTBEAT_INTERVAL_MS],
|
||||
)
|
||||
peer.close(1001, "timeout")
|
||||
continue
|
||||
|
||||
survivors.append(entry)
|
||||
|
||||
_clients = survivors
|
||||
_dedup_devices()
|
||||
|
||||
|
||||
## 契约 §4.1:同一 deviceId 只保留最新连接,旧连接以 1000 关闭(对端不重连)
|
||||
func _dedup_devices() -> void:
|
||||
if _clients.size() < 2:
|
||||
return
|
||||
var seen := {}
|
||||
var keep: Array = []
|
||||
# 倒序扫描:后加入的条目更新,先被保留
|
||||
for i in range(_clients.size() - 1, -1, -1):
|
||||
var entry: Dictionary = _clients[i]
|
||||
var device := str(entry["device_id"])
|
||||
if device == "" or not entry["handshaken"]:
|
||||
keep.append(entry)
|
||||
continue
|
||||
if seen.has(device):
|
||||
entry["state"] = ClientState.STOPPED
|
||||
var peer: WebSocketPeer = entry["peer"]
|
||||
if peer.get_ready_state() == WebSocketPeer.STATE_OPEN:
|
||||
peer.close(1000, "replaced by new connection")
|
||||
client_disconnected.emit(entry["id"], "被同一设备的新连接替换(deviceId=%s)" % device)
|
||||
device_replaced.emit(device)
|
||||
continue
|
||||
seen[device] = entry
|
||||
keep.append(entry)
|
||||
keep.reverse()
|
||||
_clients = keep
|
||||
|
||||
|
||||
static func _close_reason_text(entry: Dictionary) -> String:
|
||||
var code := int(entry["close_code"])
|
||||
var reason := str(entry["close_reason"])
|
||||
if code == 0:
|
||||
return "连接已关闭(未收到关闭帧)"
|
||||
if reason == "":
|
||||
return "连接已关闭(code=%d)" % code
|
||||
return "连接已关闭(code=%d,%s)" % [code, reason]
|
||||
|
||||
|
||||
func _handle_packet(entry: Dictionary, peer: WebSocketPeer) -> void:
|
||||
@@ -208,6 +305,8 @@ func _handle_packet(entry: Dictionary, peer: WebSocketPeer) -> void:
|
||||
|
||||
|
||||
func _update_frame_stats(entry: Dictionary, frame: KeypointFrame.PoseFrame) -> void:
|
||||
if int(entry["first_frame_at_ms"]) == 0:
|
||||
entry["first_frame_at_ms"] = Time.get_ticks_msec()
|
||||
var previous := int(entry["last_frame_id"])
|
||||
if previous >= 0 and frame.frame_id > previous + 1:
|
||||
# u32 回绕保护:差距过大视为计数器重置而非丢包
|
||||
@@ -215,8 +314,13 @@ func _update_frame_stats(entry: Dictionary, frame: KeypointFrame.PoseFrame) -> v
|
||||
if gap < 100000:
|
||||
entry["dropped"] = int(entry["dropped"]) + gap
|
||||
frames_dropped += gap
|
||||
# 契约 §8:仍在连接但出现丢帧 -> degraded(3 秒无新丢帧后自动回到 streaming)
|
||||
entry["state"] = ClientState.DEGRADED
|
||||
entry["degraded_until_ms"] = Time.get_ticks_msec() + degraded_hold_ms
|
||||
entry["last_frame_id"] = frame.frame_id
|
||||
entry["frames"] = int(entry["frames"]) + 1
|
||||
if entry["state"] != ClientState.DEGRADED:
|
||||
entry["state"] = ClientState.STREAMING
|
||||
|
||||
entry["fps_window_frames"] = int(entry["fps_window_frames"]) + 1
|
||||
var now := Time.get_ticks_msec()
|
||||
@@ -232,6 +336,8 @@ func _update_frame_stats(entry: Dictionary, frame: KeypointFrame.PoseFrame) -> v
|
||||
"frames": entry["frames"],
|
||||
"dropped": entry["dropped"],
|
||||
"device": entry["device"],
|
||||
"device_id": entry["device_id"],
|
||||
"state": state_name(entry["state"]),
|
||||
"total_frames": frames_total,
|
||||
"total_dropped": frames_dropped,
|
||||
"malformed": malformed_frames,
|
||||
@@ -252,6 +358,7 @@ func _handle_control(entry: Dictionary, text: String) -> void:
|
||||
match type:
|
||||
"hello":
|
||||
entry["device"] = str(message.get("device", ""))
|
||||
entry["device_id"] = str(message.get("deviceId", ""))
|
||||
var protocol := str(message.get("protocol", subprotocol))
|
||||
if protocol != subprotocol:
|
||||
push_warning("[PoseServer] 协议版本不匹配:客户端 %s,本端 %s" % [protocol, subprotocol])
|
||||
@@ -264,13 +371,17 @@ func _handle_control(entry: Dictionary, text: String) -> void:
|
||||
return
|
||||
if message.has("targetFps"):
|
||||
expected_fps = int(message["targetFps"])
|
||||
entry["state"] = ClientState.STREAMING if int(entry["first_frame_at_ms"]) > 0 else ClientState.HANDSHAKING
|
||||
_send_json(entry, welcome_payload())
|
||||
"ping":
|
||||
# 契约 §3.6:应用层心跳(协议层 ping 对应用层不可见,无法续命)
|
||||
_send_json(entry, {
|
||||
"type": "welcome",
|
||||
"protocol": subprotocol,
|
||||
"sessionId": _session_id(),
|
||||
"expectedFps": expected_fps,
|
||||
"type": "pong",
|
||||
"sentAtMs": int(message.get("sentAtMs", 0)),
|
||||
"serverAtMs": Time.get_ticks_msec(),
|
||||
})
|
||||
"bye":
|
||||
entry["state"] = ClientState.STOPPED
|
||||
client_disconnected.emit(entry["id"], str(message.get("reason", "客户端主动断开")))
|
||||
var peer2: WebSocketPeer = entry["peer"]
|
||||
peer2.close(1000, "bye")
|
||||
@@ -278,6 +389,18 @@ func _handle_control(entry: Dictionary, text: String) -> void:
|
||||
pass
|
||||
|
||||
|
||||
## 契约 §3.2:welcome 必须声明保活参数,客户端据此定心跳节奏
|
||||
func welcome_payload() -> Dictionary:
|
||||
return {
|
||||
"type": "welcome",
|
||||
"protocol": subprotocol,
|
||||
"sessionId": _session_id(),
|
||||
"expectedFps": expected_fps,
|
||||
"silentTimeoutMs": CLIENT_TIMEOUT_MS,
|
||||
"heartbeatIntervalMs": HEARTBEAT_INTERVAL_MS,
|
||||
}
|
||||
|
||||
|
||||
func _maybe_push_config() -> void:
|
||||
if CONFIG_PUSH_INTERVAL_MS <= 0:
|
||||
return
|
||||
@@ -295,6 +418,9 @@ func broadcast_config() -> void:
|
||||
"targetFps": expected_fps,
|
||||
"upperBodyOnly": upper_body_only,
|
||||
"resolution": resolution,
|
||||
# 契约 §3.3:运行中可一并调整保活参数
|
||||
"silentTimeoutMs": CLIENT_TIMEOUT_MS,
|
||||
"heartbeatIntervalMs": HEARTBEAT_INTERVAL_MS,
|
||||
}
|
||||
for entry in _clients:
|
||||
if entry["handshaken"]:
|
||||
@@ -312,12 +438,53 @@ func _session_id() -> String:
|
||||
return "%d-%d" % [Time.get_unix_time_from_system(), _port]
|
||||
|
||||
|
||||
## 状态名(契约 §8,两端同名,便于对照日志)
|
||||
static func state_name(state: int) -> String:
|
||||
return str(STATE_NAMES.get(state, "unknown"))
|
||||
|
||||
|
||||
## 逐客户端信息(界面 / 日志用)
|
||||
func clients_info() -> Array:
|
||||
var out: Array = []
|
||||
for entry in _clients:
|
||||
out.append({
|
||||
"id": entry["id"],
|
||||
"device": entry["device"],
|
||||
"device_id": entry["device_id"],
|
||||
"state": state_name(entry["state"]),
|
||||
"protocol": entry["selected_protocol"],
|
||||
"frames": entry["frames"],
|
||||
"dropped": entry["dropped"],
|
||||
"fps": entry["fps"],
|
||||
"first_frame_at_ms": entry["first_frame_at_ms"],
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
## 去重后的设备数(契约 §4.1:同一 deviceId 只算一台)
|
||||
func device_count() -> int:
|
||||
var ids := {}
|
||||
var anonymous := 0
|
||||
for entry in _clients:
|
||||
if not entry["handshaken"]:
|
||||
continue
|
||||
var device := str(entry["device_id"])
|
||||
if device == "":
|
||||
anonymous += 1
|
||||
else:
|
||||
ids[device] = true
|
||||
return ids.size() + anonymous
|
||||
|
||||
|
||||
func snapshot() -> Dictionary:
|
||||
return {
|
||||
"running": _running,
|
||||
"port": _port,
|
||||
"clients": _clients.size(),
|
||||
"connected": connected_count(),
|
||||
"devices": device_count(),
|
||||
"silent_timeout_ms": CLIENT_TIMEOUT_MS,
|
||||
"heartbeat_interval_ms": HEARTBEAT_INTERVAL_MS,
|
||||
"frames_total": frames_total,
|
||||
"frames_dropped": frames_dropped,
|
||||
"malformed_frames": malformed_frames,
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ const ENTRIES := [
|
||||
## 因此不能用单个常量表示;界面展示统一走 summary()。
|
||||
const CONTRACT_VERSIONS := {
|
||||
"rest-api": "0.5.0",
|
||||
"websocket-keypoints": "0.1.0",
|
||||
"websocket-keypoints": "0.2.0",
|
||||
"data-models": "0.7.0",
|
||||
"skeleton-topology": "0.2.0",
|
||||
"model-formats": "0.5.0",
|
||||
|
||||
@@ -426,7 +426,8 @@ func _on_score_updated(result: Dictionary) -> void:
|
||||
|
||||
|
||||
func _refresh_connection_hint() -> void:
|
||||
var count := PoseReceiver.connected_client_count()
|
||||
# 契约 §4.1:按 deviceId 去重后的设备数(连接数可能因重连而短暂偏大)
|
||||
var count := PoseReceiver.connected_device_count()
|
||||
if count > 0:
|
||||
_hint_label.text = "● 已连接 %d 台设备:%s" % [count, AppConfig.listen_url()]
|
||||
_hint_label.add_theme_color_override("font_color", UiKit.COLOR_GOOD)
|
||||
|
||||
+16
-1
@@ -342,13 +342,28 @@ func _report_receiver() -> void:
|
||||
if snap.is_empty():
|
||||
_set_status("接收服务未初始化")
|
||||
return
|
||||
_set_status("接收服务:%s,端口 %d,连接 %d,累计帧 %d,丢帧 %d,畸形帧 %d" % [
|
||||
# 契约 §4.1:显示去重后的设备数,避免"反复重连"被计成多台设备
|
||||
var per_client := PackedStringArray()
|
||||
for info in PoseReceiver.clients_info():
|
||||
per_client.append("#%d %s[%s] %d 帧/%d 丢" % [
|
||||
int(info["id"]),
|
||||
str(info["device"]) if str(info["device"]) != "" else "未命名设备",
|
||||
str(info["state"]),
|
||||
int(info["frames"]),
|
||||
int(info["dropped"]),
|
||||
])
|
||||
var detail := ("| " + ",".join(per_client)) if per_client.size() > 0 else ""
|
||||
_set_status("接收服务:%s,端口 %d,连接 %d,设备 %d,累计帧 %d,丢帧 %d,畸形帧 %d|静默超时 %dms 心跳 %dms%s" % [
|
||||
"运行中" if bool(snap.get("running", false)) else "已停止",
|
||||
int(snap.get("port", 0)),
|
||||
int(snap.get("connected", 0)),
|
||||
int(snap.get("devices", 0)),
|
||||
int(snap.get("frames_total", 0)),
|
||||
int(snap.get("frames_dropped", 0)),
|
||||
int(snap.get("malformed_frames", 0)),
|
||||
int(snap.get("silent_timeout_ms", 0)),
|
||||
int(snap.get("heartbeat_interval_ms", 0)),
|
||||
detail,
|
||||
])
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user