launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #32693
[Merge] ~ines-almeida/launchpad:merge-button/fix-timeouts into launchpad:master
Ines Almeida has proposed merging ~ines-almeida/launchpad:merge-button/fix-timeouts into launchpad:master.
Commit message:
Fix merge button timeout behavior
Add control over the timeout when making requests to turnip to merge (default 5 seconds), to prevent situation where a request in turnip succeeds but Launchpad raises a timeout error.
Change behavior when the source branch has already been merged into the target branch so we still set the status to "merged" with the correct information.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~ines-almeida/launchpad/+git/launchpad/+merge/488444
Both these changes will help us deal with the timeout issues we were seeing in the merge button
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~ines-almeida/launchpad:merge-button/fix-timeouts into launchpad:master.
diff --git a/lib/lp/code/model/branchmergeproposal.py b/lib/lp/code/model/branchmergeproposal.py
index 1914117..6bcce43 100644
--- a/lib/lp/code/model/branchmergeproposal.py
+++ b/lib/lp/code/model/branchmergeproposal.py
@@ -1078,21 +1078,27 @@ class BranchMergeProposal(StormBase, BugLinkTargetMixin):
)
merge_commit = response["merge_commit"]
- # Edge case, if `merge_commit` is None it means the merge proposal had
- # already been merged by the time this endpoint was run. Might happen
- # in the odd case that someone changes the status of a proposal from
- # 'merged' back to another state and then tries merging again.
- if merge_commit is None:
- return
+ previously_merged = response.get("previously_merged", False)
- # Update commit message in DB
- self.commit_message = commit_message
+ if previously_merged:
+ merge_type = (
+ self.merge_type if self.merge_type else MergeType.UNKNOWN
+ )
+
+ # If there was already a merge commit (odd case that someone
+ # changes the status of an old proposal from 'merged' back to
+ # another state and then tries merging again) don't overwrite it
+ if self.merged_revision_id:
+ merge_commit = self.merged_revision_id
+ else:
+ merge_type = MergeType.REGULAR_MERGE
+ self.commit_message = commit_message
self.markAsMerged(
merge_reporter=person,
merged_revision_id=merge_commit,
date_merged=UTC_NOW,
- merge_type=MergeType.REGULAR_MERGE,
+ merge_type=merge_type,
)
def _reviewProposal(
diff --git a/lib/lp/code/model/githosting.py b/lib/lp/code/model/githosting.py
index cc5f84b..a8b60a2 100644
--- a/lib/lp/code/model/githosting.py
+++ b/lib/lp/code/model/githosting.py
@@ -31,10 +31,12 @@ from lp.code.errors import (
)
from lp.code.interfaces.githosting import IGitHostingClient
from lp.services.config import config
+from lp.services.features import getFeatureFlag
from lp.services.timeline.requesttimeline import get_request_timeline
from lp.services.timeout import (
TimeoutError,
get_default_timeout_function,
+ override_timeout,
urlfetch,
)
@@ -56,6 +58,9 @@ class RefCopyOperation:
self.target_ref = target_ref
+MERGE_TIMEOUT_FEATURE_FLAG = "git_client.merge.timeout"
+
+
@implementer(IGitHostingClient)
class GitHostingClient:
"""A client for the internal API provided by the Git hosting system."""
@@ -304,6 +309,13 @@ class GitHostingClient:
logger.info("Sending request to turnip '%s'" % url)
try:
+ # XXX ines-almeida 2025-06-26 we are currently experiencing some
+ # git slowness which leads to timeouts in the merge request
+ # This should help us have control of the timeout for this request.
+ timeout = getFeatureFlag(MERGE_TIMEOUT_FEATURE_FLAG)
+ if timeout and timeout.isdigit():
+ with override_timeout(int(timeout)):
+ return self._post(url, json=json_data)
return self._post(url, json=json_data)
except requests.RequestException as e:
if e.response is not None:
diff --git a/lib/lp/code/model/tests/test_branchmergeproposal.py b/lib/lp/code/model/tests/test_branchmergeproposal.py
index 54fcf84..84bb853 100644
--- a/lib/lp/code/model/tests/test_branchmergeproposal.py
+++ b/lib/lp/code/model/tests/test_branchmergeproposal.py
@@ -4324,5 +4324,61 @@ class TestBranchMergeProposalMerge(TestCaseWithFactory):
self.person,
)
+ def test_merge_already_merged(self):
+ # Test if proposal had already been merged previously, we still mark
+ # it as merged with the correct merge_revsision_id and merge_type
+
+ self.proposal.createComment(
+ owner=self.reviewer,
+ vote=CodeReviewVote.APPROVE,
+ )
+ self.proposal.next_preview_diff_job.start()
+ self.proposal.next_preview_diff_job.complete()
+
+ self.hosting_fixture.merge.result = {
+ "merge_commit": "fake-sha1",
+ "previously_merged": True,
+ }
+
+ with person_logged_in(self.person):
+ self.proposal.merge(self.person)
+ self.assertEqual(
+ BranchMergeProposalStatus.MERGED,
+ self.proposal.queue_status,
+ )
+ self.assertEqual("fake-sha1", self.proposal.merged_revision_id)
+ self.assertEqual(MergeType.UNKNOWN, self.proposal.merge_type)
+
+ def test_merge_already_merged_with_merge_commit(self):
+ # Test that if proposal had already been merged previously, we don't
+ # overwrite the merge_revision_id or the merge_type
+
+ self.proposal.createComment(
+ owner=self.reviewer,
+ vote=CodeReviewVote.APPROVE,
+ )
+ self.proposal.next_preview_diff_job.start()
+ self.proposal.next_preview_diff_job.complete()
+
+ with person_logged_in(self.person):
+ self.proposal.merge(self.person)
+ self.assertEqual("fake-sha1", self.proposal.merged_revision_id)
+
+ self.proposal.setAsWorkInProgress()
+
+ self.hosting_fixture.merge.result = {
+ "merge_commit": "new-sha1",
+ "previously_merged": True,
+ }
+
+ with person_logged_in(self.person):
+ self.proposal.merge(self.person)
+ self.assertEqual(
+ BranchMergeProposalStatus.MERGED,
+ self.proposal.queue_status,
+ )
+ self.assertEqual("fake-sha1", self.proposal.merged_revision_id)
+ self.assertEqual(MergeType.REGULAR_MERGE, self.proposal.merge_type)
+
load_tests = load_tests_apply_scenarios
diff --git a/lib/lp/code/model/tests/test_githosting.py b/lib/lp/code/model/tests/test_githosting.py
index 08fdf2c..58a4b93 100644
--- a/lib/lp/code/model/tests/test_githosting.py
+++ b/lib/lp/code/model/tests/test_githosting.py
@@ -37,8 +37,12 @@ from lp.code.errors import (
GitTargetError,
)
from lp.code.interfaces.githosting import IGitHostingClient
-from lp.code.model.githosting import RefCopyOperation
+from lp.code.model.githosting import (
+ MERGE_TIMEOUT_FEATURE_FLAG,
+ RefCopyOperation,
+)
from lp.code.tests.helpers import GitHostingFixture
+from lp.services.features.testing import FeatureFixture
from lp.services.job.interfaces.job import IRunnableJob, JobStatus
from lp.services.job.model.job import Job
from lp.services.job.runner import BaseRunnableJob, JobRunner
@@ -753,6 +757,35 @@ class TestGitHostingClientMerge(TestCaseWithFactory):
self.requests[0].url,
)
+ def test_merge_timeout_feature_flag(self):
+ """Test feature flag to increase timeout on merge API"""
+
+ self.useFixture(FeatureFixture({MERGE_TIMEOUT_FEATURE_FLAG: "10"}))
+
+ person = self.factory.makePerson()
+
+ with self.mockRequests(
+ "POST",
+ set_default_timeout=False,
+ json={"merge_commit": "dummy commit"},
+ ):
+ response = self.client.merge(
+ 1, # repository id
+ "target-branch",
+ "target_commit_sha1",
+ "source-branch",
+ "source_commit_sha1",
+ person,
+ "Test commit message",
+ )
+
+ self.assertIn("merge_commit", response)
+ self.assertEqual(
+ "http://git.launchpad.test:19417/repo/1/merge/"
+ "target-branch:source-branch",
+ self.requests[0].url,
+ )
+
def test_cross_repo_merge_success(self):
"""Test successful merge for cross repo branches"""
person = self.factory.makePerson()
diff --git a/lib/lp/code/tests/helpers.py b/lib/lp/code/tests/helpers.py
index 63e28a6..7ed3044 100644
--- a/lib/lp/code/tests/helpers.py
+++ b/lib/lp/code/tests/helpers.py
@@ -384,7 +384,7 @@ class GitHostingFixture(fixtures.Fixture):
result=({} if merges is None else merges)
)
self.merge = fake_method_factory(
- result=({"merge_commit": "fake-sha1"})
+ result=({"merge_commit": "fake-sha1", "previously_merged": False})
)
self.getBlob = fake_method_factory(result=blob)
self.delete = fake_method_factory()
Follow ups