GraphQL blockingCount keeps counting a blocked issue after it is deleted

Summary

Deleting an open issue that another issue blocks leaves the blocker's cached issues.blocking_issues_count one too high. GraphQL blockingCount (the issue field and the linked items widget), the work items REST API's blocking_count and the BLOCKING_ISSUES_ASC and BLOCKING_ISSUES_DESC sorts read that column, so they keep counting the deleted issue. The issues REST API counts live and answers the right number.

The link models refresh the column in their after_create and after_destroy callbacks (linkable_item.rb#L11-12), but deleting the blocked issue removes its issue_links rows through the ON DELETE CASCADE foreign key on target_id (fk_e71bb44f1f), which runs no callback. When the blocker blocks nothing else, as A in the steps below, the count stays wrong until it gains a blocking link; closing or reopening it does not correct it.

Where the column is read

Steps to reproduce

  1. On Premium or Ultimate, create issues A and B in a project, and mark A as blocking B.
  2. Query { project(fullPath: "<project_path>") { issue(iid: "<A>") { blockingCount } } }. It answers 1.
  3. Delete B, for example with DELETE /api/v4/projects/<project_id>/issues/<B>.
  4. Query again, and compare with blocking_issues_count in GET /api/v4/projects/<project_id>/issues/<A>.

Example Project

None on GitLab.com. I reproduced it on a self-managed GitLab EE 17.6.1 instance and with a request spec on master.

What is the current bug behavior?

After B is deleted, GraphQL still answers blockingCount: 1 for A, and BLOCKING_ISSUES_DESC still lists A first. The issues REST API answers blocking_issues_count: 0.

What is the expected correct behavior?

GraphQL answers blockingCount: 0 for A, as the issues REST API does, and the sorts order A by that count.

Relevant logs and/or screenshots

Output on GitLab EE 17.6.1 and in a request spec on master

On GitLab Enterprise Edition 17.6.1, following the steps above:

== before delete (GraphQL)
{"data":{"project":{"issue":{"iid":"1","blockingCount":1}}}}
DELETE victim issue -> HTTP 204
== after delete (GraphQL)
{"data":{"project":{"issue":{"iid":"1","blockingCount":1}}}}
== after delete (REST /issues/:iid)
{"iid":1,"blocking_issues_count":0}
== remaining links on blocker (REST)
0

The same steps in a request spec on master (the blocker is iid 1, the deleted issue iid 3, and iid 2 an unrelated issue):

before delete: {"issue":{"iid":"1","blockingCount":1},"workItems":{"nodes":[{"iid":"1","features":{"linkedItems":{"blockingCount":1}}}]},"issues":{"nodes":[{"iid":"1","blockingCount":1},{"iid":"3","blockingCount":0},{"iid":"2","blockingCount":0}]}}  REST: {"iid":1,"blocking_issues_count":1}
DELETE /projects/:id/issues/3 -> HTTP 204
after delete:  {"issue":{"iid":"1","blockingCount":1},"workItems":{"nodes":[{"iid":"1","features":{"linkedItems":{"blockingCount":1}}}]},"issues":{"nodes":[{"iid":"1","blockingCount":1},{"iid":"2","blockingCount":0}]}}  REST: {"iid":1,"blocking_issues_count":0}  links left on the blocker: 0

Output of checks

Not applicable: not reproduced on GitLab.com.

Possible fixes

Read the blockers before the delete and recount them after the commit with the existing update_blocking_issues_count!, in a callback on the issue model, which covers the REST API, workItemDelete, the issue page and project deletion. Group deletion (another cascade, on issues.namespace_id) and the counts that are already wrong could follow separately. Computing the fields live, as EpicType#blocking_count does, would not fix the sorts.

I am opening a merge request that does this in EE::Issue. I found the bug while working on gitlab-mcp-server, which reads blocking_issues_count through the REST API.

Edited by 🤖 GitLab Bot 🤖