forked from jzerebecki/autogits
common: fix three bugs in the timeline cache
1. Double-check locking (TOCTOU) GetTimeline dropped the read lock, then acquired the write lock to fetch fresh data, but never re-checked the cache after the write lock was acquired. A second goroutine could pass the stale-check under the read lock at the same time, resulting in both goroutines fetching the full timeline independently and overwriting each other's result. Add a re-check immediately after acquiring the write lock so only the first writer fetches. 2. LastCachedTime used the wrong item The incremental fetch used data[0].Updated as the high-water mark for the Since filter, but data is sorted in descending Created order so data[0] is the most-recently-created entry, whose Updated timestamp may not be the latest across all cached items. Switch to a linear scan over all items to find the true maximum Updated value, matching the fix in origin/fix/timeline-cache-race. 3. Cache not invalidated after RequestReviews / UnrequestReview After the bot called RequestReviews or UnrequestReview, the timeline cache for that PR was left hot for up to 5 seconds. Any subsequent GetTimeline call within that window returned stale data that did not include the just-created review_requested / review_request_removed entry, causing FindMissingAndExtraReviewers to make decisions based on outdated reviewer state. Call ResetTimelineCache on success in both methods so the next read always sees the mutation.
This commit is contained in:
@@ -768,6 +768,10 @@ func (gitea *GiteaTransport) RequestReviews(pr *models.PullRequest, reviewers ..
|
||||
return nil, fmt.Errorf("Cannot create pull request reviews: %w", err)
|
||||
}
|
||||
|
||||
// Invalidate the timeline cache so the next GetTimeline call reflects
|
||||
// the newly created review_requested entry.
|
||||
gitea.ResetTimelineCache(pr.Base.Repo.Owner.UserName, pr.Base.Repo.Name, pr.Index)
|
||||
|
||||
return review.GetPayload(), nil
|
||||
}
|
||||
|
||||
@@ -776,6 +780,13 @@ func (gitea *GiteaTransport) UnrequestReview(org, repo string, id int64, reviwer
|
||||
repository.NewRepoDeletePullReviewRequestsParams().WithOwner(org).WithRepo(repo).WithIndex(id).WithBody(&models.PullReviewRequestOptions{
|
||||
Reviewers: reviwers,
|
||||
}), gitea.transport.DefaultAuthentication)
|
||||
|
||||
if err == nil {
|
||||
// Invalidate the timeline cache so the next GetTimeline call reflects
|
||||
// the newly created review_request_removed entry.
|
||||
gitea.ResetTimelineCache(org, repo, id)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -861,24 +872,31 @@ func (gitea *GiteaTransport) GetTimeline(org, repo string, idx int64) ([]*models
|
||||
prID := fmt.Sprintf("%s/%s!%d", org, repo, idx)
|
||||
giteaTimelineCacheMutex.RLock()
|
||||
TimelineCache, IsCached := giteaTimelineCache[prID]
|
||||
var LastCachedTime strfmt.DateTime
|
||||
if IsCached {
|
||||
l := len(TimelineCache.data)
|
||||
if l > 0 {
|
||||
LastCachedTime = TimelineCache.data[0].Updated
|
||||
}
|
||||
|
||||
// cache data for 5 seconds
|
||||
if TimelineCache.lastCheck.Add(time.Second*5).Compare(time.Now()) > 0 {
|
||||
giteaTimelineCacheMutex.RUnlock()
|
||||
return TimelineCache.data, nil
|
||||
}
|
||||
if IsCached && TimelineCache.lastCheck.Add(time.Second*5).Compare(time.Now()) > 0 {
|
||||
giteaTimelineCacheMutex.RUnlock()
|
||||
return TimelineCache.data, nil
|
||||
}
|
||||
giteaTimelineCacheMutex.RUnlock()
|
||||
|
||||
giteaTimelineCacheMutex.Lock()
|
||||
defer giteaTimelineCacheMutex.Unlock()
|
||||
|
||||
// Re-read after acquiring the write lock: another goroutine may have
|
||||
// already refreshed the cache while we were waiting.
|
||||
TimelineCache, IsCached = giteaTimelineCache[prID]
|
||||
if IsCached && TimelineCache.lastCheck.Add(time.Second*5).Compare(time.Now()) > 0 {
|
||||
return TimelineCache.data, nil
|
||||
}
|
||||
|
||||
// Find the highest Updated timestamp across all cached items so the
|
||||
// incremental fetch picks up both new entries and modified ones.
|
||||
var LastCachedTime strfmt.DateTime
|
||||
for _, d := range TimelineCache.data {
|
||||
if time.Time(d.Updated).Compare(time.Time(LastCachedTime)) > 0 {
|
||||
LastCachedTime = d.Updated
|
||||
}
|
||||
}
|
||||
|
||||
for resCount > 0 {
|
||||
opts := issue.NewIssueGetCommentsAndTimelineParams().WithOwner(org).WithRepo(repo).WithIndex(idx).WithPage(&page)
|
||||
if !LastCachedTime.IsZero() {
|
||||
|
||||
Reference in New Issue
Block a user