feat(memory): 调优重排改为唤醒通道,不再 Stop/Start(消灭与到期清扫的耦合)
按塔的裁定把「重排心跳」从生命周期操作改成唤醒通道,调优路径上一个 Start/Stop 都没有了。 - tidy_service.go:TidyService 新增 wake(cap 1 + 非阻塞发)与 wakeMu/wakeCancel; loop 的等待段抽成 waitForTick,三态判定(睡满 / 被调优唤醒 / 停);被唤醒只 重算下一拍、**绝不**跑轮。调度读(nextDelay / dueNow)改走 scheduleSnapshot, 在同一把锁下同取「cfg + lastRun」,于是运行期写 cfg 不必再把后台件停下来。 Start / Stop / wg / running 与 Enabled 早退语义**一个字节没动**。 - tidy_service_tuning.go:ApplyPanelTuning 变成「持 runMu 写 cfg → 发一次唤醒」。 唤醒 = 一次性标记(保证不丢:心跳不在等时标记留在缓冲里)**加上**取消当前那次 等待(保证立刻生效,否则 24h→3h 要等原来那一觉睡完)。 两条冲突就此消失,各配一条可复现断言: - **面板保存不再触发到期清扫、也不重置清扫 ticker**:新用例在保存前后各造一行已 到期的记忆,保存之后那行必须**还在**、清扫审计计数必须不变;该用例自带活对照 (启动清扫会删掉第一行),因此「行还在」不是量具失灵。若保存走 Stop/Start,清扫 的「启动即扫一轮」会立刻把它删掉。 - **调优路径上不再存在任何 Start 调用**(结构上消灭交叉 Start/Stop):包内生产代码 已 grep 不到任何 `.Start(` / `.Stop(` 调用,Start 与 Stop 各只剩组合根那一处调用点 (`cmd/cyrene/main.go` / `cmd/cyrene/wire_services.go`),M3 注释里那条「Start/Stop 各只调用一次」的不变式因此重新成立;交叉场景由带真心跳的调优用例在 `-race` 下跑过 (不再 panic)。 热生效没退化:原来那条「23h30m → 30m」的断言按新机制重写 —— 用一个「听得懂 ctx 取消」的假等待看两觉的时长(保存前 23h30m、保存当场的第二觉 30m),并补上「唤醒不 跑轮(lastRun 不前移)」「唤醒后 running 仍为 true」。 变异自证(都跑过再还原,`diff` 逐字一致): - 去掉 `notifyReschedule` 调用 ⇒「被唤醒后的第二觉」等不到 ⇒ 用例红; - 只留标记、去掉「取消当前等待」那一半 ⇒ 同一条红(两半都必需)。 M3 的守卫 `expire_loop_test.go` **一行未动**:它的假 wait 只认 ctx,父 ctx 取消时 返回 false,在新机制下语义不变(无人唤醒时照样把心跳停住)。
This commit is contained in:
1 parent
a87fbeb494
commit
97c0c1b94a
3 files changed
+328
-82
No files matched your search
@@ -63,8 +63,23 @@ type TidyService struct {
|
||||
now func() time.Time
|
||||
// wait 是心跳的等待原语:返回 false 表示 ctx 已取消。生产是 time.Timer;
|
||||
// 测试注入假等待(推进假时钟后立刻返回),因此补跑判据可以确定性验证。
|
||||
//
|
||||
// 🔴 它的返回值语义**没有变**(`false` 仍表示「这一觉没睡成就结束了」):
|
||||
// 面板调优的重排**不是**靠它自报,而是靠下面那个可被取消的子 ctx —— 于是注入的
|
||||
// 假等待(不认识 wake 的那些)在测试里语义逐字不变(见 loop 的等待段)。
|
||||
wait func(ctx context.Context, d time.Duration) bool
|
||||
|
||||
// wake 是「面板调优改了配置 ⇒ 把这一觉重排一次」的一次性标记:**有缓冲(cap 1)
|
||||
// + 非阻塞发**,见 tidy_service_tuning.go 的 notifyReschedule。写端跑在面板的
|
||||
// HTTP 保存路径上,因此**绝不允许阻塞**;满了丢弃是**正确**语义(通道里那一个
|
||||
// 标记表达的就是「有一次待处理的重排」,重算是幂等的)。
|
||||
wake chan struct{}
|
||||
// wakeMu 保护 wakeCancel(= 取消「当前这一次等待」的手段;两次等待之间是 nil)。
|
||||
// 它是 wake 的**中断**那一半:只有标记没有中断的话,新周期要等这一觉(可能是一天)
|
||||
// 睡完才生效 —— 那正是主人把 24h 改成 3h 时看到的「改了跟没改一样」。
|
||||
wakeMu sync.Mutex
|
||||
wakeCancel context.CancelFunc
|
||||
|
||||
// runMu 保证同一时刻只有一轮整理(design/23 §9 单写者):心跳与手动触发
|
||||
// 若撞在一起,后者排队而不是并发改写候选表。
|
||||
runMu sync.Mutex
|
||||
@@ -86,6 +101,7 @@ func NewTidyService(cfg TidyConfig, deps TidyDeps) (*TidyService, error) {
|
||||
log: tidier.log,
|
||||
now: tidier.now,
|
||||
wait: waitTidyTimer,
|
||||
wake: make(chan struct{}, 1),
|
||||
runs: newTidyRunRegistry(defaultTidyRunTTL),
|
||||
}, nil
|
||||
}
|
||||
@@ -280,11 +296,19 @@ func (s *TidyService) loop(ctx context.Context) {
|
||||
defer s.wg.Done()
|
||||
s.catchUp(ctx)
|
||||
for {
|
||||
if !s.wait(ctx, s.nextDelay()) {
|
||||
return
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
// 上一轮攒下的「调优唤醒」标记在这里清掉:它全部的含义就是「把下一拍重算
|
||||
// 一遍」,因此必须在重算**之前**清空 —— 否则重算完发现标记还在,会自旋。
|
||||
s.drainWake()
|
||||
switch s.waitForTick(ctx) {
|
||||
case tidyTickStopped:
|
||||
// 父 ctx 被取消(Stop / 进程退出),或注入的假等待自报「停」:既有语义。
|
||||
return
|
||||
case tidyTickWokenByTuning:
|
||||
// 被**面板调优**唤醒:这一觉是按**旧**周期算出来的,现在作废 ⇒ 只重算
|
||||
// 下一拍,**绝不**因此跑一轮(唤醒不是「到点了」,判据仍只有 dueNow)。
|
||||
s.log.Debug("memory tidy heartbeat rescheduled by panel tuning",
|
||||
"now_hour", s.now().Hour())
|
||||
continue
|
||||
}
|
||||
// 到点即跑:间隔用的是**此刻**的时段(nextDelay 里现算的),因此跨过
|
||||
// 安静时段边界后,下一轮自动换档。判据再次复核是因为等待期间可能有别的
|
||||
@@ -330,21 +354,19 @@ func (s *TidyService) catchUp(ctx context.Context) {
|
||||
// nextDelay 用同一个函数,因此「下一拍的时间」与「到没到点」不可能各说各话。
|
||||
func (s *TidyService) dueNow() (bool, string) {
|
||||
now := s.now()
|
||||
s.mu.Lock()
|
||||
last := s.lastRun
|
||||
s.mu.Unlock()
|
||||
cfg, last := s.scheduleSnapshot()
|
||||
|
||||
if last.IsZero() {
|
||||
if s.cfg.inQuietHours(now) {
|
||||
if cfg.inQuietHours(now) {
|
||||
return true, "no_history_in_quiet_window"
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
if now.Sub(last) >= s.cfg.intervalAt(now) {
|
||||
if now.Sub(last) >= cfg.intervalAt(now) {
|
||||
return true, "interval_elapsed"
|
||||
}
|
||||
if s.cfg.inQuietHours(now) {
|
||||
if start := s.cfg.currentQuietStart(now); last.Before(start) {
|
||||
if cfg.inQuietHours(now) {
|
||||
if start := cfg.currentQuietStart(now); last.Before(start) {
|
||||
return true, "quiet_window_not_run"
|
||||
}
|
||||
}
|
||||
@@ -368,18 +390,18 @@ func (s *TidyService) dueNow() (bool, string) {
|
||||
// 把它挪回启动时算一次 ⇒ 白天/晚上的切换不会生效(本切片有测试钉住这条)。
|
||||
func (s *TidyService) nextDelay() time.Duration {
|
||||
now := s.now()
|
||||
s.mu.Lock()
|
||||
last := s.lastRun
|
||||
s.mu.Unlock()
|
||||
cfg, last := s.scheduleSnapshot()
|
||||
// lastRun 与 cfg 必须在**同一把锁**下取到同一版本(见 scheduleSnapshot),
|
||||
// 否则一次面板保存可能让这里拿到「新周期 + 旧历史」的混合体。
|
||||
|
||||
var next time.Time
|
||||
if last.IsZero() {
|
||||
next = s.cfg.nextQuietStart(now)
|
||||
if s.cfg.quietWindowDisabled() {
|
||||
next = now.Add(s.cfg.intervalAt(now))
|
||||
next = cfg.nextQuietStart(now)
|
||||
if cfg.quietWindowDisabled() {
|
||||
next = now.Add(cfg.intervalAt(now))
|
||||
}
|
||||
} else {
|
||||
next = last.Add(s.cfg.intervalAt(now))
|
||||
next = last.Add(cfg.intervalAt(now))
|
||||
}
|
||||
delay := next.Sub(now)
|
||||
if delay < minTidyTickDelay {
|
||||
@@ -388,6 +410,19 @@ func (s *TidyService) nextDelay() time.Duration {
|
||||
return delay
|
||||
}
|
||||
|
||||
// scheduleSnapshot 在锁下同时取「调度配置」与「上次运行时刻」。
|
||||
//
|
||||
// 🔴 为什么要有它(切片 MEMPANEL / M4 立):`cfg` 在这条特性之前是启动期冻结的,
|
||||
// 因此调度读(`nextDelay` / `dueNow`)直接读字段本来安全;面板调优开了「运行期改
|
||||
// cfg」这条路之后,调度读**必须**与写方(tidy_service_tuning.go 的 ApplyPanelTuning)
|
||||
// 同步,否则就是数据竞争(`-race` 可复现)。两样东西一起取,是为了让一次判定看到
|
||||
// 的是**同一个版本**(分开取可能落在两次不同的写入之间)。
|
||||
func (s *TidyService) scheduleSnapshot() (TidyConfig, time.Time) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.cfg, s.lastRun
|
||||
}
|
||||
|
||||
// runRound 跑一轮并把结果与审计留痕。单写者:runMu 串行化心跳与手动触发。
|
||||
func (s *TidyService) runRound(ctx context.Context, trigger string) (TidyStats, error) {
|
||||
s.runMu.Lock()
|
||||
|
||||
Reference in new issue
Block a user