Skip to content

Commit 224e554

Browse files
alexcottnerleplatremgrahamalama
authored
Adding queue to bugzilla webhook route (#939)
* Adding queue logic to bugzilla webhook. Still need tests. * Adding test scenarios for execute_or_queue --------- Co-authored-by: Mathieu Leplatre <[email protected]> Co-authored-by: Graham Beckley <[email protected]>
1 parent b6ab599 commit 224e554

File tree

8 files changed

+133
-18
lines changed

8 files changed

+133
-18
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,7 @@ dmypy.json
127127

128128
# Pyre type checker
129129
.pyre/
130+
131+
# IDEs
132+
.idea/
133+
.vscode/

jbi/router.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
from jbi.configuration import ACTIONS
1717
from jbi.environment import Settings, get_settings
1818
from jbi.models import Actions
19-
from jbi.runner import IgnoreInvalidRequestError, execute_action
19+
from jbi.queue import DeadLetterQueue, get_dl_queue
20+
from jbi.runner import execute_or_queue
2021

2122
SettingsDep = Annotated[Settings, Depends(get_settings)]
2223
ActionsDep = Annotated[Actions, Depends(lambda: ACTIONS)]
@@ -64,17 +65,14 @@ def api_key_auth(
6465
"/bugzilla_webhook",
6566
dependencies=[Depends(api_key_auth)],
6667
)
67-
def bugzilla_webhook(
68+
async def bugzilla_webhook(
6869
request: Request,
6970
actions: ActionsDep,
71+
queue: Annotated[DeadLetterQueue, Depends(get_dl_queue)],
7072
webhook_request: bugzilla.WebhookRequest = Body(..., embed=False),
7173
):
7274
"""API endpoint that Bugzilla Webhook Events request"""
73-
try:
74-
result = execute_action(webhook_request, actions)
75-
return result
76-
except IgnoreInvalidRequestError as exception:
77-
return {"error": str(exception)}
75+
return await execute_or_queue(webhook_request, queue, actions)
7876

7977

8078
@router.get(

jbi/runner.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
JiraContext,
2424
RunnerContext,
2525
)
26+
from jbi.queue import DeadLetterQueue
2627
from jbi.steps import StepStatus
2728

2829
logger = logging.getLogger(__name__)
@@ -157,6 +158,29 @@ def __call__(self, context: ActionContext) -> ActionResult:
157158
return True, {"responses": responses}
158159

159160

161+
async def execute_or_queue(
162+
request: bugzilla.WebhookRequest, queue: DeadLetterQueue, actions: Actions
163+
):
164+
if await queue.is_blocked(request):
165+
# If it's blocked, store it and wait for it to be processed later.
166+
await queue.postpone(request)
167+
logger.info(
168+
"%r event on Bug %s was put in queue for later processing.",
169+
request.event.action,
170+
request.bug.id,
171+
extra={"payload": request.model_dump()},
172+
)
173+
return {"status": "skipped"}
174+
175+
try:
176+
return execute_action(request, actions)
177+
except IgnoreInvalidRequestError as exc:
178+
return {"status": "invalid", "error": str(exc)}
179+
except Exception as exc:
180+
await queue.track_failed(request, exc)
181+
return {"status": "failed", "error": str(exc)}
182+
183+
160184
@statsd.timer("jbi.action.execution.timer")
161185
def execute_action(
162186
request: bugzilla.WebhookRequest,

tests/conftest.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from jbi import Operation, bugzilla, jira
1616
from jbi.environment import Settings
1717
from jbi.models import ActionContext
18+
from jbi.queue import DeadLetterQueue
1819

1920

2021
class FilteredLogCaptureFixture(pytest.LogCaptureFixture):
@@ -94,7 +95,9 @@ def test_api_key():
9495
@pytest.fixture
9596
def authenticated_client(app, test_api_key):
9697
"""An test client with a valid API key."""
97-
return TestClient(app, headers={"X-Api-Key": test_api_key})
98+
return TestClient(
99+
app, headers={"X-Api-Key": test_api_key}, raise_server_exceptions=False
100+
)
98101

99102

100103
@pytest.fixture
@@ -108,6 +111,11 @@ def actions(actions_factory):
108111
return actions_factory()
109112

110113

114+
@pytest.fixture
115+
def mock_queue():
116+
return mock.MagicMock(spec=DeadLetterQueue)
117+
118+
111119
@pytest.fixture(autouse=True)
112120
def mocked_bugzilla(request):
113121
if "no_mocked_bugzilla" in request.keywords:

tests/unit/test_app.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,10 @@ def test_traces_sampler(sampling_context, expected):
7979
assert traces_sampler(sampling_context) == expected
8080

8181

82-
def test_errors_are_reported_to_sentry(anon_client, bugzilla_webhook_request):
82+
@pytest.mark.asyncio
83+
async def test_errors_are_reported_to_sentry(anon_client, bugzilla_webhook_request):
8384
with patch("sentry_sdk.hub.Hub.capture_event") as mocked:
84-
with patch("jbi.router.execute_action", side_effect=ValueError):
85+
with patch("jbi.router.execute_or_queue", side_effect=ValueError):
8586
with pytest.raises(ValueError):
8687
anon_client.post(
8788
"/bugzilla_webhook",
@@ -92,7 +93,8 @@ def test_errors_are_reported_to_sentry(anon_client, bugzilla_webhook_request):
9293
assert mocked.called, "Sentry captured the exception"
9394

9495

95-
def test_request_id_is_passed_down_to_logger_contexts(
96+
@pytest.mark.asyncio
97+
async def test_request_id_is_passed_down_to_logger_contexts(
9698
caplog,
9799
bugzilla_webhook_request,
98100
authenticated_client,

tests/unit/test_retry.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
import pytest
55

6-
from jbi.queue import DeadLetterQueue
76
from jbi.retry import RETRY_TIMEOUT_DAYS, retry_failed
87
from jbi.runner import execute_action
98

@@ -20,11 +19,6 @@ async def aiter_sync(iterable):
2019
yield i
2120

2221

23-
@pytest.fixture
24-
def mock_queue():
25-
return MagicMock(spec=DeadLetterQueue)
26-
27-
2822
@pytest.fixture
2923
def mock_executor():
3024
return MagicMock(spec=execute_action)

tests/unit/test_router.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from fastapi.testclient import TestClient
99

1010
from jbi.environment import get_settings
11+
from jbi.queue import get_dl_queue
1112

1213

1314
def test_read_root(anon_client):
@@ -156,6 +157,35 @@ def test_webhook_is_200_if_action_raises_IgnoreInvalidRequestError(
156157
assert response.json()["error"] == "no bug whiteboard matching action tags: devtest"
157158

158159

160+
def test_webhook_is_200_if_action_raises_Exception(
161+
webhook_request_factory, mocked_bugzilla, authenticated_client
162+
):
163+
webhook = webhook_request_factory()
164+
mocked_bugzilla.get_bug.side_effect = Exception("Throwing an exception")
165+
166+
response = authenticated_client.post(
167+
"/bugzilla_webhook",
168+
data=webhook.model_dump_json(),
169+
)
170+
assert response
171+
assert response.status_code == 200
172+
173+
174+
def test_webhook_is_500_if_queue_raises_Exception(
175+
webhook_request_factory, mocked_bugzilla, authenticated_client, mock_queue
176+
):
177+
webhook = webhook_request_factory()
178+
authenticated_client.app.dependency_overrides[get_dl_queue] = lambda: mock_queue
179+
mock_queue.is_blocked.side_effect = Exception("Throwing an exception")
180+
181+
response = authenticated_client.post(
182+
"/bugzilla_webhook",
183+
data=webhook.model_dump_json(),
184+
)
185+
assert response
186+
assert response.status_code == 500
187+
188+
159189
def test_webhook_is_401_if_unathenticated(
160190
webhook_request_factory, mocked_bugzilla, anon_client
161191
):

tests/unit/test_runner.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@
99
from jbi.environment import get_settings
1010
from jbi.errors import ActionNotFoundError, IgnoreInvalidRequestError
1111
from jbi.models import ActionContext
12-
from jbi.runner import Executor, execute_action, lookup_action
12+
from jbi.runner import (
13+
Actions,
14+
Executor,
15+
execute_action,
16+
execute_or_queue,
17+
lookup_action,
18+
)
1319

1420

1521
def test_bugzilla_object_is_always_fetched(
@@ -249,6 +255,55 @@ def test_runner_ignores_request_if_jira_is_linked_but_without_whiteboard(
249255
assert str(exc_info.value) == "no bug whiteboard matching action tags: devtest"
250256

251257

258+
@pytest.mark.asyncio
259+
async def test_execute_or_queue_happy_path(
260+
mock_queue,
261+
bugzilla_webhook_request,
262+
):
263+
mock_queue.is_blocked.return_value = False
264+
await execute_or_queue(
265+
request=bugzilla_webhook_request,
266+
queue=mock_queue,
267+
actions=mock.MagicMock(spec=Actions),
268+
)
269+
mock_queue.is_blocked.assert_called_once()
270+
mock_queue.postpone.assert_not_called()
271+
mock_queue.track_failed.assert_not_called()
272+
273+
274+
@pytest.mark.asyncio
275+
async def test_execute_or_queue_blocked(
276+
actions,
277+
mock_queue,
278+
bugzilla_webhook_request,
279+
):
280+
mock_queue.is_blocked.return_value = True
281+
await execute_or_queue(
282+
request=bugzilla_webhook_request,
283+
queue=mock_queue,
284+
actions=mock.MagicMock(spec=Actions),
285+
)
286+
mock_queue.is_blocked.assert_called_once()
287+
mock_queue.postpone.assert_called_once()
288+
mock_queue.track_failed.assert_not_called()
289+
290+
291+
@pytest.mark.asyncio
292+
async def test_execute_or_queue_exception(
293+
actions,
294+
mock_queue,
295+
bugzilla_webhook_request,
296+
):
297+
mock_queue.is_blocked.return_value = False
298+
# should trigger an exception for this scenario
299+
await execute_or_queue(
300+
request=bugzilla_webhook_request, queue=mock_queue, actions=actions
301+
)
302+
mock_queue.is_blocked.assert_called_once()
303+
mock_queue.postpone.assert_not_called()
304+
mock_queue.track_failed.assert_called_once()
305+
306+
252307
def test_default_invalid_init():
253308
with pytest.raises(TypeError):
254309
Executor()

0 commit comments

Comments
 (0)