fix: possible cause of invalid issue counts; cache invalidation occurs before a active transaction is committed (#10130)

Although #9922 was deployed to Codeberg, it was reported on Matrix that a user observed a `-1` pull request count.

@Gusted checked and verified that the stats stored in redis appeared incorrect, and that no errors occurred on Codeberg that included the repo ID (eg. deadlocks, SQL queries).
```
127.0.0.1:6379> GET Repo:CountPulls:924266
"1"
127.0.0.1:6379> GET Repo:CountPullsClosed:924266
"2"
```

One possible cause is that when `UpdateRepoIssueNumbers` is invoked and invalidates the cache key for the repository, it is currently in a transaction; the next request for that cached count could be computed before the transaction is committed and the update is visible.  It's been verified that `UpdateRepoIssueNumbers` is called within a transaction in most interactions (I put a panic in it if `db.InTransaction(ctx)`, and most related tests failed).

This PR fixes that hole by performing the cache invalidation in an `AfterTx()` hook which is invoked after the transaction is committed to the database.

(Another possible cause is documented in #10127)

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10130
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net>
Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
This commit is contained in:
Mathieu Fenniak 2025-11-17 01:07:29 +01:00 committed by Gusted
parent 20f8572b92
commit a9452d11d0
18 changed files with 290 additions and 73 deletions

View file

@ -81,11 +81,13 @@ func labelStatsCorrectNumIssuesRepo(ctx context.Context, id int64) error {
}
func labelStatsCorrectNumClosedIssues(ctx context.Context, id int64) error {
return stats.QueueRecalcLabelByID(id)
stats.QueueRecalcLabelByID(ctx, id)
return nil
}
func labelStatsCorrectNumClosedIssuesRepo(ctx context.Context, id int64) error {
return stats.QueueRecalcLabelByRepoID(id)
stats.QueueRecalcLabelByRepoID(ctx, id)
return nil
}
var milestoneStatsQueryNumIssues = "SELECT `milestone`.id FROM `milestone` WHERE `milestone`.num_closed_issues!=(SELECT COUNT(*) FROM `issue` WHERE `issue`.milestone_id=`milestone`.id AND `issue`.is_closed=?) OR `milestone`.num_issues!=(SELECT COUNT(*) FROM `issue` WHERE `issue`.milestone_id=`milestone`.id)"
@ -98,9 +100,7 @@ func milestoneStatsCorrectNumIssuesRepo(ctx context.Context, id int64) error {
}
for _, result := range results {
id, _ := strconv.ParseInt(string(result["id"]), 10, 64)
if err := stats.QueueRecalcMilestoneByID(id); err != nil {
return err
}
stats.QueueRecalcMilestoneByID(ctx, id)
}
return nil
}
@ -171,7 +171,8 @@ func CheckRepoStats(ctx context.Context) error {
{
statsQuery(milestoneStatsQueryNumIssues, true),
func(ctx context.Context, milestoneID int64) error {
return stats.QueueRecalcMilestoneByID(milestoneID)
stats.QueueRecalcMilestoneByID(ctx, milestoneID)
return nil
},
"milestone count 'num_closed_issues' and 'num_issues'",
},