Skip to content

Commit b321a5e

Browse files
Implement rudimentary rate limiting (#4653)
1 parent ad7e815 commit b321a5e

File tree

2 files changed

+54
-23
lines changed

2 files changed

+54
-23
lines changed

src/clusterfuzz/_internal/cron/external_testcase_reader.py

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,9 @@ def close_issue_if_invalid(upload_request, attachment_info, description,
9696
return invalid
9797

9898

99-
def close_issue_if_not_reproducible(issue):
100-
if issue.status == ISSUETRACKER_ACCEPTED_STATE and filed_one_day_ago(
101-
issue.created_time):
99+
def close_issue_if_not_reproducible(issue, config):
100+
if issue.status == ISSUETRACKER_ACCEPTED_STATE and filed_n_days_ago(
101+
issue.created_time, config):
102102
comment_message = ('Clusterfuzz failed to reproduce - '
103103
'please check testcase details for more info.')
104104
issue.status = ISSUETRACKER_WONTFIX_STATE
@@ -107,10 +107,11 @@ def close_issue_if_not_reproducible(issue):
107107
return False
108108

109109

110-
def filed_one_day_ago(issue_created_time_string):
110+
def filed_n_days_ago(issue_created_time_string, config):
111111
created_time = datetime.datetime.strptime(issue_created_time_string,
112112
'%Y-%m-%dT%H:%M:%S.%fZ')
113-
return datetime.datetime.now() - created_time > datetime.timedelta(days=1)
113+
return datetime.datetime.now() - created_time > datetime.timedelta(
114+
days=config.get('submitted-buffer-days'))
114115

115116

116117
def submit_testcase(issue_id, file, filename, filetype, cmds):
@@ -152,31 +153,41 @@ def submit_testcase(issue_id, file, filename, filetype, cmds):
152153

153154

154155
def handle_testcases(tracker, config):
155-
"""Fetches and submits testcases from bugs or closes unnecssary bugs."""
156-
# TODO(pgrace) remove ID filter once done testing.
157-
issues = tracker.find_issues_with_filters(
156+
"""Fetches and submits testcases from bugs or closes unnecessary bugs."""
157+
158+
# Handle bugs that were already submitted and still open.
159+
older_issues = tracker.find_issues_with_filters(
158160
keywords=[],
159-
query_filters=['componentid:1600865', 'id:373893311'],
161+
query_filters=['componentid:1600865', 'status:accepted'],
160162
only_open=True)
163+
for issue in older_issues:
164+
# Close out older bugs that may have failed to reproduce.
165+
if close_issue_if_not_reproducible(issue, config):
166+
helpers.log('Closing issue {issue_id} as it failed to reproduce',
167+
issue.id)
161168

169+
# Handle new bugs that may need to be submitted.
170+
issues = tracker.find_issues_with_filters(
171+
keywords=[],
172+
query_filters=['componentid:1600865', 'status:new'],
173+
only_open=True)
162174
if len(issues) == 0:
163175
return
164176

165177
# TODO(pgrace) Cache in redis.
166178
vrp_uploaders = get_vrp_uploaders(config)
167179

168-
# TODO(pgrace) Implement rudimentary rate limiting.
180+
# Rudimentary rate limiting -
181+
# Process only a certain number of bugs per reporter for each job run.
182+
reporters_map = {}
169183

170184
for issue in issues:
171-
# Close out older bugs that may have failed to reproduce.
172-
if close_issue_if_not_reproducible(issue):
173-
helpers.log('Closing issue {issue_id} as it failed to reproduce',
174-
issue.id)
175-
continue
176-
177-
# Close out invalid bugs.
178185
attachment_metadata = tracker.get_attachment_metadata(issue.id)
179186
commandline_flags = tracker.get_description(issue.id)
187+
if reporters_map.get(issue.reporter,
188+
0) > config.get('max-report-count-per-run'):
189+
continue
190+
reporters_map[issue.reporter] = reporters_map.get(issue.reporter, 1) + 1
180191
if close_issue_if_invalid(issue, attachment_metadata, commandline_flags,
181192
vrp_uploaders):
182193
helpers.log('Closing issue {issue_id} as it is invalid', issue.id)

src/clusterfuzz/_internal/tests/appengine/handlers/cron/external_testcase_reader_test.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@
3232
}
3333
BASIC_CONFIG = {
3434
'vrp-uploaders-bucket': 'bucket-name',
35-
'vrp-uploaders-blob': 'blob-name'
35+
'vrp-uploaders-blob': 'blob-name',
36+
'max-report-count-per-run': 5,
37+
'submitted-buffer-days': 1,
3638
}
3739

3840

@@ -53,8 +55,8 @@ def test_handle_testcases(self, mock_close_issue_if_invalid,
5355
mock_close_issue_if_invalid.return_value = False
5456
mock_it = mock.create_autospec(issue_tracker.IssueTracker)
5557
basic_issue = mock.MagicMock()
56-
basic_issue.reporter.return_value = '[email protected]'
57-
mock_it.find_issues_with_filters.return_value = [basic_issue]
58+
basic_issue.reporter = '[email protected]'
59+
mock_it.find_issues_with_filters.side_effect = [[], [basic_issue]]
5860

5961
external_testcase_reader.handle_testcases(mock_it, BASIC_CONFIG)
6062

@@ -68,15 +70,33 @@ def test_handle_testcases_invalid(self, mock_close_issue_if_invalid,
6870
mock_close_issue_if_invalid.return_value = True
6971
mock_it = mock.create_autospec(issue_tracker.IssueTracker)
7072
basic_issue = mock.MagicMock()
71-
basic_issue.reporter.return_value = '[email protected]'
72-
mock_it.find_issues_with_filters.return_value = [basic_issue]
73+
basic_issue.reporter = '[email protected]'
74+
mock_it.find_issues_with_filters.side_effect = [[], [basic_issue]]
7375

7476
external_testcase_reader.handle_testcases(mock_it, BASIC_CONFIG)
7577

7678
mock_close_issue_if_invalid.assert_called_once()
7779
mock_it.get_attachment.assert_not_called()
7880
mock_submit_testcase.assert_not_called()
7981

82+
def test_handle_testcases_rate_limit(self, mock_close_issue_if_invalid,
83+
mock_submit_testcase, _):
84+
"""Test a handle_testcases where one reporter hits the rate limit."""
85+
mock_close_issue_if_invalid.return_value = False
86+
mock_it = mock.create_autospec(issue_tracker.IssueTracker)
87+
basic_issue = mock.MagicMock()
88+
basic_issue.reporter = '[email protected]'
89+
mock_it.find_issues_with_filters.side_effect = [[], [
90+
basic_issue, basic_issue, basic_issue, basic_issue, basic_issue,
91+
basic_issue
92+
]]
93+
94+
external_testcase_reader.handle_testcases(mock_it, BASIC_CONFIG)
95+
96+
mock_close_issue_if_invalid.assert_called()
97+
self.assertEqual(mock_it.get_attachment.call_count, 5)
98+
self.assertEqual(mock_submit_testcase.call_count, 5)
99+
80100
@mock.patch.object(
81101
external_testcase_reader,
82102
'close_issue_if_not_reproducible',
@@ -88,7 +108,7 @@ def test_handle_testcases_not_reproducible(
88108
mock_it = mock.create_autospec(issue_tracker.IssueTracker)
89109
basic_issue = mock.MagicMock()
90110
basic_issue.reporter.return_value = '[email protected]'
91-
mock_it.find_issues_with_filters.return_value = [basic_issue]
111+
mock_it.find_issues_with_filters.side_effect = [[basic_issue], []]
92112

93113
external_testcase_reader.handle_testcases(mock_it, BASIC_CONFIG)
94114

0 commit comments

Comments
 (0)