Some improvements

This commit is contained in:
Lunny Xiao 2025-03-25 11:41:29 -07:00
parent ae0af8ea5b
commit 12237939cb
2 changed files with 51 additions and 48 deletions

View File

@ -4,6 +4,7 @@
package repo
import (
std_ctx "context"
"errors"
"net/http"
@ -27,7 +28,7 @@ func Contributors(ctx *context.Context) {
// ContributorsData renders JSON of contributors along with their weekly commit statistics
func ContributorsData(ctx *context.Context) {
if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil {
if errors.Is(err, contributors_service.ErrAwaitGeneration) {
if errors.Is(err, contributors_service.ErrAwaitGeneration) || errors.Is(err, std_ctx.DeadlineExceeded) {
ctx.Status(http.StatusAccepted)
return
}

View File

@ -11,7 +11,6 @@ import (
"os"
"strconv"
"strings"
"sync"
"time"
"code.gitea.io/gitea/models/avatars"
@ -20,7 +19,7 @@ import (
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/globallock"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
)
@ -32,8 +31,7 @@ const (
var (
ErrAwaitGeneration = errors.New("generation took longer than ")
awaitGenerationTime = time.Second * 5
generateLock = sync.Map{}
awaitGenerationTime = time.Second * 60
)
type WeekData struct {
@ -81,41 +79,44 @@ func findLastSundayBeforeDate(dateStr string) (string, error) {
func GetContributorStats(ctx context.Context, cache cache.StringCache, repo *repo_model.Repository, revision string) (map[string]*ContributorData, error) {
// as GetContributorStats is resource intensive we cache the result
cacheKey := fmt.Sprintf(contributorStatsCacheKey, repo.FullName(), revision)
if !cache.IsExist(cacheKey) {
genReady := make(chan struct{})
// dont start multiple async generations
_, run := generateLock.Load(cacheKey)
if run {
return nil, ErrAwaitGeneration
if cache.IsExist(cacheKey) {
// TODO: renew timeout of cache cache.UpdateTimeout(cacheKey, contributorStatsCacheTimeout)
var res map[string]*ContributorData
if _, cacheErr := cache.GetJSON(cacheKey, &res); cacheErr != nil {
return nil, fmt.Errorf("cached error: %w", cacheErr.ToError())
}
return res, nil
}
generateLock.Store(cacheKey, struct{}{})
// run generation async
go generateContributorStats(genReady, cache, cacheKey, repo, revision)
// dont start multiple async generations
releaser, err := globallock.Lock(ctx, cacheKey)
if err != nil {
return nil, err
}
defer releaser()
select {
case <-time.After(awaitGenerationTime):
return nil, ErrAwaitGeneration
case <-genReady:
// we got generation ready before timeout
break
// set a timeout for the generation
ctx, cancel := context.WithTimeout(ctx, awaitGenerationTime)
defer cancel()
// run generation async
res, err := generateContributorStats(ctx, cache, cacheKey, repo, revision)
if err != nil {
switch {
case errors.Is(err, context.DeadlineExceeded):
_ = cache.PutJSON(cacheKey, fmt.Errorf("generateContributorStats: %w", ErrAwaitGeneration), contributorStatsCacheTimeout)
default:
_ = cache.PutJSON(cacheKey, fmt.Errorf("generateContributorStats: %w", err), contributorStatsCacheTimeout)
}
return nil, err
}
// TODO: renew timeout of cache cache.UpdateTimeout(cacheKey, contributorStatsCacheTimeout)
var res map[string]*ContributorData
if _, cacheErr := cache.GetJSON(cacheKey, &res); cacheErr != nil {
return nil, fmt.Errorf("cached error: %w", cacheErr.ToError())
}
_ = cache.PutJSON(cacheKey, res, contributorStatsCacheTimeout)
return res, nil
}
// getExtendedCommitStats return the list of *ExtendedCommitStats for the given revision
func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int */) ([]*ExtendedCommitStats, error) {
baseCommit, err := repo.GetCommit(revision)
if err != nil {
return nil, err
}
func getExtendedCommitStats(ctx context.Context, repoPath string, baseCommit *git.Commit, revision string /*, limit int */) ([]*ExtendedCommitStats, error) {
stdoutReader, stdoutWriter, err := os.Pipe()
if err != nil {
return nil, err
@ -131,8 +132,8 @@ func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int
var extendedCommitStats []*ExtendedCommitStats
stderr := new(strings.Builder)
err = gitCmd.Run(repo.Ctx, &git.RunOpts{
Dir: repo.Path,
err = gitCmd.Run(ctx, &git.RunOpts{
Dir: repoPath,
Stdout: stdoutWriter,
Stderr: stderr,
PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
@ -140,6 +141,12 @@ func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int
scanner := bufio.NewScanner(stdoutReader)
for scanner.Scan() {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
line := strings.TrimSpace(scanner.Text())
if line != "---" {
continue
@ -199,27 +206,26 @@ func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int
return extendedCommitStats, nil
}
func generateContributorStats(genDone chan struct{}, cache cache.StringCache, cacheKey string, repo *repo_model.Repository, revision string) {
ctx := graceful.GetManager().HammerContext()
func generateContributorStats(ctx context.Context, cache cache.StringCache, cacheKey string, repo *repo_model.Repository, revision string) (map[string]*ContributorData, error) {
gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
if err != nil {
_ = cache.PutJSON(cacheKey, fmt.Errorf("OpenRepository: %w", err), contributorStatsCacheTimeout)
return
return nil, err
}
defer closer.Close()
if len(revision) == 0 {
revision = repo.DefaultBranch
}
extendedCommitStats, err := getExtendedCommitStats(gitRepo, revision)
baseCommit, err := gitRepo.GetCommit(revision)
if err != nil {
_ = cache.PutJSON(cacheKey, fmt.Errorf("ExtendedCommitStats: %w", err), contributorStatsCacheTimeout)
return
return nil, err
}
extendedCommitStats, err := getExtendedCommitStats(ctx, repo.RepoPath(), baseCommit, revision)
if err != nil {
return nil, err
}
if len(extendedCommitStats) == 0 {
_ = cache.PutJSON(cacheKey, fmt.Errorf("no commit stats returned for revision '%s'", revision), contributorStatsCacheTimeout)
return
return nil, fmt.Errorf("no commit stats returned for revision '%s'", revision)
}
layout := time.DateOnly
@ -300,9 +306,5 @@ func generateContributorStats(genDone chan struct{}, cache cache.StringCache, ca
total.TotalCommits++
}
_ = cache.PutJSON(cacheKey, contributorsCommitStats, contributorStatsCacheTimeout)
generateLock.Delete(cacheKey)
if genDone != nil {
genDone <- struct{}{}
}
return contributorsCommitStats, nil
}