Fix proposed bugs link reuse #305

Merged
jgroman merged 1 commit from fix/304-proposed-bug-one-link-two-milestones into develop 2026-05-14 07:51:40 +00:00
Owner
  • Fix problem with proposed bugs with different milestones of the same release creating two different tickets
  • Add tests covering this usecase

Fixes #304

Assisted-By: Claude Code

- Fix problem with proposed bugs with different milestones of the same release creating two different tickets - Add tests covering this usecase Fixes #304 Assisted-By: Claude Code
jgroman self-assigned this 2026-05-12 11:19:11 +00:00
Fix proposed bugs link reuse
Some checks failed
Run tests and linters / test (pull_request) Successful in 2m47s
Run tests and linters / lint (pull_request) Failing after 2s
AI Code Review / ai-review (pull_request_target) Successful in 24s
e0361d76da
- Fix problem with proposed bugs with different milestones of the same release creating two different tickets
- Add tests covering this usecase

Fixes #304

Assisted-By: Claude Code
jgroman changed title from WIP: Fix proposed bugs link reuse to Fix proposed bugs link reuse 2026-05-12 11:32:58 +00:00

AI Code Review

📋 MR Summary

Fixes an issue where duplicate discussion tickets were created for the same proposed bug across different milestones within the same release.

  • Key Changes:
    • Added logic to create_discussions_links to check for existing discussion links for the same bugs in other milestones of the same release.
    • Populates the links_to_reuse dictionary with existing links to prevent duplicate ticket creation.
    • Added unit tests to verify that discussion links are reused when a bug is proposed for a second milestone.
  • Impact: blockerbugs/util/discussion_sync.py, testing/test_discussion_sync.py
  • Risk Level: 🟢 Low - The changes only affect metadata synchronization with Forgejo and include proper test coverage. The primary risk is a potential performance bottleneck if processing large batches of bugs.

Detailed Code Review

The implementation correctly solves the duplicate discussion ticket issue by looking up pre-existing links for the same bug ID in the release. The logic is sound and the included test coverage is excellent, verifying the fix accurately. However, the database query introduced in the loop creates an N+1 query pattern, which could cause performance degradation when processing many bugs at once.

📂 File Reviews

📄 `blockerbugs/util/discussion_sync.py` - Updated logic to query the database for existing discussion links if a pre-built cache is not provided.
  • Major [Performance]: The current implementation performs a separate database query for each bug in the bugs list, leading to an N+1 query problem. When processing a large batch of bugs, this will significantly impact database performance. The query should be refactored to fetch all existing links in a single batch query.
    • Suggestion: Extract the bug IDs and use in_() to perform a single query.
        links_to_reuse = {}
        bug_ids = [bug.bugid for bug in bugs]
        current_ids = [bug.id for bug in bugs if bug.id is not None]
        
        if bug_ids:
            existing_bugs = Bug.query.filter(
                Bug.bugid.in_(bug_ids),
                Bug.discussion_link.isnot(None),
                Bug.id.notin_(current_ids) if current_ids else True,
                Bug.milestone.has(release_id=milestone.release_id)
            ).all()
            
            for existing in existing_bugs:
                links_to_reuse[_link_key(milestone, existing)] = existing.discussion_link

Summary

  • Overall Assessment: The logic is correct and the bug is successfully resolved, but a performance issue related to database queries needs to be addressed before merging.
  • Priority Issues:
    • Resolve the N+1 query issue in create_discussions_links to prevent performance degradation during batch processing.

🤖 AI Code Review | Generated with ai-code-review | Model: gemini-3.1-pro-preview

⚠️ AI-generated suggestions may be incorrect. Verify before applying. Not a replacement for human review.

