Files
gitea/modules/indexer/stats/db.go
T
5c68ba3e0a fix(repo): avoid deadlock when updating language stats concurrently (#40167)
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: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-11-03 16:18:27 +00:00

93 lines
2.7 KiB
Go

// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package stats
import (
"fmt"
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"
"gitea.dev/modules/setting"
)
// DBIndexer implements Indexer interface to use database's like search
type DBIndexer struct{}
// Index repository status function
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
}
if repo.IsEmpty {
return nil
}
status, err := repo_model.GetIndexerStatus(ctx, repo, repo_model.RepoIndexerTypeStats)
if err != nil {
return err
}
gitRepo, err := git.OpenRepository(ctx, repo)
if err != nil {
if err.Error() == "no such file or directory" {
return nil
}
return err
}
defer gitRepo.Close()
// Get latest commit for default branch
commitID, err := gitRepo.GetBranchCommitID(ctx, repo.DefaultBranch)
if err != nil {
if git.IsErrBranchNotExist(err) || git.IsErrNotExist(err) || setting.IsInTesting {
log.Debug("Unable to get commit ID for default branch %s in %s ... skipping this repository", repo.DefaultBranch, repo.FullName())
return nil
}
log.Error("Unable to get commit ID for default branch %s in %s. Error: %v", repo.DefaultBranch, repo.FullName(), err)
return err
}
// Do not recalculate stats if already calculated for this commit
if status.CommitSha == commitID {
return nil
}
// Calculate and save language statistics to database
stats, err := languagestats.GetLanguageStats(ctx, gitRepo, commitID)
if err != nil {
if !setting.IsInTesting {
log.Error("Unable to get language stats for ID %s for default branch %s in %s. Error: %v", commitID, repo.DefaultBranch, repo.FullName(), err)
}
return err
}
err = repo_model.UpdateLanguageStats(ctx, repo, commitID, stats)
if err != nil {
log.Error("Unable to update language stats for ID %s for default branch %s in %s. Error: %v", commitID, repo.DefaultBranch, repo.FullName(), err)
return err
}
log.Debug("DBIndexer completed language stats for ID %s for default branch %s in %s. stats count: %d", commitID, repo.DefaultBranch, repo.FullName(), len(stats))
return nil
}
// Close dummy function
func (db *DBIndexer) Close() {
}