Skip to content

Commit 96167b4

Browse files
authored
Update jira to only add watchers that are missing (#5073)
There are two issues that this PR fixes: 1. When self.client.add_watcher(issue.jira_issue, watcher) fails previously, it'd throw an exception resulting in the entire .save or .create call to fail, which would retry continuously hammering the jira instance. 2. The function would always try to add watchers to issues regardless if the watchers were already added previously.
1 parent a1863d2 commit 96167b4

File tree

2 files changed

+69
-8
lines changed

2 files changed

+69
-8
lines changed

src/clusterfuzz/_internal/issue_management/jira/issue_tracker_manager.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@
1818
import jira
1919

2020
from clusterfuzz._internal.config import db_config
21+
from clusterfuzz._internal.metrics import logs
22+
23+
UNCREATED_JIRA_ISSUE_ID = -1
2124

2225

2326
class IssueTrackerManager:
2427
"""Issue tracker manager."""
2528

2629
def __init__(self, project_name):
27-
""""Construct an issue tracker manager instance based on parameters."""
30+
"""Construct an issue tracker manager instance based on parameters."""
2831
self._client = None
2932
self.project_name = project_name
3033

@@ -47,13 +50,19 @@ def _create_client(self):
4750

4851
def save(self, issue):
4952
"""Save an issue."""
50-
if issue.id == -1:
53+
if issue.id == UNCREATED_JIRA_ISSUE_ID:
5154
return self._create(issue)
5255
return self._update(issue)
5356

5457
def create(self):
5558
"""Create an issue object locally."""
56-
raw_fields = {'id': '-1', 'fields': {'components': [], 'labels': []}}
59+
raw_fields = {
60+
'id': f'{UNCREATED_JIRA_ISSUE_ID}',
61+
'fields': {
62+
'components': [],
63+
'labels': []
64+
}
65+
}
5766
# Create jira issue object
5867
jira_issue = jira.resources.Issue({},
5968
jira.resilientsession.ResilientSession(),
@@ -75,16 +84,27 @@ def _transition_issue_status_if_updated(self, issue):
7584
if not isinstance(issue.status, jira.resources.Resource):
7685
self.client.transition_issue(issue.jira_issue, transition=issue.status)
7786

78-
def _add_watchers(self, issue):
87+
def _add_watchers(self, issue, update_if_different=False):
7988
"""Add watchers to the ticket. Jira has a separate endpoint to
8089
add watchers."""
8190

91+
# return if issue was not created yet
92+
if issue.id == UNCREATED_JIRA_ISSUE_ID:
93+
return
94+
8295
# Get watchers from LabelStore.
83-
watchers = list(issue.ccs)
96+
watchers = set(issue.ccs)
97+
98+
if update_if_different:
99+
# Only add the missing watchers
100+
watchers = watchers - set(self.get_watchers(issue.jira_issue))
84101

85102
# Jira weirdness, update watchers this way.
86103
for watcher in watchers:
87-
self.client.add_watcher(issue.jira_issue, watcher)
104+
try:
105+
self.client.add_watcher(issue.jira_issue, watcher)
106+
except Exception:
107+
logs.error(f'Error adding watcher {watcher} to issue {issue.id}')
88108

89109
def _get_issue_fields(self, issue):
90110
"""Get issue fields to populate the ticket"""
@@ -128,12 +148,12 @@ def _update(self, issue):
128148

129149
update_fields = self._get_issue_fields(issue)
130150
self._transition_issue_status_if_updated(issue)
131-
self._add_watchers(issue)
151+
self._add_watchers(issue, update_if_different=True)
132152
issue.jira_issue.update(fields=update_fields)
133153

134154
def get_watchers(self, issue):
135155
"""Retrieve list of watchers."""
136-
if issue.id == -1:
156+
if issue.id == UNCREATED_JIRA_ISSUE_ID:
137157
return []
138158
watchlist = self.client.watchers(issue)
139159
watchers = []

src/clusterfuzz/_internal/tests/appengine/libs/issue_management/jira/jira_test.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,44 @@ def test_issue_save(self):
183183
self.mock.client.add_comment.assert_called_with(self.mock_issue.jira_issue,
184184
'test comments')
185185
self.assertEqual(issue.status, 'Closed')
186+
187+
def test_issue_save_only_update_missing_watchers(self):
188+
"""Test save only update missing watchers for existing issue."""
189+
self.mock.get_issue.return_value = self.mock_issue
190+
self.mock_issue.ccs.add('idontexist@cc.com')
191+
self.mock_issue.ccs.add('idontexistalso@cc.com')
192+
self.mock.get_watchers.return_value = ['cc@cc.com']
193+
issue = self.issue_tracker.get_issue('VSEC-3112')
194+
issue.save(new_comment='test comments')
195+
self.mock.get_watchers.assert_called()
196+
self.mock.client.add_comment.assert_called_with(self.mock_issue.jira_issue,
197+
'test comments')
198+
self.mock.client.add_watcher.assert_has_calls(
199+
[
200+
unittest.mock.call(self.mock_issue.jira_issue,
201+
'idontexistalso@cc.com'),
202+
unittest.mock.call(self.mock_issue.jira_issue, 'idontexist@cc.com')
203+
],
204+
any_order=True)
205+
206+
def test_issue_save_bad_watcher(self):
207+
"""Test save still works if a watcher doesn't exist."""
208+
self.mock.get_issue.return_value = self.mock_issue
209+
210+
# Simulate adding a watcher that doesn't exist.
211+
def side_affect_func(watcher):
212+
if watcher == 'idontexist@cc.com':
213+
raise jira.exceptions.JIRAError(
214+
text=
215+
'The user "idontexist@cc.com" does not have permission to view this issue. This user will not be added to the watch list.',
216+
status_code=401)
217+
218+
self.mock_issue.ccs.add('idontexist@cc.com')
219+
self.mock.client.add_watcher.side_effect = side_affect_func
220+
221+
issue = self.issue_tracker.get_issue('VSEC-3112')
222+
issue.status = 'Closed'
223+
issue.save(new_comment='test comments')
224+
self.mock.client.add_comment.assert_called_with(self.mock_issue.jira_issue,
225+
'test comments')
226+
self.assertEqual(issue.status, 'Closed')

0 commit comments

Comments
 (0)