## AI Code Review ### 📋 MR Summary Fixes an issue where duplicate discussion tickets were created for the same proposed bug across different milestones within the same release. - **Key Changes:** - Added logic to `create_discussions_links` to check for existing discussion links for the same bugs in other milestones of the same release. - Populates the `links_to_reuse` dictionary with existing links to prevent duplicate ticket creation. - Added unit tests to verify that discussion links are reused when a bug is proposed for a second milestone. - **Impact:** blockerbugs/util/discussion_sync.py, testing/test_discussion_sync.py - **Risk Level:** 🟢 Low - The changes only affect metadata synchronization with Forgejo and include proper test coverage. The primary risk is a potential performance bottleneck if processing large batches of bugs. ### Detailed Code Review The implementation correctly solves the duplicate discussion ticket issue by looking up pre-existing links for the same bug ID in the release. The logic is sound and the included test coverage is excellent, verifying the fix accurately. However, the database query introduced in the loop creates an N+1 query pattern, which could cause performance degradation when processing many bugs at once. #### 📂 File Reviews <details> <summary><strong>📄 `blockerbugs/util/discussion_sync.py`</strong> - Updated logic to query the database for existing discussion links if a pre-built cache is not provided.</summary> - **Major** [Performance]: The current implementation performs a separate database query for each bug in the `bugs` list, leading to an N+1 query problem. When processing a large batch of bugs, this will significantly impact database performance. The query should be refactored to fetch all existing links in a single batch query. - *Suggestion:* Extract the bug IDs and use `in_()` to perform a single query. ```` links_to_reuse = {} bug_ids = [bug.bugid for bug in bugs] current_ids = [bug.id for bug in bugs if bug.id is not None] if bug_ids: existing_bugs = Bug.query.filter( Bug.bugid.in_(bug_ids), Bug.discussion_link.isnot(None), Bug.id.notin_(current_ids) if current_ids else True, Bug.milestone.has(release_id=milestone.release_id) ).all() for existing in existing_bugs: links_to_reuse[_link_key(milestone, existing)] = existing.discussion_link ```` </details> ### ✅ Summary - **Overall Assessment:** The logic is correct and the bug is successfully resolved, but a performance issue related to database queries needs to be addressed before merging. - **Priority Issues:** - Resolve the N+1 query issue in `create_discussions_links` to prevent performance degradation during batch processing. --- 🤖 **AI Code Review** | Generated with [ai-code-review](https://gitlab.com/redhat/edge/ci-cd/ai-code-review) | **Model:** `gemini-3.1-pro-preview` ⚠️ *AI-generated suggestions may be incorrect. Verify before applying. Not a replacement for human review.*
jgroman changed title from Fix proposed bugs link reuse to WIP: Fix proposed bugs link reuse 2026-05-12 11:38:48 +00:00
Use batch bug query
Some checks failed
Run tests and linters / test (pull_request) Successful in 2m42s
Run tests and linters / lint (pull_request) Failing after 2s
AI Code Review / ai-review (pull_request_target) Successful in 31s
8afe1f78f6
jgroman changed title from WIP: Fix proposed bugs link reuse to Fix proposed bugs link reuse 2026-05-12 11:57:09 +00:00

AI Code Review

📋 MR Summary

Fixes an issue where duplicate Forgejo discussion tickets were created for the same bug when proposed across different milestones within the same release.

  • Key Changes:
    • Added logic to create_discussions_links to query the database for existing discussion links for the given bugs within the same release.
    • Ensured existing ticket URLs are populated in the cache so subsequent logic reuses them rather than calling the Forgejo API.
    • Added a comprehensive test suite TestProposalReusesDiscussionLink to verify discussion ticket reuse across milestones.
  • Impact: blockerbugs/util/discussion_sync.py, testing/test_discussion_sync.py
  • Risk Level: 🟢 Low - The changes only modify internal link-caching behavior before creating Forgejo tickets. The database queries use batching to avoid N+1 issues and securely use parameterized ORM queries.

Detailed Code Review

The implementation cleanly addresses the duplicate ticket issue while maintaining excellent performance via batch querying. By pre-filling the links_to_reuse dictionary dynamically using the Bug.bugid.in_() clause, you successfully prevented redundant API calls to Forgejo and maintained proper synchronization across milestones. The addition of targeted test coverage using mocks correctly simulates and proves the expected behavior.

Summary

  • Overall Assessment: The code changes are well-implemented, performant, and correctly tested. No bugs or security risks were identified.

🤖 AI Code Review | Generated with ai-code-review | Model: gemini-3.1-pro-preview

⚠️ AI-generated suggestions may be incorrect. Verify before applying. Not a replacement for human review.

