fix(memory): 按字段扫清 s.cfg 的其余无锁读(P1)+ 回显文案按 rune 截断
review P1:applyTidyConfig 在运行期**整体覆写** s.cfg,而我只把调度读收进了持锁 访问器 —— 同一字段还有几处无锁读,其中两处是**活端点**: - TidyService.DryRun 的开关判定(在取 runMu **之前**)⇒ POST …/tidy/dry-run 与 ?async=1 那条会起后台 goroutine 的版本(真机 90s 级); - TidyService.ManualRun 的开关判定(同样在 runMu 之前)⇒ POST …/tidy/run; - (原本也漏的)Enabled() 与 Start 的启动日志块。 按**字段**重扫了一遍(不按文件):现在生产代码里对 s.cfg 的读**只剩**三个持锁访问器 内部(enabled / cfgSnapshot / scheduleSnapshot),写点只有 applyTidyConfig 一处; s.tidier.cfg 的读点全在持 runMu 的轮次 / 试算路径上(含异步版经 DryRun)。 新增第三条并发回归哨兵:真并发跑「试算 + 手动真跑」两个活端点 × 面板保存。 修前红/修后绿:撤回 DryRun 的同步 ⇒ 报「写 applyTidyConfig × 读 DryRun」;撤回 ManualRun 的同步 ⇒ 报「写 applyTidyConfig × 读 ManualRun」;两次变异都逐字还原 (diff 0 差异)。 顺手两条(塔指定): - 那条并发哨兵的 doc 与实现相反(还写着「正确顺序是 Stop → 写 → Start」),已改成 唤醒机制下的说法 —— 与「注释与实现不符」同类,不留旧说法。 - 非法值的回显文案原按**字节**截断(s[:40]),中文会被切成半截(非法 UTF-8,经 JSON 编码变成 U+FFFD 替换符)。改成按 **rune** 截断,直接复用同包既有的 clipRunes;用例取值刻意让截断点落在多字节字符中间(JSON 引号 + 一个 ASCII 前缀 错开对齐),并做过变异复验:改回按字节 ⇒ 用例红(切出半个汉字)。
This commit is contained in:
1 parent
97c0c1b94a
commit
4df8d6f53a
6 files changed
+167
-29
No files matched your search
@@ -227,14 +227,17 @@ func tuningUnitLabel(spec memoryTidyTuningSpec) string {
|
||||
}
|
||||
}
|
||||
|
||||
// tuningValueClipRunes 是错误文案里回显「收到的值」的 rune 上限。
|
||||
const tuningValueClipRunes = 40
|
||||
|
||||
// clipJSON 把收到的原始 JSON 片段截断成可安全回显的短串(错值可能很长)。
|
||||
//
|
||||
// 🔴 按 **rune** 截断,**不按字节**:值里完全可能是中文(主人就爱填中文),按字节切
|
||||
// 会把一个多字节字符切成两半 —— 回显里因此出现半截 UTF-8(JSON 编码后变成 U+FFFD
|
||||
// 替换符),而这条文案的用途正是让人**看清自己填错了什么**。手法与同包的
|
||||
// `clipRunes`(notifications.go)同源,直接复用而不是再写一份。
|
||||
func clipJSON(raw json.RawMessage) string {
|
||||
const maxClip = 40
|
||||
s := strings.TrimSpace(string(raw))
|
||||
if len(s) <= maxClip {
|
||||
return s
|
||||
}
|
||||
return s[:maxClip] + "…"
|
||||
return clipRunes(strings.TrimSpace(string(raw)), tuningValueClipRunes)
|
||||
}
|
||||
|
||||
// isJSONNull 报告这段原始值是不是 JSON 的 `null`(清除哨兵)。
|
||||
|
||||
@@ -2,11 +2,14 @@ package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/auth"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||||
@@ -261,3 +264,52 @@ func TestAdminMemoryTidyTuning_UnavailableWithoutDataDir(t *testing.T) {
|
||||
t.Fatal("没有数据目录时不该解析出落点")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdminMemoryTidyTuningWrite_LongValueEchoIsRuneSafe 「非法值」的文案会把**收到的
|
||||
// 值**回显出来(这是它的用途:让人看清自己填错了什么),而主人完全可能填中文 ——
|
||||
// 回显必须按 rune 截断:按**字节**切会把一个汉字切成半截(非法 UTF-8,经 JSON 编码后
|
||||
// 变成 U+FFFD 替换符),等于把「你填了什么」显示成乱码。
|
||||
//
|
||||
// 取值刻意让截断点**落在多字节字符中间**:JSON 值自带引号(1 字节),再让首个字符是
|
||||
// 1 字节 ASCII、其余是 3 字节汉字 ⇒ 「引号 + a」占 2 字节,第 40 字节正好切在第 13 个
|
||||
// 汉字的**第三个字节**上。若只堆汉字(引号 1 + 3n 字节),40 会恰好落在 rune 边界上,
|
||||
// 那样这条用例会**假绿**(实测过:按字节截断也照样通过)。
|
||||
func TestAdminMemoryTidyTuningWrite_LongValueEchoIsRuneSafe(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// ① 纯函数层:截断结果必须是合法 UTF-8(这一条与 JSON 层无关,最直接)。
|
||||
long := json.RawMessage(`"a` + strings.Repeat("三", 60) + `"`)
|
||||
clipped := clipJSON(long)
|
||||
if !utf8.ValidString(clipped) {
|
||||
t.Fatalf("clipJSON 切出了非法 UTF-8:%q", clipped)
|
||||
}
|
||||
if strings.ContainsRune(clipped, utf8.RuneError) {
|
||||
t.Fatalf("clipJSON 结果里有替换符:%q", clipped)
|
||||
}
|
||||
|
||||
// ② 端点层:400 文案里不许出现替换符,且开头几个字仍要如实回显。
|
||||
env := newTuneEnv(t, Deps{})
|
||||
before := env.seed(t, `{"intervalSec": 86400}`)
|
||||
payload := `a` + strings.Repeat("三", 60)
|
||||
resp, body := env.put(t, env.token(t, auth.RoleOwner), `{"quietStartHour": "`+payload+`"}`)
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want 400 (body=%s)", resp.StatusCode, body)
|
||||
}
|
||||
assertErrorCode(t, body, "invalid_payload")
|
||||
var eobj struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := json.Unmarshal(decodeBody(t, body)["error"], &eobj); err != nil {
|
||||
t.Fatalf("decode error: %v (body=%s)", err, body)
|
||||
}
|
||||
if !utf8.ValidString(eobj.Message) || strings.ContainsRune(eobj.Message, utf8.RuneError) {
|
||||
t.Fatalf("回显里出现了替换符/非法 UTF-8(半个汉字被切了):%q", eobj.Message)
|
||||
}
|
||||
if !strings.Contains(eobj.Message, "a三三三") {
|
||||
t.Fatalf("文案应当仍回显开头几个字:%q", eobj.Message)
|
||||
}
|
||||
if after := env.bytes(t); string(after) != string(before) {
|
||||
t.Fatalf("非法输入改动了盘上内容:\nbefore=%s\nafter=%s", before, after)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user