Skip to content

Commit 17e3278

Browse files
authored
Retry Jira and Bugzilla on error (fixes #33) (#152)
* Fixup #134: fix decorated client methods * Retry Jira and Bugzilla on error (fixes #33)
1 parent a932d5f commit 17e3278

File tree

6 files changed

+65
-19
lines changed

6 files changed

+65
-19
lines changed

poetry.lock

Lines changed: 18 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ dockerflow = "2022.7.0"
1818
Jinja2 = "^3.1.2"
1919
pydantic-yaml = {extras = ["pyyaml","ruamel"], version = "^0.6.1"}
2020
sentry-sdk = "^1.8.0"
21+
backoff = "^2.1.2"
2122

2223

2324
[tool.poetry.dev-dependencies]

src/app/environment.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class Settings(BaseSettings):
2020
port: int = 8000
2121
app_reload: bool = True
2222
env: str = "nonprod"
23+
max_retries: int = 3
2324

2425
# Jira
2526
jira_base_url: str = "https://mozit-test.atlassian.net/"

src/app/log.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def configure_logging():
3030
},
3131
"loggers": {
3232
"request.summary": {"handlers": ["console"], "level": "INFO"},
33+
"backoff": {"handlers": ["console"], "level": "INFO"},
3334
"src.jbi": {"handlers": ["console"], "level": "DEBUG"},
3435
},
3536
}

src/jbi/services.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
"""Services and functions that can be used to create custom actions"""
2-
import contextlib
32
import logging
43
from typing import Dict, List
54

5+
import backoff
66
import bugzilla as rh_bugzilla
7-
from atlassian import Jira
7+
from atlassian import Jira, errors
88
from prometheus_client import Counter, Summary
99

1010
from src.app import environment
@@ -19,16 +19,18 @@
1919

2020

2121
class InstrumentedClient:
22-
"""This class wraps an object and increments a counter everytime
23-
the specieds methods are called, and times their execution.
22+
"""This class wraps an object and increments a counter every time
23+
the specified methods are called, and times their execution.
24+
It retries the methods if the specified exceptions are raised.
2425
"""
2526

2627
counters: Dict[str, Counter] = {}
2728
timers: Dict[str, Counter] = {}
2829

29-
def __init__(self, wrapped, prefix, methods):
30+
def __init__(self, wrapped, prefix, methods, exceptions):
3031
self.wrapped = wrapped
3132
self.methods = methods
33+
self.exceptions = exceptions
3234

3335
# We have a single instrument per prefix. Methods are reported as labels.
3436
counter_name = prefix + "_methods_total"
@@ -46,14 +48,23 @@ def __init__(self, wrapped, prefix, methods):
4648
self.timer = self.timers[timer_name]
4749

4850
def __getattr__(self, attr):
49-
if attr in self.methods:
51+
if attr not in self.methods:
52+
return getattr(self.wrapped, attr)
53+
54+
@backoff.on_exception(
55+
backoff.expo,
56+
self.exceptions,
57+
max_tries=settings.max_retries + 1,
58+
)
59+
def wrapped_func(*args, **kwargs):
60+
# Increment the call counter.
5061
self.counter.labels(method=attr).inc()
51-
timer_cm = self.timer.labels(method=attr).time()
52-
else:
53-
timer_cm = contextlib.nullcontext()
62+
# Time its execution.
63+
with self.timer.labels(method=attr).time():
64+
return getattr(self.wrapped, attr)(*args, **kwargs)
5465

55-
with timer_cm:
56-
return getattr(self.wrapped, attr)
66+
# The method was not called yet.
67+
return wrapped_func
5768

5869

5970
def get_jira():
@@ -71,7 +82,10 @@ def get_jira():
7182
"create_issue",
7283
)
7384
return InstrumentedClient(
74-
wrapped=jira_client, prefix="jbi_jira", methods=instrumented_methods
85+
wrapped=jira_client,
86+
prefix="jbi_jira",
87+
methods=instrumented_methods,
88+
exceptions=(errors.ApiError,),
7589
)
7690

7791

@@ -93,7 +107,10 @@ def get_bugzilla():
93107
"update_bugs",
94108
)
95109
return InstrumentedClient(
96-
wrapped=bugzilla_client, prefix="jbi_bugzilla", methods=instrumented_methods
110+
wrapped=bugzilla_client,
111+
prefix="jbi_bugzilla",
112+
methods=instrumented_methods,
113+
exceptions=(rh_bugzilla.BugzillaError,),
97114
)
98115

99116

tests/unit/jbi/test_services.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44
from unittest import mock
55

6+
import bugzilla
67
import pytest
78

89
from src.jbi.services import get_bugzilla, get_jira
@@ -45,3 +46,16 @@ def test_timer_is_used_on_bugzilla_getcomments():
4546
bugzilla_client.get_comments([])
4647

4748
assert mocked.called, "Timer was used on get_comments()"
49+
50+
51+
def test_bugzilla_methods_are_retried_if_raising():
52+
with mock.patch(
53+
"src.jbi.services.rh_bugzilla.Bugzilla.return_value.get_comments"
54+
) as mocked:
55+
mocked.side_effect = (bugzilla.BugzillaError("boom"), [mock.sentinel])
56+
bugzilla_client = get_bugzilla()
57+
58+
# Not raising
59+
bugzilla_client.get_comments([])
60+
61+
assert mocked.call_count == 2

0 commit comments

Comments
 (0)