v0.4.0:窗口调度/限流分类/可观测性,抢购改每账号独立循环,移除 CLI 只保留 GUI
This commit is contained in:
@@ -3,9 +3,12 @@ package wup
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"huya-go/internal/taf"
|
||||
)
|
||||
@@ -261,3 +264,88 @@ func NormalizePayload(data []byte) []byte {
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// 诊断串里的取值上限:服务端错误信息可能很长,日志里只需要够判断原因的前缀。
|
||||
const (
|
||||
diagnoseValueLimit = 80
|
||||
diagnoseFieldLimit = 6
|
||||
)
|
||||
|
||||
// Diagnose 汇总一次 WUP 响应里可用于排障的字段。
|
||||
//
|
||||
// 「响应缺少 tRsp/tResp」这种报错过去只有一句文案,服务端的 iRet / status /
|
||||
// context 全被丢掉,事后只能靠抓包复现(2026-10-16 pc-3 日志里同一账号连续 13 次
|
||||
// 命中该错误,却看不出任何原因)。这里把 tag3(iRet)、tag9(context)、tag10(status)
|
||||
// 与 tResp 数据体的 key 一并带出来,一次日志就能定性。
|
||||
func (r *Response) Diagnose() string {
|
||||
if r == nil {
|
||||
return "响应为空"
|
||||
}
|
||||
parts := []string{fmt.Sprintf("iRet=%d", r.Message)}
|
||||
if r.Servant != "" || r.Func != "" {
|
||||
parts = append(parts, fmt.Sprintf("servant=%s.%s", r.Servant, r.Func))
|
||||
}
|
||||
if text := formatDiagnoseMap(r.Status); text != "" {
|
||||
parts = append(parts, "status="+text)
|
||||
}
|
||||
if text := formatDiagnoseMap(r.Context); text != "" {
|
||||
parts = append(parts, "context="+text)
|
||||
}
|
||||
if len(r.NewData) == 0 {
|
||||
parts = append(parts, "keys=空")
|
||||
} else {
|
||||
parts = append(parts, "keys="+formatDiagnoseKeys(r.NewData))
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("payload=%dB", len(r.Buffer)))
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// formatDiagnoseMap 按键名排序输出 map,值过长时截断,保证日志稳定且不刷屏。
|
||||
func formatDiagnoseMap(values map[string]string) string {
|
||||
if len(values) == 0 {
|
||||
return ""
|
||||
}
|
||||
keys := make([]string, 0, len(values))
|
||||
for key := range values {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
parts := make([]string, 0, len(keys))
|
||||
for index, key := range keys {
|
||||
if index >= diagnoseFieldLimit {
|
||||
parts = append(parts, fmt.Sprintf("…(共%d项)", len(keys)))
|
||||
break
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%s=%s", key, truncateDiagnoseValue(values[key])))
|
||||
}
|
||||
return "{" + strings.Join(parts, " ") + "}"
|
||||
}
|
||||
|
||||
// formatDiagnoseKeys 输出 tResp 数据体的键名(值可能是任意结构,只列键)。
|
||||
func formatDiagnoseKeys(values map[string][]byte) string {
|
||||
keys := make([]string, 0, len(values))
|
||||
for key := range values {
|
||||
keys = append(keys, fmt.Sprintf("%s(%dB)", key, len(values[key])))
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return "{" + strings.Join(keys, " ") + "}"
|
||||
}
|
||||
|
||||
func truncateDiagnoseValue(value string) string {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if len(trimmed) <= diagnoseValueLimit {
|
||||
return trimmed
|
||||
}
|
||||
return trimmed[:diagnoseValueLimit] + "…"
|
||||
}
|
||||
|
||||
// HexHead 把二进制帧的前 n 字节转成十六进制,用于 debug 级日志定位协议问题。
|
||||
func HexHead(data []byte, n int) string {
|
||||
if n <= 0 || len(data) == 0 {
|
||||
return ""
|
||||
}
|
||||
if n > len(data) {
|
||||
n = len(data)
|
||||
}
|
||||
return hex.EncodeToString(data[:n])
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package wup_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"huya-go/internal/huya"
|
||||
@@ -31,3 +32,56 @@ func TestWupEncodeDecodeHeader(t *testing.T) {
|
||||
t.Fatal("missing tReq")
|
||||
}
|
||||
}
|
||||
|
||||
// TestResponseDiagnoseIncludesServerFields 验证诊断串会把 WUP 响应里的排障字段带出来。
|
||||
//
|
||||
// 2026-10-16 的复盘卡在「响应缺少 tRsp/tResp」只有一句文案:服务端回的 iRet、
|
||||
// status、context 与数据体键名全被丢掉,同一账号连续 13 次命中也无法定性。
|
||||
func TestResponseDiagnoseIncludesServerFields(t *testing.T) {
|
||||
resp := &wup.Response{
|
||||
Message: -1,
|
||||
Servant: "webActUI",
|
||||
Func: "scoreExchangePrize",
|
||||
Status: map[string]string{"STATUS_RESULT_DESC": "风控拦截", "STATUS_RESULT_CODE": "-1"},
|
||||
Context: map[string]string{"uid": "123"},
|
||||
NewData: map[string][]byte{"tErr": {0x0c}},
|
||||
Buffer: []byte{1, 2, 3},
|
||||
}
|
||||
got := resp.Diagnose()
|
||||
for _, want := range []string{
|
||||
"iRet=-1",
|
||||
"servant=webActUI.scoreExchangePrize",
|
||||
"STATUS_RESULT_CODE=-1",
|
||||
"STATUS_RESULT_DESC=风控拦截",
|
||||
"context={uid=123}",
|
||||
"keys={tErr(1B)}",
|
||||
"payload=3B",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("Diagnose()=%q,缺少 %q", got, want)
|
||||
}
|
||||
}
|
||||
if got := (*wup.Response)(nil).Diagnose(); got != "响应为空" {
|
||||
t.Fatalf("空响应诊断=%q", got)
|
||||
}
|
||||
if got := (&wup.Response{}).Diagnose(); !strings.Contains(got, "iRet=0") || !strings.Contains(got, "keys=空") {
|
||||
t.Fatalf("零值响应诊断=%q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHexHead 验证原始帧前缀输出的截断与边界。
|
||||
func TestHexHead(t *testing.T) {
|
||||
data := []byte{0x1f, 0x8b, 0x08, 0x00}
|
||||
if got := wup.HexHead(data, 2); got != "1f8b" {
|
||||
t.Fatalf("HexHead(2)=%q", got)
|
||||
}
|
||||
if got := wup.HexHead(data, 32); got != "1f8b0800" {
|
||||
t.Fatalf("HexHead 越界应截断到实际长度: %q", got)
|
||||
}
|
||||
if got := wup.HexHead(nil, 8); got != "" {
|
||||
t.Fatalf("空数据=%q", got)
|
||||
}
|
||||
if got := wup.HexHead(data, 0); got != "" {
|
||||
t.Fatalf("长度为 0=%q", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user