Skip to content

Commit 01b5b48

Browse files
authored
Only lookup a bug once through the API and correctly handle errors (#83)
Currently we retrieve the full bug in router and then again in the action. Here a property is added to BugzillaWebhookRequest to get the full bug when needed and cache it.
1 parent ef222b4 commit 01b5b48

File tree

5 files changed

+61
-19
lines changed

5 files changed

+61
-19
lines changed

src/jbi/bugzilla.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66
import datetime
77
import json
88
import logging
9+
from functools import cached_property
910
from typing import Dict, List, Optional, Tuple
1011
from urllib.parse import ParseResult, urlparse
1112

1213
from pydantic import BaseModel # pylint: disable=no-name-in-module
1314

1415
from src.jbi.errors import ActionNotFoundError
1516
from src.jbi.models import Action, Actions
17+
from src.jbi.services import get_bugzilla
1618

1719
logger = logging.getLogger(__name__)
1820

@@ -216,6 +218,13 @@ def lookup_action(self, actions: Actions) -> Tuple[str, Action]:
216218
class BugzillaWebhookRequest(BaseModel):
217219
"""Bugzilla Webhook Request Object"""
218220

221+
class Config:
222+
"""pydantic model config"""
223+
224+
keep_untouched = (
225+
cached_property,
226+
) # https://github.com/samuelcolvin/pydantic/issues/1241
227+
219228
webhook_id: int
220229
webhook_name: str
221230
event: BugzillaWebhookEvent
@@ -264,6 +273,20 @@ def map_as_comments(
264273

265274
return [json.dumps(comment, indent=4) for comment in comments]
266275

276+
def getbug_as_bugzilla_object(self) -> BugzillaBug:
277+
"""Helper method to get up to date bug data from Request.bug.id in BugzillaBug format"""
278+
current_bug_info = get_bugzilla().getbug(self.bug.id) # type: ignore
279+
return BugzillaBug.parse_obj(current_bug_info.__dict__)
280+
281+
@cached_property
282+
def bugzilla_object(self) -> BugzillaBug:
283+
"""Returns the bugzilla bug object, querying the API as needed for private bugs"""
284+
if not self.bug:
285+
raise ValueError("missing bug reference")
286+
if not self.bug.is_private:
287+
return self.bug
288+
return self.getbug_as_bugzilla_object()
289+
267290

268291
class BugzillaApiResponse(BaseModel):
269292
"""Bugzilla Response Object"""

src/jbi/runner.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from src.jbi.bugzilla import BugzillaBug, BugzillaWebhookRequest
88
from src.jbi.errors import ActionNotFoundError, IgnoreInvalidRequestError
99
from src.jbi.models import Actions
10-
from src.jbi.services import getbug_as_bugzilla_object
1110

1211
logger = logging.getLogger(__name__)
1312

@@ -47,7 +46,13 @@ def execute_action(
4746
if not request.bug:
4847
raise IgnoreInvalidRequestError("no bug data received")
4948

50-
bug_obj: BugzillaBug = getbug_as_bugzilla_object(request=request)
49+
try:
50+
bug_obj: BugzillaBug = request.bugzilla_object
51+
except Exception as ex:
52+
logger.exception("Failed to get bug: %s", ex, extra=log_context)
53+
raise IgnoreInvalidRequestError(
54+
"bug not accessible or bugzilla down"
55+
) from ex
5156
log_context["bug"] = bug_obj.json()
5257

5358
try:

src/jbi/services.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from atlassian import Jira # type: ignore
44

55
from src.app import environment
6-
from src.jbi.bugzilla import BugzillaBug, BugzillaWebhookRequest
76

87
settings = environment.get_settings()
98

@@ -25,12 +24,6 @@ def get_bugzilla():
2524
)
2625

2726

28-
def getbug_as_bugzilla_object(request: BugzillaWebhookRequest) -> BugzillaBug:
29-
"""Helper method to get up to date bug data from Request.bug.id in BugzillaBug format"""
30-
current_bug_info = get_bugzilla().getbug(request.bug.id) # type: ignore
31-
return BugzillaBug.parse_obj(current_bug_info.__dict__)
32-
33-
3427
def bugzilla_check_health():
3528
"""Check health for Bugzilla Service"""
3629
bugzilla = get_bugzilla()

src/jbi/whiteboard_actions/default.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from src.app.environment import get_settings
1111
from src.jbi.bugzilla import BugzillaBug, BugzillaWebhookRequest
1212
from src.jbi.errors import ActionError
13-
from src.jbi.services import get_bugzilla, get_jira, getbug_as_bugzilla_object
13+
from src.jbi.services import get_bugzilla, get_jira
1414

1515
settings = get_settings()
1616

@@ -41,11 +41,9 @@ def __call__( # pylint: disable=inconsistent-return-statements
4141
"""Called from BZ webhook when default action is used. All default-action webhook-events are processed here."""
4242
target = payload.event.target # type: ignore
4343
if target == "comment":
44-
bug_obj = payload.bug
45-
return self.comment_create_or_noop(payload=payload, bug_obj=bug_obj) # type: ignore
44+
return self.comment_create_or_noop(payload=payload) # type: ignore
4645
if target == "bug":
47-
bug_obj = getbug_as_bugzilla_object(payload)
48-
return self.bug_create_or_update(payload=payload, bug_obj=bug_obj)
46+
return self.bug_create_or_update(payload=payload)
4947
logger.debug(
5048
"Ignore event target %r",
5149
target,
@@ -54,10 +52,9 @@ def __call__( # pylint: disable=inconsistent-return-statements
5452
},
5553
)
5654

57-
def comment_create_or_noop(
58-
self, payload: BugzillaWebhookRequest, bug_obj: BugzillaBug
59-
):
55+
def comment_create_or_noop(self, payload: BugzillaWebhookRequest):
6056
"""Confirm issue is already linked, then apply comments; otherwise noop"""
57+
bug_obj = payload.bugzilla_object
6158
linked_issue_key = bug_obj.extract_from_see_also()
6259

6360
log_context = {
@@ -100,9 +97,10 @@ def update_issue(
10097
"""Allows sub-classes to modify the Jira issue in response to a bug event"""
10198

10299
def bug_create_or_update(
103-
self, payload: BugzillaWebhookRequest, bug_obj: BugzillaBug
100+
self, payload: BugzillaWebhookRequest
104101
): # pylint: disable=too-many-locals
105102
"""Create and link jira issue with bug, or update; rollback if multiple events fire"""
103+
bug_obj = payload.bugzilla_object
106104
linked_issue_key = bug_obj.extract_from_see_also() # type: ignore
107105
if not linked_issue_key:
108106
return self.create_and_link_issue(payload, bug_obj)
@@ -180,7 +178,10 @@ def create_and_link_issue(
180178
raise ActionError(f"response contains error: {jira_response_create}")
181179

182180
jira_key_in_response = jira_response_create.get("key")
183-
bug_obj = getbug_as_bugzilla_object(request=payload)
181+
182+
# In the time taken to create the Jira issue the bug may have been updated so
183+
# re-retrieve it to ensure we have the latest data.
184+
bug_obj = payload.getbug_as_bugzilla_object()
184185
jira_key_in_bugzilla = bug_obj.extract_from_see_also()
185186
_duplicate_creation_event = (
186187
jira_key_in_bugzilla is not None

tests/unit/jbi/test_bugzilla.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,23 @@ def test_payload_changes_list_in_routing_key(webhook_change_status_assignee):
112112
"assigned_to",
113113
"status",
114114
]
115+
116+
117+
def test_payload_bugzilla_object_public(mocked_bugzilla, webhook_modify_example):
118+
bug_obj = webhook_modify_example.bugzilla_object
119+
mocked_bugzilla().getbug.assert_not_called()
120+
assert bug_obj.product == "JBI"
121+
assert bug_obj.status == "NEW"
122+
assert webhook_modify_example.bug == bug_obj
123+
124+
125+
def test_bugzilla_object_private(mocked_bugzilla, webhook_modify_private_example):
126+
bug_obj = webhook_modify_private_example.bugzilla_object
127+
mocked_bugzilla().getbug.assert_called_once_with(
128+
webhook_modify_private_example.bug.id
129+
)
130+
assert bug_obj.product == "JBI"
131+
assert bug_obj.status == "NEW"
132+
133+
bug_obj = webhook_modify_private_example.bugzilla_object
134+
mocked_bugzilla().getbug.assert_called_once()

0 commit comments

Comments
 (0)