- 新包 internal/runtime/events:Tier/Category/Platform/ChannelType/ActorRole 具名常量 + Event/Filter/Counter + Store 六方法 - 留存口径:T2/T3 逐条落行;T1 只计数;unknown / decode_error / link 三例外逐条落行(链路是否活着是事故定案唯一证据) - 计数:内存聚合 + 上限 512 的有界缓冲 + 满即整批 upsert,落盘失败回填而不丢;GC:保留期 720h/间隔 1h,≤0 = 关闭 - 迁移 0021_channel_events(两表 + 6 索引,含计数 upsert 唯一键);Event 无 flag/正文/URL 字段(design/26 §4.2 红线)
122 lines
5.1 KiB
Go
122 lines
5.1 KiB
Go
package events
|
||
|
||
// 本文件归属**保留期清扫(Sweep)**(design/26 §4.4;切片 E1)。它逐字沿用
|
||
// `internal/runtime/documents/gc.go` + `cmd/cyrene/wire_document_gc.go` 的既有 GC
|
||
// 范式(design/26 §4.4 明写「不另造」),三条取舍也照抄:
|
||
//
|
||
// 1. **启动期扫一次、此后按间隔定时扫**(间隔默认 DefaultGCInterval)。启动期那次
|
||
// 与定时循环都**不**阻塞启动、不绑在磁盘上 —— 常驻循环住在组合根(E4 的
|
||
// `wire_channel_events.go` 逐字对齐 `wire_document_gc.go`),本包只提供
|
||
// `Sweep` 与它需要的三个口径(Enabled / Interval / Retention)。
|
||
// 2. **失败只记 WARN、绝不让进程退出**:`Sweep` 把查询/删除失败**上抛**给调用方
|
||
// (组合根记 WARN 后继续等下一轮);已经删掉的行不会因为后续批次失败而「回滚」,
|
||
// 未删的行也不会被清空 —— 一轮失败就只是「这一轮没扫干净」。
|
||
// 3. **≤0 = 关闭**(显式),关闭原因在 NewStore 里记进日志 —— 「配了却不生效」
|
||
// 不存在(与文档 GC 同口径)。
|
||
//
|
||
// 清理条件只有一条:`created_at < now − 保留期` 的 `channel_events` 行逐批删除。
|
||
// **计数表不参与 GC**:它本身就是「历史已浓缩成一行」的形态,删了就再也答不上
|
||
// 「上游推过没有」——那正是本切片存在的理由(design/26 §4.4)。
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"time"
|
||
)
|
||
|
||
const (
|
||
// DefaultGCInterval 是 `CHANNEL_EVENT_GC_INTERVAL` 的默认值:每小时清扫一次。
|
||
DefaultGCInterval = time.Hour
|
||
|
||
// DefaultRetention 是 `CHANNEL_EVENT_RETENTION` 的默认值:30 天。它比文档保留期
|
||
// (168h)长得多,因为事件表的核心用途是**事后定案**(design/26 §0.2)——
|
||
// 一周太短,事故复盘常常发生在一周之后(§4.4)。
|
||
DefaultRetention = 720 * time.Hour
|
||
|
||
// sweepBatchSize 是单批删除的行数上限;maxSweepBatches 是一轮清扫的批数上限。
|
||
// 两者一起把「一轮清扫」的时长与单次内存占用**有界**(AGENTS §10 的压测纪律:
|
||
// 有界,不靠堆进程):积压百万行时不是一次 DELETE 全表,而是每轮最多
|
||
// maxSweepBatches × sweepBatchSize 行,剩下的留给下一轮。
|
||
sweepBatchSize = 500
|
||
maxSweepBatches = 100
|
||
)
|
||
|
||
// SweepStats 是一轮清扫的汇总(成功件只记总数,绝不记任何行内容)。
|
||
type SweepStats struct {
|
||
// Batches 是实际执行的删除批数。
|
||
Batches int
|
||
// Deleted 是删除的过期行数。
|
||
Deleted int64
|
||
}
|
||
|
||
// Enabled 报告清扫是否真的会删行(保留期与间隔都 > 0)。
|
||
func (s *SQLStore) Enabled() bool { return s != nil && s.gcEnabled }
|
||
|
||
// Interval 返回定时清扫间隔(关闭时为 0)。
|
||
func (s *SQLStore) Interval() time.Duration {
|
||
if s == nil {
|
||
return 0
|
||
}
|
||
return s.gcInterval
|
||
}
|
||
|
||
// Retention 返回事件行保留期(关闭时为 0)。
|
||
func (s *SQLStore) Retention() time.Duration {
|
||
if s == nil {
|
||
return 0
|
||
}
|
||
return s.retention
|
||
}
|
||
|
||
// sweepSQL 按时间戳删一批过期行。子查询带 `ORDER BY created_at LIMIT ?`,因此它
|
||
// 吃得上 `idx_channel_events_created_at`,且每批的扫描面有界(design/26 §4.4)。
|
||
// 时间戳用定宽 UTC 字符串比较:字典序等价于时间序(见 store.go 的口径注释)。
|
||
const sweepSQL = `DELETE FROM channel_events WHERE id IN (
|
||
SELECT id FROM channel_events WHERE created_at < ? ORDER BY created_at LIMIT ?)`
|
||
|
||
// Sweep 按保留期清扫 `channel_events` 的过期行,返回一轮汇总。
|
||
//
|
||
// 关闭时(Enabled() 为 false)是**空操作**:返回零值与 nil 错误、不碰任何一行。
|
||
// 这与「配了却不生效」是两回事 —— 关闭原因在 NewStore 里已记 WARN。
|
||
//
|
||
// 失败语义:查询/删除失败返回错误并**带上已完成的统计**(调用方据此知道这一轮
|
||
// 走到了哪一步,记 WARN 后等下一轮)。ctx 取消时立刻停下:**已删的保持删除、
|
||
// 未删的绝不因此被清空** —— 这是「失败不清空」在代码里的落点。
|
||
func (s *SQLStore) Sweep(ctx context.Context) (SweepStats, error) {
|
||
if !s.Enabled() {
|
||
return SweepStats{}, nil
|
||
}
|
||
cutoff := sqliteTimeStamp(s.now().Add(-s.retention))
|
||
var stats SweepStats
|
||
for i := 0; i < maxSweepBatches; i++ {
|
||
if err := ctx.Err(); err != nil {
|
||
return stats, fmt.Errorf("events: sweep: %w", err)
|
||
}
|
||
deleted, err := s.sweepBatch(ctx, cutoff)
|
||
if err != nil {
|
||
return stats, err
|
||
}
|
||
stats.Batches++
|
||
stats.Deleted += deleted
|
||
if deleted < int64(sweepBatchSize) {
|
||
break
|
||
}
|
||
}
|
||
s.log.Info("channel event sweep",
|
||
"deleted", stats.Deleted, "batches", stats.Batches, "retention", s.retention.String())
|
||
return stats, nil
|
||
}
|
||
|
||
// sweepBatch 删一批过期行并返回本批删掉的行数。
|
||
func (s *SQLStore) sweepBatch(ctx context.Context, cutoff string) (int64, error) {
|
||
res, err := s.db.ExecContext(ctx, sweepSQL, cutoff, sweepBatchSize)
|
||
if err != nil {
|
||
return 0, fmt.Errorf("events: sweep 删过期行: %w", err)
|
||
}
|
||
n, err := res.RowsAffected()
|
||
if err != nil {
|
||
return 0, fmt.Errorf("events: sweep 读取删除行数: %w", err)
|
||
}
|
||
return n, nil
|
||
}
|