fix(actions): deadlock between PrepareRunAndInsert and UpdateTaskByState (#37692)

Fix #36234

## Bug

Logs show `PrepareRunAndInsert: InsertRun: Error 1213: Deadlock found`,
which `handleWorkflows` silently swallows via `log.Error + continue`, so
the triggered run is dropped.

## Root cause

The path `UpdateRun -> UpdateRepoRunsNumbers` runs the following SQL
inside every status-changing transaction:

```sql
UPDATE repository
SET num_action_runs        = (SELECT count(*) FROM action_run WHERE repo_id = N),
    num_closed_action_runs = (SELECT count(*) FROM action_run WHERE repo_id = N AND status IN (...))
WHERE id = N;
```

On any DB that treats subqueries inside an UPDATE as locking reads, this
statement takes locks in two steps:

1. The outer UPDATE acquires an X lock on `repository[id=N]`
2. The embedded SELECT subqueries are evaluated as locking reads, taking
S locks on every `action_run` row matching `repo_id = N`

Two such concurrent transactions form a cycle via `repository[N]`:

| Tx | Holds | Wants | Blocked by |
|---|---|---|---|
| A: `PrepareRunAndInsert` (push trigger) | X on inserted `action_run`
row R_A; X on `repository[N]` (outer UPDATE already through step 1) | S
on `action_run` rows for repo N (subquery, step 2) | B's X lock on R_B |
| B: `UpdateTaskByState` (runner callback) | X on `action_run` row R_B
(from `UpdateRun`) | X on `repository[N]` (outer UPDATE, step 1) | A's X
lock on `repository[N]` |
| **Cycle** | A waits for R_B; B waits for `repository[N]` | | deadlock
error -> `handleWorkflows` swallows -> run lost |


PostgreSQL's MVCC reads do not take these locks and SQLite serializes
writers, so the symptom only surfaces on MySQL/MSSQL.

## Fix

Split `UpdateRepoRunsNumbers` into small SQLs to avoid locking reads and
move it out of DB transactions.

---------

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Nicolas <bircni@icloud.com>
This commit is contained in:
Zettat123
2026-05-15 08:39:18 +00:00
committed by GitHub
co-authored by wxiaoguang Nicolas
parent f9b7b65371
commit cf0f25b798
4 changed files with 35 additions and 39 deletions
+1 -2
View File
@@ -29,8 +29,7 @@ func TestUpdateRepoRunsNumbers(t *testing.T) {
assert.Equal(t, 2, repo.NumClosedActionRuns)
// now update will correct them, only num_actionr_runs and num_closed_action_runs should be updated
err = UpdateRepoRunsNumbers(t.Context(), repo)
assert.NoError(t, err)
UpdateRepoRunsNumbers(t.Context(), repo.ID)
repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
assert.Equal(t, 4, repo.NumActionRuns)
assert.Equal(t, 3, repo.NumClosedActionRuns)