Skip to content

Commit f7eecb3

Browse files
author
Arron Atchison
committed
[bugzillarest] Skip bug pages that return HTTP 400 instead of aborting
A single bug can make the Bugzilla REST API return a 400 (e.g. a field whose data is missing from network storage: "Failed to fetch key <n> from network storage: Not Found"). Because the backend requests include_fields=_all and pages by last_change_time, one such bug aborts the whole batch and freezes incremental collection indefinitely. Catch the HTTPError in __fetch_and_parse_bugs: on a 400, log and skip that page (advance the offset) and continue, so one bad bug no longer wedges collection. A MAX_CONSECUTIVE_SKIPS cap (reset on any success) guards against an infinite skip loop on a systemic failure, re-raising so it still surfaces. Adds tests for both behaviours (skip-and-continue, and abort-after-cap). (cherry picked from commit e85b333cab00f1072f0bcdb80a2d9271d8f3c123) Signed-off-by: Arron Atchison <aatchison@thunderbird.net>
1 parent d0af48f commit f7eecb3

2 files changed

Lines changed: 145 additions & 2 deletions

File tree

perceval/backends/core/bugzillarest.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
CATEGORY_BUG = "bug"
4646
MAX_BUGS = 500 # Maximum number of bugs per query
4747
MAX_CONTENTS = 25 # Maximum number of bug contents (history, comments) per query
48+
MAX_CONSECUTIVE_SKIPS = 100 # Abort after this many consecutive HTTP 400 pages
4849

4950

5051
class BugzillaREST(Backend):
@@ -184,12 +185,31 @@ def _init_client(self, from_archive=False):
184185
def __fetch_and_parse_bugs(self, from_date):
185186
max_contents = min(MAX_CONTENTS, self.max_bugs)
186187
offset = 0
188+
consecutive_skips = 0
187189

188190
while True:
189191
logger.debug("Fetching and parsing bugs from: %s, offset: %s, limit: %s ",
190192
str(from_date), offset, self.max_bugs)
191-
raw_bugs = self.client.bugs(from_date=from_date, offset=offset,
192-
max_bugs=self.max_bugs)
193+
try:
194+
raw_bugs = self.client.bugs(from_date=from_date, offset=offset,
195+
max_bugs=self.max_bugs)
196+
except requests.exceptions.HTTPError as e:
197+
# A single bug can make Bugzilla return a 400 (e.g. a field
198+
# whose data is missing from network storage). Skip that page
199+
# and keep going so one bad bug does not wedge the collection.
200+
if e.response is not None and e.response.status_code == 400:
201+
consecutive_skips += 1
202+
if consecutive_skips > MAX_CONSECUTIVE_SKIPS:
203+
logger.error("Too many consecutive HTTP 400s (%s) fetching bugs "
204+
"from %s; aborting", consecutive_skips, self.url)
205+
raise
206+
logger.warning("Skipping bugs page at offset %s after HTTP 400: %s",
207+
offset, e.response.text)
208+
offset += self.max_bugs
209+
continue
210+
raise
211+
212+
consecutive_skips = 0
193213

194214
data = json.loads(raw_bugs)
195215
buglist = data['bugs']

tests/test_bugzillarest.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@
2929
import os
3030
import shutil
3131
import unittest
32+
import unittest.mock
3233

3334
import httpretty
35+
import requests
3436

3537
from perceval.backend import BackendCommandArgumentParser
3638
from perceval.errors import BackendError
@@ -128,6 +130,87 @@ def request_callback(method, uri, headers):
128130
return http_requests
129131

130132

