fix(repo): avoid deadlock when updating language stats concurrently (#40167) (#40202)

Backport #40167 by @perfectra1n

The `repo_stats_update` queue only deduplicates waiting items, so
several workers can index the same repository at once, then
`UpdateLanguageStats` can lead to transaction deadlock.

Use a global lock to avoid concurrent updates, and also helps
https://github.com/go-gitea/gitea/pull/39342 to avoid duplicate updates.

Co-authored-by: Jon Fuller <jonfuller2012@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-11-03 17:02:11 +00:00
1 parent ad8fa1770a
commit a4cad57174
3 files changed
+37 -1

No files matched your search

+1 -1
View File
@@ -151,7 +151,7 @@ func UpdateLanguageStats(ctx context.Context, repo *Repository, commitID string,
var topLang string
var s int64
for lang, size := range stats {
if size > s {
if size > s || (size == s && lang < topLang) {
s = size
topLang = lang
}
+28
View File
@@ -0,0 +1,28 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo_test
import (
"testing"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUpdateLanguageStatsPrimaryTie(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
require.NoError(t, repo_model.UpdateLanguageStats(t.Context(), repo, "1234567890", map[string]int64{"Go": 10, "C": 10}))
stats, err := repo_model.GetLanguageStats(t.Context(), repo)
require.NoError(t, err)
require.Len(t, stats, 2)
for _, stat := range stats {
assert.Equal(t, stat.Language == "C", stat.IsPrimary, stat.Language)
}
}
+8
View File
@@ -9,6 +9,7 @@ import (
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/git"
"gitea.dev/modules/git/languagestats"
"gitea.dev/modules/globallock"
"gitea.dev/modules/graceful"
"gitea.dev/modules/log"
"gitea.dev/modules/process"
@@ -23,6 +24,13 @@ func (db *DBIndexer) Index(id int64) error {
ctx, _, finished := process.GetManager().AddContext(graceful.GetManager().ShutdownContext(), fmt.Sprintf("Stats.DB Index Repo[%d]", id))
defer finished()
// the unique queue doesn't stop two workers from indexing the same repository at once
releaser, err := globallock.Lock(ctx, fmt.Sprintf("repo_stats_index_%d", id))
if err != nil {
return err
}
defer releaser()
repo, err := repo_model.GetRepositoryByID(ctx, id)
if err != nil {
return err