fix and update

This commit is contained in:
tao.chen
2026-08-26 10:42:53 +08:00
parent 64b5d9f7f3
commit 8c0e14987e
3 changed files with 29 additions and 88 deletions
+5 -4
View File
@@ -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"
)
+1 -1
View File
@@ -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)
+18 -78
View File
@@ -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 + debounceDurationpendingCommit 由调用方预先设置。
// 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)
})
}
func handleWebhook(c *gin.Context) {
@@ -122,28 +77,29 @@ 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 !entry.lastExec.IsZero() && now.Sub(entry.lastExec) < debounceDuration {
remainingSeconds := max(int((debounceDuration - now.Sub(entry.lastExec)).Seconds()), 0)
entry.mu.Unlock()
msg := fmt.Sprintf("3 分钟内频繁提交被拦截, 剩余冷却时间: %d 秒", remainingSeconds)
log.Printf("[DEBOUNCE] 跳过执行 [%s] %s\n", lockKey, msg)
c.JSON(http.StatusOK, gin.H{
"status": "debounced",
"message": msg,
"remaining_seconds": remainingSeconds,
})
return
}
if firstRun {
// 立即执行本次请求的 commitID;同时安排尾部补刀捕获期间新 push
entry.lastExec = now
entry.pendingCommit = commitID // 兜底:timer fire 时无新 push 则用本次 commitID
entry.scheduleLocked(lockKey)
entry.mu.Unlock()
log.Printf("[INFO] 开始执行脚本: %s (仓库: %s, 分支: %s, commit: %s)\n",
@@ -156,20 +112,4 @@ func handleWebhook(c *gin.Context) {
"ref": ref,
"script": scriptPath,
})
return
}
// 防抖窗口内(活动定时器存在):续命定时器,使用最新 commitID
remainingSeconds := max(int((debounceDuration - now.Sub(entry.lastExec)).Seconds()), 0)
entry.pendingCommit = commitID
entry.scheduleLocked(lockKey)
entry.mu.Unlock()
msg := fmt.Sprintf("触发太频繁,防抖拦截中(5分钟限制),剩余等待时间: %d 秒", remainingSeconds)
log.Printf("[DEBOUNCE] 跳过执行 [%s] -> %s (已安排尾部补刀)\n", lockKey, msg)
c.JSON(http.StatusOK, gin.H{
"status": "debounced",
"message": msg,
"remaining_seconds": remainingSeconds,
})
}