## AI Code Review ### 📋 MR Summary Fixes an issue where duplicate Forgejo discussion tickets were created for the same bug when proposed across different milestones within the same release. - **Key Changes:** - Added logic to `create_discussions_links` to query the database for existing discussion links for the given bugs within the same release. - Ensured existing ticket URLs are populated in the cache so subsequent logic reuses them rather than calling the Forgejo API. - Added a comprehensive test suite `TestProposalReusesDiscussionLink` to verify discussion ticket reuse across milestones. - **Impact:** blockerbugs/util/discussion_sync.py, testing/test_discussion_sync.py - **Risk Level:** 🟢 Low - The changes only modify internal link-caching behavior before creating Forgejo tickets. The database queries use batching to avoid N+1 issues and securely use parameterized ORM queries. ### Detailed Code Review The implementation cleanly addresses the duplicate ticket issue while maintaining excellent performance via batch querying. By pre-filling the `links_to_reuse` dictionary dynamically using the `Bug.bugid.in_()` clause, you successfully prevented redundant API calls to Forgejo and maintained proper synchronization across milestones. The addition of targeted test coverage using mocks correctly simulates and proves the expected behavior. ### ✅ Summary - **Overall Assessment:** The code changes are well-implemented, performant, and correctly tested. No bugs or security risks were identified. --- 🤖 **AI Code Review** | Generated with [ai-code-review](https://gitlab.com/redhat/edge/ci-cd/ai-code-review) | **Model:** `gemini-3.1-pro-preview` ⚠️ *AI-generated suggestions may be incorrect. Verify before applying. Not a replacement for human review.*
jgroman changed title from Fix proposed bugs link reuse to WIP: Fix proposed bugs link reuse 2026-05-12 12:00:44 +00:00
Owner

I'm not sure this is quite the right approach.

We have three paths that call create_discussions_links. As I read it, the main, original one is sync_discussions(), which passes in a correct links_to_reuse cache. The other two were, I think, added later.

The one we're trying to "fix" here is bugzilla_sync_proposal in main.py. This was added in 38ac5941a3 (then reverted in d56e8bbdf7 because it caused another duplication issue, then reapplied in f699d81f5f with the duplication issue fixed in 06bfb4e060) to make it so proposals through the app took effect immediately rather than waiting for the next sync. The bug here is in that specific path; we don't build a links_to_reuse cache on that path, so we don't spot if there's an existing discussion and reuse it.

This would "fix" that by always creating a cache in create_discussions_links, but...I think that would break the third path, which is in recreate_discussion in discussion_sync.py. That function is specifically intended to recreate discussions, per the name. It does not want to reuse existing ones. So it doesn't pass in a links_to_reuse cache on purpose.

I guess there's two ways we can deal with this: we can either change approach to just building a links_to_reuse cache in bugzilla_sync_proposal, or we can make it so that if you pass links_to_reuse=False (instead of None) then we won't build the cache in create_discussions_links.

I'm not sure this is quite the right approach. We have three paths that call `create_discussions_links`. As I read it, the main, original one is `sync_discussions()`, which passes in a correct `links_to_reuse` cache. The other two were, I think, added later. The one we're trying to "fix" here is `bugzilla_sync_proposal` in `main.py`. This was added in 38ac5941a3a35cda86d83db59dbe4d197126f4f9 (then reverted in d56e8bbdf7b6b773295d0ccc5da7a1b4e83766e2 because it caused another duplication issue, then reapplied in f699d81f5f3c1e50314c7016087de55fc7ef0e34 with the duplication issue fixed in 06bfb4e060ca7196a836c85ce52900bf2560c468) to make it so proposals through the app took effect immediately rather than waiting for the next sync. The bug here is in that specific path; we don't build a `links_to_reuse` cache on that path, so we don't spot if there's an existing discussion and reuse it. This would "fix" that by always creating a cache in `create_discussions_links`, but...I think that would *break* the *third* path, which is in `recreate_discussion` in `discussion_sync.py`. That function is specifically intended to *re*create discussions, per the name. It does not *want* to reuse existing ones. So it doesn't pass in a `links_to_reuse` cache *on purpose*. I guess there's two ways we can deal with this: we can either change approach to just building a `links_to_reuse` cache in `bugzilla_sync_proposal`, or we can make it so that if you pass `links_to_reuse=False` (instead of `None`) then we won't build the cache in `create_discussions_links`.
@ -77,0 +80,4 @@
# discussion ticket across the whole release (e.g. Beta + Final).
links_to_reuse = {}
bug_ids = [bug.bugid for bug in bugs]
current_ids = [bug.id for bug in bugs if bug.id is not None]
Owner

the naming here makes it very hard to understand what's going on, plus it just feels weird. How would bug.id ever be None? I don't think it can be, per its definition:

    id: db.Mapped[int] = db.mapped_column(primary_key=True)