133+
def setup_http_server_with_initial_error():
134+
"""Like setup_http_server, but the first /rest/bug page returns HTTP 400.
135+
136+
Mimics a single BMO bug that 400s on include_fields=_all
137+
("Failed to fetch key ... from network storage"), which otherwise
138+
wedges the whole collection.
139+
"""
140+
http_requests = []
141+
142+
bodies_bugs = [read_file('data/bugzilla/bugzilla_rest_bugs.json', mode='rb'),
143+
read_file('data/bugzilla/bugzilla_rest_bugs_next.json', mode='rb'),
144+
read_file('data/bugzilla/bugzilla_rest_bugs_empty.json', mode='rb')]
145+
body_comments = [read_file('data/bugzilla/bugzilla_rest_bugs_comments.json', mode='rb'),
146+
read_file('data/bugzilla/bugzilla_rest_bugs_comments_empty.json', mode='rb')]
147+
body_history = [read_file('data/bugzilla/bugzilla_rest_bugs_history.json', mode='rb'),
148+
read_file('data/bugzilla/bugzilla_rest_bugs_history_empty.json', mode='rb')]
149+
body_attachments = [read_file('data/bugzilla/bugzilla_rest_bugs_attachments.json', mode='rb'),
150+
read_file('data/bugzilla/bugzilla_rest_bugs_attachments_empty.json', mode='rb')]
151+
152+
error_body = ('{"error":true,"code":68000,'
153+
'"message":"Failed to fetch key 9327669 from network storage: Not Found"}')
154+
155+
def request_callback(method, uri, headers):
156+
if uri.startswith(BUGZILLA_VERSION_URL):
157+
body = '{"version":"5.1.2"}'
158+
elif uri.startswith(BUGZILLA_BUGS_COMMENTS_1273442_URL):
159+
body = body_comments[0]
160+
elif uri.startswith(BUGZILLA_BUGS_HISTORY_1273442_URL):
161+
body = body_history[0]
162+
elif uri.startswith(BUGZILLA_BUGS_ATTACHMENTS_1273442_URL):
163+
body = body_attachments[0]
164+
elif uri.startswith(BUGZILLA_BUG_947945_URL):
165+
if uri.find('comment') > 0:
166+
body = body_comments[1]
167+
elif uri.find('history') > 0:
168+
body = body_history[1]
169+
else:
170+
body = body_attachments[1]
171+
else:
172+
body = bodies_bugs.pop(0)
173+
174+
http_requests.append(httpretty.last_request())
175+
176+
return (200, headers, body)
177+
178+
httpretty.register_uri(httpretty.GET,
179+
BUGZILLA_VERSION_URL,
180+
responses=[
181+
httpretty.Response(body=request_callback)
182+
for _ in range(3)
183+
])
184+
185+
# First bug page 400s (the broken bug); the rest succeed.
186+
httpretty.register_uri(httpretty.GET,
187+
BUGZILLA_BUGS_URL,
188+
responses=[
189+
httpretty.Response(body=error_body, status=400),
190+
*[httpretty.Response(body=request_callback)
191+
for _ in range(3)]
192+
])
193+
194+
http_urls = [BUGZILLA_BUGS_COMMENTS_1273442_URL,
195+
BUGZILLA_BUGS_HISTORY_1273442_URL,
196+
BUGZILLA_BUGS_ATTACHMENTS_1273442_URL]
197+
198+
suffixes = ['comment', 'history', 'attachment']
199+
200+
for http_url in [BUGZILLA_BUG_947945_URL]:
201+
for suffix in suffixes:
202+
http_urls.append(http_url + suffix)
203+
204+
for req_url in http_urls:
205+
httpretty.register_uri(httpretty.GET,
206+
req_url,
207+
responses=[
208+
httpretty.Response(body=request_callback)
209+
])
210+
211+
return http_requests
212+
213+
131214
class TestBugzillaRESTBackend(unittest.TestCase):
132215
"""Bugzilla REST backend tests"""
133216

@@ -257,6 +340,46 @@ def test_fetch(self):
257340
for i in range(len(expected)):
258341
self.assertDictEqual(http_requests[i].querystring, expected[i])
259342

343+
@httpretty.activate
344+
def test_fetch_skips_bug_page_on_http_400(self):
345+
"""Test whether a bug page that returns HTTP 400 is skipped
346+
347+
A single bug can make Bugzilla return a 400 (e.g. a field whose
348+
data is missing from network storage). The whole collection must
349+
not abort: the failing page is skipped and the rest are fetched.
350+
"""
351+
setup_http_server_with_initial_error()
352+
353+
bg = BugzillaREST(BUGZILLA_SERVER_URL, max_bugs=2)
354+
bugs = [bug for bug in bg.fetch(from_date=None)]
355+
356+
# The 400 page was skipped; the remaining pages still yield all bugs.
357+
self.assertEqual(len(bugs), 3)
358+
self.assertEqual(bugs[0]['data']['id'], 1273442)
359+
self.assertEqual(bugs[1]['data']['id'], 1273439)
360+
self.assertEqual(bugs[2]['data']['id'], 947945)
361+
362+
@httpretty.activate
363+
def test_fetch_aborts_after_too_many_consecutive_400s(self):
364+
"""Test whether persistent HTTP 400s abort instead of looping forever"""
365+
366+
httpretty.register_uri(httpretty.GET,
367+
BUGZILLA_VERSION_URL,
368+
body='{"version":"5.1.2"}')
369+
error_body = ('{"error":true,"code":68000,'
370+
'"message":"Failed to fetch key 9327669 from network storage: Not Found"}')
371+
httpretty.register_uri(httpretty.GET,
372+
BUGZILLA_BUGS_URL,
373+
body=error_body,
374+
status=400)
375+
376+
bg = BugzillaREST(BUGZILLA_SERVER_URL, max_bugs=1)
377+
378+
with unittest.mock.patch(
379+
'perceval.backends.core.bugzillarest.MAX_CONSECUTIVE_SKIPS', 3):
380+
with self.assertRaises(requests.exceptions.HTTPError):
381+
_ = [bug for bug in bg.fetch(from_date=None)]
382+
260383
@httpretty.activate
261384
def test_search_fields(self):
262385
"""Test whether the search_fields is properly set"""

0 commit comments

Comments
 (0)