From a4cad571746f20f425eded9a2d56cd948dbcff06 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Tue, 3 Nov 2026 09:02:11 -0800 Subject: [PATCH] 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 Co-authored-by: silverwind Co-authored-by: wxiaoguang --- models/repo/language_stats.go | 2 +- models/repo/language_stats_test.go | 28 ++++++++++++++++++++++++++++ modules/indexer/stats/db.go | 8 ++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 models/repo/language_stats_test.go diff --git a/models/repo/language_stats.go b/models/repo/language_stats.go index daa2ab80e41..6ede932bd45 100644 --- a/models/repo/language_stats.go +++ b/models/repo/language_stats.go @@ -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 } diff --git a/models/repo/language_stats_test.go b/models/repo/language_stats_test.go new file mode 100644 index 00000000000..9a07a40dea3 --- /dev/null +++ b/models/repo/language_stats_test.go @@ -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) + } +} diff --git a/modules/indexer/stats/db.go b/modules/indexer/stats/db.go index 3f5ba15d0cb..3198b090730 100644 --- a/modules/indexer/stats/db.go +++ b/modules/indexer/stats/db.go @@ -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