So...I think this should maybe be something like:

our_bugzilla_ids = [bug.bugid for bug in bugs]
our_db_ids = [bug.id for bug in bugs]

I don't love the our_db_ids, but, you know...something that clearly distinguishes between the two "id" concepts going on here, and makes it more obvious that what we're doing here is just excluding the Bug instances that we're explicitly operating on from the query results.

the naming here makes it very hard to understand what's going on, plus it just feels weird. How would `bug.id` ever be `None`? I don't think it can be, per its definition: ``` id: db.Mapped[int] = db.mapped_column(primary_key=True) ``` So...I think this should maybe be something like: ``` our_bugzilla_ids = [bug.bugid for bug in bugs] our_db_ids = [bug.id for bug in bugs] ``` I don't love the `our_db_ids`, but, you know...something that clearly distinguishes between the two "id" concepts going on here, and makes it more obvious that what we're doing here is just excluding the `Bug` instances that we're explicitly operating on from the query results.
jgroman marked this conversation as resolved
@ -77,0 +82,4 @@
bug_ids = [bug.bugid for bug in bugs]
current_ids = [bug.id for bug in bugs if bug.id is not None]
if bug_ids:
Owner

we could probably avoid this and the if current_ids: later by just adding an early bail-out if the bugs list is empty?

