主人实测:memory_items 1191 行、候选 77 条全 pending、memory_tidy_runs 仅 1 行、两晚定时没跑。 根因两条(都在这片里修掉): ① **以前根本没有执行面** —— 只有只读试算,候选永远 pending ② **调度被重启饿死** —— interval=24h 的 ticker 从进程启动算起,两天重启十几次 ⇒ 24h 永远走不完 - 应用路径:逐条 / 按 run 批量(默认 100、硬顶 500、超限 truncated)/ revert;**软合并** (status + merged_into/merged_from/merge_count,**绝不改 content、行不删**)+ 审计 (memory.tidy.apply / memory.tidy.revert)+ **幂等**(候选状态条件更新 + 行 status 闸两道) - 调度:改成**到点即跑**(catchUp:无历史+窗口内 / 距上次 run ≥ interval / 窗口起点晚于上次 run), 「上次 run」以 memory_tidy_runs 为**持久事实**;注入时钟用例证明**重启不饿死** - 手动真跑端点 POST /admin/memories/tidy/run(同档鉴权 + 审计 + 回执:新增/合并/软删/耗时) - 候选表加 run_id 列 + 索引(前滚;顺序=建表→补列→建索引,否则既有库会 no such column) - 3 个变异自检(删 catchUp / 去掉两道 status 闸 / truncated 恒 false) ⚠️ 生产库零动作:1191 行真跑与 77 条候选应用**未执行**(按红线由主人确认后主会话执行) ⚠️ 契约段在报告里(candidates 列表/apply/revert/tidy.run),Docs/ 待登记
101 lines
3.7 KiB
Go
101 lines
3.7 KiB
Go
package gateway
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"net/http"
|
||
|
||
apperr "git.yeij.top/AskaEth/Cyrene_v2/internal/errors"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory"
|
||
)
|
||
|
||
// 本文件是记忆整理**手动真跑**入口(切片 TIDY-1):
|
||
// POST /api/v1/admin/memories/tidy/run —— 立刻按同一套规则跑一轮,写候选 + 写运行
|
||
// 留痕,并把「新增/合并/软删各多少 + 耗时」如实回执。审计由领域侧 TidyService 记
|
||
// `memory.tidy.run`(一次点击一条,本文件不重复)。
|
||
|
||
// adminMemoryTidyRun 处理 POST /api/v1/admin/memories/tidy/run:手动**真跑**一轮
|
||
// (与心跳、试算走同一套规则与配置;写候选 + 运行留痕)。它是「panic button」:
|
||
// 主人不必等到 23:00 就能让整理真的产出一批待定候选,再决定应用。
|
||
//
|
||
// 审计不在这里写 —— 领域侧 TidyService 已按 design §8 记 `memory.tidy.run`
|
||
// (result 里带 trigger=manual / run_id / 计数)。一次点击一条审计。
|
||
func (s *Server) adminMemoryTidyRun(w http.ResponseWriter, r *http.Request) {
|
||
if s.memoryTidy == nil {
|
||
writeError(w, apperr.Wrap(apperr.ErrInternal, tidyUnavailableCode, "记忆整理服务未接线"))
|
||
return
|
||
}
|
||
runner, ok := s.memoryTidy.(MemoryTidyRunner)
|
||
if !ok {
|
||
writeError(w, &apperr.AppError{
|
||
Code: "tidy_run_unavailable",
|
||
Message: "记忆整理真跑入口未接线(只有只读试算可用)",
|
||
HTTPCode: http.StatusInternalServerError,
|
||
})
|
||
return
|
||
}
|
||
ctx, cancel := context.WithTimeout(r.Context(), tidyRunTimeout)
|
||
defer cancel()
|
||
|
||
stats, err := runner.ManualRun(ctx)
|
||
if err != nil {
|
||
writeError(w, tidyRunAppError(err))
|
||
return
|
||
}
|
||
s.log.Info("memory tidy manual run finished",
|
||
"run_id", stats.RunID, "scanned", stats.Scanned, "candidates", stats.Candidates,
|
||
"duplicates", stats.Duplicates, "duration_ms", stats.DurationMS)
|
||
writeJSON(w, http.StatusOK, adminTidyRunResponse(stats))
|
||
}
|
||
|
||
// tidyRunAppError 把「真跑」失败映射成诚实的错误码。映射表与试算那张
|
||
// (tidyDryRunAppError)**分开**:试算的 500/409 文案讲的是「没产出候选」,
|
||
// 真跑讲的是「没跑成」—— 同一类别、不同事实,混用会让排障读到错的那句。
|
||
func tidyRunAppError(err error) *apperr.AppError {
|
||
switch {
|
||
case errors.Is(err, memory.ErrTidyDisabled):
|
||
return &apperr.AppError{
|
||
Code: "tidy_disabled",
|
||
Message: "记忆整理已被配置关闭(MEMORY_TIDY_ENABLED=false)",
|
||
HTTPCode: http.StatusConflict,
|
||
}
|
||
case errors.Is(err, context.DeadlineExceeded):
|
||
return &apperr.AppError{
|
||
Code: "tidy_timeout",
|
||
Message: "记忆整理真跑超时(本轮未完整跑完,已跑部分仍写入了留痕)",
|
||
HTTPCode: http.StatusServiceUnavailable,
|
||
}
|
||
default:
|
||
return apperr.Wrap(apperr.ErrInternal, "tidy_run_failed", "记忆整理真跑失败")
|
||
}
|
||
}
|
||
|
||
// adminTidyRunResponse 投影真跑回执。
|
||
func adminTidyRunResponse(stats memory.TidyStats) AdminTidyRunResponse {
|
||
byAction := stats.ByAction
|
||
if byAction == nil {
|
||
byAction = map[string]int{}
|
||
}
|
||
reasons := stats.VetoReasons
|
||
if reasons == nil {
|
||
reasons = map[string]int{}
|
||
}
|
||
return AdminTidyRunResponse{
|
||
RunID: stats.RunID,
|
||
Trigger: "manual",
|
||
RuleVersion: memory.TidyRuleVersion,
|
||
DurationMs: stats.DurationMS,
|
||
Partitions: stats.Partitions,
|
||
Scanned: stats.Scanned,
|
||
Compared: stats.Compared,
|
||
Pairs: stats.Pairs,
|
||
Vetoed: stats.Vetoed,
|
||
Errors: stats.Errors,
|
||
Truncated: stats.Truncated,
|
||
VetoReasons: reasons,
|
||
CandidatesCreated: stats.Candidates,
|
||
CandidatesDuplicate: stats.Duplicates,
|
||
CandidatesByAction: byAction,
|
||
}
|
||
}
|