diff --git a/config.go b/config.go index 0a9518b..d567cbe 100644 --- a/config.go +++ b/config.go @@ -16,10 +16,11 @@ type Config struct { var ( globalConfig Config - // pendingDebounceMap 防抖窗口内的待执行任务,key: "repoName:ref", value: *debounceEntry - pendingDebounceMap sync.Map - // 防抖冷却时间限制:5 分钟 - debounceDuration = 5 * time.Minute + // lastExecMap 冷却去重状态,key: "repoName:ref", value: *debounceEntry + // 记录每个 key 上次实际执行时刻,冷却窗口内到达的 push 直接丢弃 + lastExecMap sync.Map + // 防抖冷却时间限制:3 分钟 + debounceDuration = 3 * time.Minute // bark 推送消息 pushURL = "https://bark.maimaicuizhiji.top/push" ) diff --git a/main.go b/main.go index 0088bbc..a41b46a 100644 --- a/main.go +++ b/main.go @@ -30,7 +30,7 @@ func main() { r.POST("/webhook", handleWebhook) listenAddr := fmt.Sprintf(":%d", *portFlag) - log.Printf("GitLab Webhook 服务已启动 (带 5 分钟防抖),监听端口 %d ...\n", *portFlag) + log.Printf("GitLab Webhook 服务已启动 (3 分钟防抖去重),监听端口 %d ...\n", *portFlag) if err := r.Run(listenAddr); err != nil { log.Fatalf("服务启动失败: %v", err) diff --git a/webhook.go b/webhook.go index 61b119d..d98cdec 100644 --- a/webhook.go +++ b/webhook.go @@ -20,56 +20,11 @@ type GitLabPayload struct { } `json:"project"` } -// debounceEntry 单个 key 的防抖状态。 -// lastExec:上次实际执行时刻(首次或 timer fire)。 -// timer:当前活动定时器,为 nil 表示已 fire 或未安排。 -// pendingCommit:防抖窗口内累计的最新 commitID,timer fire 时用它执行。 -// 其余字段缓存以便 timer fire 回调无需外部参数。 +// debounceEntry 单个 key 的冷却去重状态。 +// lastExec:上次实际执行时刻;冷却窗口内到达的 push 直接丢弃,无补跑。 type debounceEntry struct { - mu sync.Mutex - lastExec time.Time - timer *time.Timer - pendingCommit string - - scriptPath string - repoName string - ref string -} - -// scheduleLocked 续命或新建防抖定时器(必须持有 entry.mu)。 -// fireAt = now + debounceDuration,pendingCommit 由调用方预先设置。 -// fire 闭包内通过指针身份做代际校验:若已被新定时器顶替,本次不执行。 -func (e *debounceEntry) scheduleLocked(lockKey string) { - if e.timer != nil { - e.timer.Stop() - } - - e.timer = time.AfterFunc(debounceDuration, func() { - e.mu.Lock() - if e.timer == nil { - e.mu.Unlock() - return - } - fired := e.timer - e.mu.Unlock() - - // 代际校验:再次拿锁确认 fired 仍是当前定时器 - e.mu.Lock() - if fired == nil || e.timer != fired { - e.mu.Unlock() - return // 已被新 push 顶替 - } - e.timer = nil - e.lastExec = time.Now() - finalCommit := e.pendingCommit - sp, rn, rf := e.scriptPath, e.repoName, e.ref - pendingDebounceMap.Delete(lockKey) - e.mu.Unlock() - - log.Printf("[DEBOUNCE-FIRE] 防抖窗口结束,补刀执行 [%s] (使用最新 commit %s)\n", - lockKey, shortCommit(finalCommit)) - go runScript(sp, rn, rf, finalCommit) - }) + mu sync.Mutex + lastExec time.Time } func handleWebhook(c *gin.Context) { @@ -122,54 +77,39 @@ func handleWebhook(c *gin.Context) { return } - // 6. 防抖 + 尾部补刀 + // 6. 冷却去重:窗口外 push 立即执行并刷新 lastExec,窗口内 push 直接丢弃 lockKey := fmt.Sprintf("%s:%s", repoName, ref) - now := time.Now() - - entryVal, _ := pendingDebounceMap.LoadOrStore(lockKey, &debounceEntry{ - scriptPath: scriptPath, - repoName: repoName, - ref: ref, - }) + entryVal, _ := lastExecMap.LoadOrStore(lockKey, &debounceEntry{}) entry := entryVal.(*debounceEntry) entry.mu.Lock() + now := time.Now() - // 是否首次/窗口外执行:没有活动定时器即视为窗口外 - // (timer==nil 涵盖首次 lastExec==0 和上一次 timer fire 后两种情况) - firstRun := entry.timer == nil - - if firstRun { - // 立即执行本次请求的 commitID;同时安排尾部补刀捕获期间新 push - entry.lastExec = now - entry.pendingCommit = commitID // 兜底:timer fire 时无新 push 则用本次 commitID - entry.scheduleLocked(lockKey) + if !entry.lastExec.IsZero() && now.Sub(entry.lastExec) < debounceDuration { + remainingSeconds := max(int((debounceDuration - now.Sub(entry.lastExec)).Seconds()), 0) entry.mu.Unlock() - log.Printf("[INFO] 开始执行脚本: %s (仓库: %s, 分支: %s, commit: %s)\n", - scriptPath, repoName, ref, shortCommit(commitID)) - go runScript(scriptPath, repoName, ref, commitID) - + msg := fmt.Sprintf("3 分钟内频繁提交被拦截, 剩余冷却时间: %d 秒", remainingSeconds) + log.Printf("[DEBOUNCE] 跳过执行 [%s] %s\n", lockKey, msg) c.JSON(http.StatusOK, gin.H{ - "status": "triggered", - "repository": repoName, - "ref": ref, - "script": scriptPath, + "status": "debounced", + "message": msg, + "remaining_seconds": remainingSeconds, }) return } - // 防抖窗口内(活动定时器存在):续命定时器,使用最新 commitID - remainingSeconds := max(int((debounceDuration - now.Sub(entry.lastExec)).Seconds()), 0) - entry.pendingCommit = commitID - entry.scheduleLocked(lockKey) + entry.lastExec = now entry.mu.Unlock() - msg := fmt.Sprintf("触发太频繁,防抖拦截中(5分钟限制),剩余等待时间: %d 秒", remainingSeconds) - log.Printf("[DEBOUNCE] 跳过执行 [%s] -> %s (已安排尾部补刀)\n", lockKey, msg) + log.Printf("[INFO] 开始执行脚本: %s (仓库: %s, 分支: %s, commit: %s)\n", + scriptPath, repoName, ref, shortCommit(commitID)) + go runScript(scriptPath, repoName, ref, commitID) + c.JSON(http.StatusOK, gin.H{ - "status": "debounced", - "message": msg, - "remaining_seconds": remainingSeconds, + "status": "triggered", + "repository": repoName, + "ref": ref, + "script": scriptPath, }) }