we could probably avoid this and the `if current_ids:` later by just adding an early bail-out if the `bugs` list is empty?
jgroman marked this conversation as resolved
@ -77,0 +83,4 @@
current_ids = [bug.id for bug in bugs if bug.id is not None]
if bug_ids:
query = Bug.query.filter(
Owner

Maybe add a comment here which just clarifies exactly what we're doing: searching for db Bugs that have the same bugzilla ID as the one(s) we were called on, but are not the one(s) we were called on.

Maybe add a comment here which just clarifies exactly what we're doing: searching for db `Bug`s that have the same bugzilla ID as the one(s) we were called on, but are *not* the one(s) we were called on.
jgroman marked this conversation as resolved
Fix linter warnings in create_discussion_links()
Some checks failed
Run tests and linters / test (pull_request) Successful in 2m48s
Run tests and linters / lint (pull_request) Failing after 2s
AI Code Review / ai-review (pull_request_target) Successful in 40s
f922852b42
Author
Owner

@adamwill wrote in #305 (comment):

This would "fix" that by always creating a cache in create_discussions_links, but...I think that would break the third path, which is in recreate_discussion in discussion_sync.py. That function is specifically intended to _re_create discussions, per the name. It does not want to reuse existing ones. So it doesn't pass in a links_to_reuse cache on purpose.

I guess there's two ways we can deal with this: we can either change approach to just building a links_to_reuse cache in bugzilla_sync_proposal, or we can make it so that if you pass links_to_reuse=False (instead of None) then we won't build the cache in create_discussions_links.

Thanks! I was not aware of this. Modified create_discussions_link so that links_to_reuse=None would trigger cache rebuild and if any dict is passed instead it would be left untouched.

@adamwill wrote in https://forge.fedoraproject.org/quality/blockerbugs/pulls/305#issuecomment-710545: > This would "fix" that by always creating a cache in `create_discussions_links`, but...I think that would _break_ the _third_ path, which is in `recreate_discussion` in `discussion_sync.py`. That function is specifically intended to _re_create discussions, per the name. It does not _want_ to reuse existing ones. So it doesn't pass in a `links_to_reuse` cache _on purpose_. > > I guess there's two ways we can deal with this: we can either change approach to just building a `links_to_reuse` cache in `bugzilla_sync_proposal`, or we can make it so that if you pass `links_to_reuse=False` (instead of `None`) then we won't build the cache in `create_discussions_links`. Thanks! I was not aware of this. Modified `create_discussions_link` so that `links_to_reuse=None` would trigger cache rebuild and if any dict is passed instead it would be left untouched.
jgroman changed title from WIP: Fix proposed bugs link reuse to Fix proposed bugs link reuse 2026-05-13 08:43:20 +00:00

AI Code Review

📋 MR Summary

Fixes duplicate Forgejo discussion ticket creation for proposed bugs spanning multiple milestones of the same release.

  • Key Changes:
    • Added automatic lookup for existing discussion links of bugs with the same Bugzilla ID in the same release.
    • Utilized a single batch query for better performance instead of N+1 individual lookups.
    • Updated recreate_discussion to pass an empty dict to skip link reuse explicitly.
    • Fixed linter warnings by converting string interpolation to comma-separated arguments in logger calls.
  • Impact: blockerbugs/util/discussion_sync.py, testing/test_discussion_sync.py
  • Risk Level: 🟢 Low - The changes introduce a non-destructive lookup to reuse URLs and explicitly preserve previous behavior for recreate_discussion. The logic is isolated and backed by newly added tests.

Detailed Code Review

The implementation correctly aligns with previous review consensus. The batch lookup strategy efficiently mitigates the duplicate ticket issue without introducing an N+1 query problem. The distinction between links_to_reuse=None (automatic lookup) and links_to_reuse={} (forced new link) safely preserves the isolated operational intent of the recreate_discussion pipeline. Linter fixes applied to standard logging inputs are also well executed.

Summary

  • Overall Assessment: Code review complete. The implementation correctly addresses the duplicate ticket bug while preserving existing functionality in alternate flows. Test coverage is appropriate and effectively validates the new logic.

🤖 AI Code Review | Generated with ai-code-review | Model: gemini-3.1-pro-preview

⚠️ AI-generated suggestions may be incorrect. Verify before applying. Not a replacement for human review.

## AI Code Review ### 📋 MR Summary Fixes duplicate Forgejo discussion ticket creation for proposed bugs spanning multiple milestones of the same release. - **Key Changes:** - Added automatic lookup for existing discussion links of bugs with the same Bugzilla ID in the same release. - Utilized a single batch query for better performance instead of N+1 individual lookups. - Updated `recreate_discussion` to pass an empty dict to skip link reuse explicitly. - Fixed linter warnings by converting string interpolation to comma-separated arguments in logger calls. - **Impact:** blockerbugs/util/discussion_sync.py, testing/test_discussion_sync.py - **Risk Level:** 🟢 Low - The changes introduce a non-destructive lookup to reuse URLs and explicitly preserve previous behavior for `recreate_discussion`. The logic is isolated and backed by newly added tests. ### Detailed Code Review The implementation correctly aligns with previous review consensus. The batch lookup strategy efficiently mitigates the duplicate ticket issue without introducing an N+1 query problem. The distinction between `links_to_reuse=None` (automatic lookup) and `links_to_reuse={}` (forced new link) safely preserves the isolated operational intent of the `recreate_discussion` pipeline. Linter fixes applied to standard logging inputs are also well executed. ### ✅ Summary - **Overall Assessment:** Code review complete. The implementation correctly addresses the duplicate ticket bug while preserving existing functionality in alternate flows. Test coverage is appropriate and effectively validates the new logic. --- 🤖 **AI Code Review** | Generated with [ai-code-review](https://gitlab.com/redhat/edge/ci-cd/ai-code-review) | **Model:** `gemini-3.1-pro-preview` ⚠️ *AI-generated suggestions may be incorrect. Verify before applying. Not a replacement for human review.*
jgroman changed title from Fix proposed bugs link reuse to WIP: Fix proposed bugs link reuse 2026-05-13 08:44:31 +00:00
jgroman changed title from WIP: Fix proposed bugs link reuse to Fix proposed bugs link reuse 2026-05-13 13:34:50 +00:00
Owner

OK, LGTM now. Thanks. Please squash appropriately and merge.

We should really do a lint cleanup next, though.

OK, LGTM now. Thanks. Please squash appropriately and merge. We should really do a lint cleanup next, though.
Author
Owner

Thank you!

Created #306 to do the linter cleanup.

Thank you! Created #306 to do the linter cleanup.
jgroman force-pushed fix/304-proposed-bug-one-link-two-milestones from f922852b42
Some checks failed
Run tests and linters / test (pull_request) Successful in 2m48s
Run tests and linters / lint (pull_request) Failing after 2s
AI Code Review / ai-review (pull_request_target) Successful in 40s
to d009979c45
Some checks failed
Run tests and linters / test (pull_request) Successful in 3m35s
Run tests and linters / lint (pull_request) Failing after 2s
2026-05-14 07:46:22 +00:00
Compare
jgroman merged commit d009979c45 into develop 2026-05-14 07:51:40 +00:00
jgroman deleted branch fix/304-proposed-bug-one-link-two-milestones 2026-05-14 07:51:41 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
quality/blockerbugs!305
No description provided.