open_issues_count in /user/repos is stale — not invalidated/recomputed on issue create #16

Closed
opened 2026-05-17 14:15:19 +03:00 by oleks · 1 comment
Owner

Summary: The per-repo aggregate open_issues_count returned by GET /user/repos (and the related open_pr_counter) does not reflect live state immediately after issues are created — the denormalized counter is not being invalidated/updated on the data-change path, so consumers that trust it get wrong numbers.

Concrete reproduction (observed 2026-05-16 on git.oleks.space):

  1. Created 3 new open issues in oleks/fleet (#4, #5, #6) and 2 in oleks/cluster (#29, #30) via the API.
  2. Immediately after, GET /user/repos reported open_issues_count: 0 for oleks/fleet — the repo appeared to have no open issues at all.
  3. GET /repos/oleks/fleet/issues?state=open at the same moment correctly returned all 3 newly-created open issues.
  4. So the live issue list is correct; the cached aggregate on the repo record is stale and was never bumped when the issues were inserted.

Impact: Any tooling that inventories issues via the /user/repos aggregate (dashboards, account sweeps, the steward/issuer automation) silently under-counts and can drop freshly-active repos entirely. The discrepancy is worst exactly after a write, which is when inventory is most likely to be run.

Investigation asks:

  • Trace the issue-create path: is Repository.NumIssues/NumOpenIssues (or the equivalent counter column feeding open_issues_count) updated transactionally on issue insert, or only by the periodic CheckRepoStats/repo-stats recompute? If the latter, the API exposes a counter that is correct only between recompute cycles.
  • Confirm whether this is fork-introduced or inherited upstream behavior (check against upstream Gitea + any fork patches touching repo issue counters / project-board changes).
  • Decide the fix direction: update the counter on the create/close/reopen/delete paths (cache-invalidate on data change), or document that /user/repos counts are eventually-consistent and steer consumers to per-repo enumeration.

Severity: low (data-integrity / observability; no data loss — live endpoints are correct).

**Summary:** The per-repo aggregate `open_issues_count` returned by `GET /user/repos` (and the related `open_pr_counter`) does not reflect live state immediately after issues are created — the denormalized counter is not being invalidated/updated on the data-change path, so consumers that trust it get wrong numbers. **Concrete reproduction (observed 2026-05-16 on git.oleks.space):** 1. Created 3 new open issues in `oleks/fleet` (#4, #5, #6) and 2 in `oleks/cluster` (#29, #30) via the API. 2. Immediately after, `GET /user/repos` reported `open_issues_count: 0` for `oleks/fleet` — the repo appeared to have no open issues at all. 3. `GET /repos/oleks/fleet/issues?state=open` at the same moment correctly returned all 3 newly-created open issues. 4. So the live issue list is correct; the cached aggregate on the repo record is stale and was never bumped when the issues were inserted. **Impact:** Any tooling that inventories issues via the `/user/repos` aggregate (dashboards, account sweeps, the steward/issuer automation) silently under-counts and can drop freshly-active repos entirely. The discrepancy is worst exactly after a write, which is when inventory is most likely to be run. **Investigation asks:** - Trace the issue-create path: is `Repository.NumIssues`/`NumOpenIssues` (or the equivalent counter column feeding `open_issues_count`) updated transactionally on issue insert, or only by the periodic `CheckRepoStats`/repo-stats recompute? If the latter, the API exposes a counter that is correct only between recompute cycles. - Confirm whether this is fork-introduced or inherited upstream behavior (check against upstream Gitea + any fork patches touching repo issue counters / project-board changes). - Decide the fix direction: update the counter on the create/close/reopen/delete paths (cache-invalidate on data change), or document that `/user/repos` counts are eventually-consistent and steer consumers to per-repo enumeration. Severity: low (data-integrity / observability; no data loss — live endpoints are correct).
oleks added the kind/backend label 2026-05-17 14:15:19 +03:00
oleks added the activity/builddomain/fleetprovisional-activity labels 2026-05-17 16:01:11 +03:00
oleks added this to the oleks — state board project 2026-05-17 16:02:37 +03:00
oleks removed this from the oleks — state board project 2026-05-17 16:02:37 +03:00
oleks added this to the oleks — domain board project 2026-05-17 16:02:37 +03:00
oleks removed this from the oleks — domain board project 2026-05-17 16:02:59 +03:00
oleks added this to the oleks — activity board project 2026-05-17 16:04:53 +03:00
oleks added this to the oleks — state board project 2026-05-17 16:05:11 +03:00
Author
Owner

Investigated — this is not a bug in the fork, and the title premise ("not invalidated/recomputed on issue create") does not hold for the normal create path.

Findings:

  • Repository.NumOpenIssues is a computed field (xorm:"-", models/repo/repo.go), derived in AfterLoad() as num_issues - num_closed_issues on every repo row read — including /user/repos.
  • The normal issue-create path (services/issue/issue.goissues_model.NewIssueNewIssueWithIndex) calls IncrRepoIssueNumbers(repoID, isPull, true) inside the same transaction as the insert, atomically incrementing the num_issues column. So open_issues_count is correct immediately after a create via this path.
  • The only in-tree path that inserts issues without incrementing counters is the bulk migration importer (InsertIssues, used by services/migrations/gitea_uploader.go) — and that path calls models.UpdateRepoStats() at finalize, so it is also consistent once the import completes.
  • All of the above is stock upstream Gitea behavior; none of it is touched by the fork's projects-API work.

Why the repro showed open_issues_count: 0: the most plausible cause is pre-existing denormalized-counter drift on the specific oleks/fleet repo row (counters can drift after manual DB operations, restores, or historical bugs), which the periodic CheckRepoStats later corrects. The live /issues?state=open list is computed by COUNT(*) and is unaffected by drift, which is why it was correct while the cached counter was not.

Resolution: No source change is warranted — the invalidation-on-write mechanism already exists and is correct. To repair a repo whose denormalized counters have drifted, run gitea doctor --run check-db-consistency --fix (or wait for the periodic CheckRepoStats recompute). Closing as works-as-designed / not-fork-introduced.


Investigated by Claude Opus 4.7 (assisting @oleks).

Investigated — this is **not a bug in the fork**, and the title premise ("not invalidated/recomputed on issue create") does not hold for the normal create path. **Findings:** - `Repository.NumOpenIssues` is a computed field (`xorm:"-"`, `models/repo/repo.go`), derived in `AfterLoad()` as `num_issues - num_closed_issues` on **every** repo row read — including `/user/repos`. - The normal issue-create path (`services/issue/issue.go` → `issues_model.NewIssue` → `NewIssueWithIndex`) calls `IncrRepoIssueNumbers(repoID, isPull, true)` **inside the same transaction as the insert**, atomically incrementing the `num_issues` column. So `open_issues_count` is correct immediately after a create via this path. - The only in-tree path that inserts issues without incrementing counters is the bulk migration importer (`InsertIssues`, used by `services/migrations/gitea_uploader.go`) — and that path calls `models.UpdateRepoStats()` at finalize, so it is also consistent once the import completes. - All of the above is stock upstream Gitea behavior; none of it is touched by the fork's projects-API work. **Why the repro showed `open_issues_count: 0`:** the most plausible cause is pre-existing denormalized-counter drift on the specific `oleks/fleet` repo row (counters can drift after manual DB operations, restores, or historical bugs), which the periodic `CheckRepoStats` later corrects. The live `/issues?state=open` list is computed by `COUNT(*)` and is unaffected by drift, which is why it was correct while the cached counter was not. **Resolution:** No source change is warranted — the invalidation-on-write mechanism already exists and is correct. To repair a repo whose denormalized counters have drifted, run `gitea doctor --run check-db-consistency --fix` (or wait for the periodic `CheckRepoStats` recompute). Closing as works-as-designed / not-fork-introduced. --- Investigated by Claude Opus 4.7 (assisting @oleks).
oleks closed this issue 2026-05-17 17:01:07 +03:00
oleks removed this from the oleks — state board project 2026-05-17 20:21:18 +03:00
oleks removed this from the oleks — activity board project 2026-05-17 20:21:21 +03:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: oleks/gitea#16