| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768 |
- package loaders
- import (
- "context"
- "errors"
- "time"
- "github.com/zeromicro/go-zero/core/logx"
- )
- // cacheCleanTimeout 是 post-commit 缓存失效的默认硬超时。
- // 3s 足够覆盖正常 Redis 节点的 DEL/SUNION/Pipelined 三步互动,同时给悬挂 goroutine 兜底。
- const cacheCleanTimeout = 3 * time.Second
- // DetachCacheCleanCtx 为 post-commit 阶段的缓存失效构造一个独立 ctx(审计 L-R13-5 方案 A):
- // - 用 context.WithoutCancel(parent) 切断"HTTP 请求 ctx 被 client 断连 / server 超时取消 →
- // 紧跟在事务提交之后的 Clean / BatchDel / InvalidateProfileCache 半途被打断"的联动——
- // 事务已经落盘,这几次 Redis 写属于**后置补偿**,不应该随请求生命周期一起结束;
- // - 套 3s 硬 timeout 兜底,防止 Redis 慢 / 挂起时后台 goroutine 悬挂不退。
- //
- // 典型用法:
- //
- // if err := transactionBody(...); err != nil { return err }
- // cleanCtx, cancel := loaders.DetachCacheCleanCtx(l.ctx)
- // defer cancel()
- // l.svcCtx.UserDetailsLoader.Clean(cleanCtx, userId)
- // l.svcCtx.SysUserModel.InvalidateProfileCache(cleanCtx, userId, username)
- //
- // 原调用链里的 logx trace / 调用栈元信息会通过 WithoutCancel 保留,便于把后置补偿的日志与
- // 原请求 trace 关联上;parent 只被剥离了 cancel/deadline,其它值(trace id / tenant id 等)仍然在。
- func DetachCacheCleanCtx(parent context.Context) (context.Context, context.CancelFunc) {
- return context.WithTimeout(context.WithoutCancel(parent), cacheCleanTimeout)
- }
- // isCtxCanceledErr 判定错误是否源自 ctx 取消 / 超时,用于把 post-commit 缓存失效失败里
- // "请求中断"与"Redis 真的挂了"拆开(审计 L-R13-5 方案 B)。
- func isCtxCanceledErr(err error) bool {
- return errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)
- }
- // logCacheInvalidationErr 统一"缓存失效失败"的日志打点。
- // - ctx 取消 / 超时:打一条带 `audit=cache_invalidation_skipped_due_to_ctx_cancel` tag 的
- // Errorw,运维可以按 tag 单独建看板而不污染"Redis 真挂了"的告警;
- // - 其它错误:审计 L-R18-8 改用 Errorw 带 `audit=cache_invalidation_failed` tag,方便
- // 运维按 tag 在日志平台聚合告警(项目当前未集成 Prometheus,以日志 tag 作为告警输入面,
- // 未来接入 metrics 时再把 tag 聚合转成 counter 即可,不需要二次改造业务代码)。
- //
- // scope 形如 "userDetailsLoader.BatchDel",detail 可附带 key / 业务上下文。
- func logCacheInvalidationErr(ctx context.Context, scope, detail string, err error) {
- if err == nil {
- return
- }
- if isCtxCanceledErr(err) {
- logx.WithContext(ctx).Errorw("cache invalidation skipped: ctx canceled",
- logx.Field("audit", "cache_invalidation_skipped_due_to_ctx_cancel"),
- logx.Field("scope", scope),
- logx.Field("detail", detail),
- logx.Field("err", err.Error()),
- )
- return
- }
- logx.WithContext(ctx).Errorw("cache invalidation failed",
- logx.Field("audit", "cache_invalidation_failed"),
- logx.Field("scope", scope),
- logx.Field("detail", detail),
- logx.Field("err", err.Error()),
- )
- }
|