Skip to content

Commit 1d1feec

Browse files
committed
Add retry logic for all management commands
- use tenacity library - log at INFO level - Remove some exception handling in linksearchtotal_collect command to enable retry Bug: T403209
1 parent f7c2364 commit 1d1feec

File tree

6 files changed

+80
-34
lines changed

6 files changed

+80
-34
lines changed

extlinks/aggregates/tests.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,19 @@
3535
from extlinks.organisations.models import Organisation
3636

3737

38-
class LinkAggregateCommandTest(TransactionTestCase):
38+
class BaseTransactionTest(TransactionTestCase):
39+
@classmethod
40+
def setUpClass(cls):
41+
super(BaseTransactionTest, cls).setUpClass()
42+
cls.tenacity_patcher = mock.patch('tenacity.nap.time')
43+
cls.mock_tenacity = cls.tenacity_patcher.start()
44+
45+
@classmethod
46+
def tearDownClass(cls):
47+
super(BaseTransactionTest, cls).tearDownClass()
48+
cls.tenacity_patcher.stop()
49+
50+
class LinkAggregateCommandTest(BaseTransactionTest):
3951
def setUp(self):
4052
# Creating one Collection
4153
self.organisation = OrganisationFactory(name="ACME Org")
@@ -233,7 +245,7 @@ def test_link_aggregate_with_argument_delete_org(self):
233245
call_command("fill_link_aggregates", collections=[new_collection.pk])
234246

235247

236-
class UserAggregateCommandTest(TransactionTestCase):
248+
class UserAggregateCommandTest(BaseTransactionTest):
237249
def setUp(self):
238250
# Creating one Collection
239251
self.organisation = OrganisationFactory(name="ACME Org")
@@ -481,7 +493,7 @@ def test_user_aggregate_with_argument_delete_org(self):
481493
call_command("fill_user_aggregates", collections=[new_collection.pk])
482494

483495

484-
class PageProjectAggregateCommandTest(TransactionTestCase):
496+
class PageProjectAggregateCommandTest(BaseTransactionTest):
485497
def setUp(self):
486498
# Creating one Collection
487499
self.organisation = OrganisationFactory(name="ACME Org")
@@ -739,7 +751,7 @@ def test_pageproject_aggregate_with_argument_delete_org(self):
739751
call_command("fill_pageproject_aggregates", collections=[new_collection.pk])
740752

741753

742-
class MonthlyLinkAggregateCommandTest(TransactionTestCase):
754+
class MonthlyLinkAggregateCommandTest(BaseTransactionTest):
743755
def setUp(self):
744756
self.organisation = OrganisationFactory(name="ACME Org")
745757
self.collection = CollectionFactory(name="ACME", organisation=self.organisation)
@@ -863,7 +875,7 @@ def test_specific_year_month(self):
863875
)
864876

865877

866-
class MonthlyUserAggregateCommandTest(TransactionTestCase):
878+
class MonthlyUserAggregateCommandTest(BaseTransactionTest):
867879
def setUp(self):
868880
self.organisation = OrganisationFactory(name="ACME Org")
869881
self.collection = CollectionFactory(name="ACME", organisation=self.organisation)
@@ -1051,7 +1063,7 @@ def test_specific_year_month(self):
10511063
)
10521064

10531065

1054-
class MonthlyPageProjectAggregateCommandTest(TransactionTestCase):
1066+
class MonthlyPageProjectAggregateCommandTest(BaseTransactionTest):
10551067
def setUp(self):
10561068
self.organisation = OrganisationFactory(name="ACME Org")
10571069
self.collection = CollectionFactory(name="ACME", organisation=self.organisation)
@@ -1259,7 +1271,7 @@ def test_specific_year_month(self):
12591271
)
12601272

12611273

1262-
class ArchiveLinkAggregatesCommandTest(TransactionTestCase):
1274+
class ArchiveLinkAggregatesCommandTest(BaseTransactionTest):
12631275
def setUp(self):
12641276
self.organisation = OrganisationFactory(name="JSTOR")
12651277
self.collection = CollectionFactory(
@@ -1518,7 +1530,7 @@ def test_archive_link_aggregates_no_dates(self, mock_swift_connection):
15181530
self.assertEqual(LinkAggregate.objects.count(), 1)
15191531

15201532

1521-
class ArchiveUserAggregatesCommandTest(TransactionTestCase):
1533+
class ArchiveUserAggregatesCommandTest(BaseTransactionTest):
15221534
def setUp(self):
15231535
self.user = UserFactory(username="jonsnow")
15241536
self.organisation = OrganisationFactory(name="JSTOR")
@@ -1782,7 +1794,7 @@ def test_archive_user_aggregates_no_dates(self, mock_swift_connection):
17821794
self.assertEqual(UserAggregate.objects.count(), 1)
17831795

17841796

1785-
class ArchivePageProjectAggregatesCommandTest(TransactionTestCase):
1797+
class ArchivePageProjectAggregatesCommandTest(BaseTransactionTest):
17861798
def setUp(self):
17871799
self.page = "TestPage"
17881800
self.project = "en.wikipedia.org"
@@ -2051,7 +2063,7 @@ def test_archive_pageproject_aggregates_no_dates(self, mock_swift_connection):
20512063
self.assertEqual(PageProjectAggregate.objects.count(), 1)
20522064

20532065

2054-
class UploadAllArchivedAggregatesCommandTest(TransactionTestCase):
2066+
class UploadAllArchivedAggregatesCommandTest(BaseTransactionTest):
20552067
@mock.patch.dict(
20562068
os.environ,
20572069
{

extlinks/common/management/commands/__init__.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,47 @@
44
import logging
55
from os import remove
66
from os.path import basename
7+
import sys
8+
from tenacity import Retrying, stop_after_attempt, wait_exponential
9+
10+
logger = logging.getLogger("django")
711

812

913
class BaseCommand(DjangoBaseCommand):
1014
"""
11-
Django BaseCommand wrapper that adds file locks to management commands
15+
Django BaseCommand wrapper that adds
16+
- file locks
17+
- up to 5 retries with exponential backoff
1218
"""
1319

20+
def __init__(self, *args, **options):
21+
super().__init__(*args, **options)
22+
self.name = basename(inspect.getfile(self.__class__))
23+
self.e = None
24+
25+
def retry_log(self, retry_state):
26+
logger.warning(f"{self.name} attempt {retry_state.attempt_number} failed")
27+
1428
def handle(self, *args, **options):
15-
lockname = basename(inspect.getfile(self.__class__))
1629
# Use a lockfile to prevent overruns.
17-
lockfile = "/tmp/{}.lock".format(lockname)
30+
logger.info(f"Executing {self.name}")
31+
lockfile = f"/tmp/{self.name}.lock"
1832
lock = FileLock(lockfile)
1933
lock.acquire()
2034
try:
21-
self._handle(*args, **options)
22-
finally:
23-
lock.release()
24-
remove(lockfile)
35+
for attempt in Retrying(
36+
after=self.retry_log,
37+
reraise=True,
38+
stop=stop_after_attempt(5),
39+
wait=wait_exponential(multiplier=1, min=60, max=300),
40+
):
41+
with attempt:
42+
self._handle(*args, **options)
43+
except Exception as e:
44+
logger.warning(f"Retries exhausted for {self.name}")
45+
logger.error(e)
46+
self.e = e
47+
lock.release()
48+
remove(lockfile)
49+
if self.e is not None:
50+
raise self.e

extlinks/links/management/commands/linksearchtotal_collect.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,12 @@ def _handle(self, *args, **options):
3232
total_links_dictionary = {}
3333
for i, language in enumerate(wiki_list_data):
3434
logger.info(f"connecting to db {language}")
35-
try:
36-
db = MySQLdb.connect(
37-
host=f"{language}wiki.analytics.db.svc.wikimedia.cloud",
38-
user=os.environ["REPLICA_DB_USER"],
39-
passwd=os.environ["REPLICA_DB_PASSWORD"],
40-
db=f"{language}wiki_p",
41-
)
42-
except MySQLdb.OperationalError as e:
43-
logger.error(str(e))
44-
sys.exit(1)
35+
db = MySQLdb.connect(
36+
host=f"{language}wiki.analytics.db.svc.wikimedia.cloud",
37+
user=os.environ["REPLICA_DB_USER"],
38+
passwd=os.environ["REPLICA_DB_PASSWORD"],
39+
db=f"{language}wiki_p",
40+
)
4541

4642
cur = db.cursor()
4743

extlinks/links/tests.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,19 @@
2323
from .helpers import link_is_tracked, reverse_host
2424
from .models import URLPattern, LinkEvent
2525

26-
27-
class LinksHelpersTest(TestCase):
26+
class BaseTest(TestCase):
27+
@classmethod
28+
def setUpClass(cls):
29+
super(BaseTest, cls).setUpClass()
30+
cls.tenacity_patcher = mock.patch('tenacity.nap.time')
31+
cls.mock_tenacity = cls.tenacity_patcher.start()
32+
33+
@classmethod
34+
def tearDownClass(cls):
35+
super(BaseTest, cls).tearDownClass()
36+
cls.tenacity_patcher.stop()
37+
38+
class LinksHelpersTest(BaseTest):
2839
def test_reverse_host(self):
2940
"""
3041
Test the reverse_host function in helpers.py.
@@ -58,7 +69,7 @@ def test_get_organisation(self):
5869
self.assertEqual(new_link.get_organisation, organisation2)
5970

6071

61-
class LinksTrackedTest(TestCase):
72+
class LinksTrackedTest(BaseTest):
6273
def setUp(self):
6374
_ = URLPatternFactory(url="test.com")
6475

@@ -123,7 +134,7 @@ def test_link_is_tracked_false_other_proxy(self):
123134
)
124135

125136

126-
class URLPatternModelTest(TestCase):
137+
class URLPatternModelTest(BaseTest):
127138
def test_get_proxied_url_1(self):
128139
"""
129140
Test that URLPattern.get_proxied_url transforms a URL correctly
@@ -140,7 +151,7 @@ def test_get_proxied_url_2(self):
140151
self.assertEqual(test_urlpattern.get_proxied_url, "platform-almanhal-com")
141152

142153

143-
class LinkEventsCollectCommandTest(TestCase):
154+
class LinkEventsCollectCommandTest(BaseTest):
144155
def setUp(self):
145156
self.organisation1 = OrganisationFactory(name="JSTOR")
146157
self.collection1 = CollectionFactory(
@@ -1250,7 +1261,7 @@ def test_fix_proxy_linkevents_on_user_list_command(self):
12501261
self.assertEqual(PageProjectAggregate.objects.count(), 2)
12511262

12521263

1253-
class UploadAllArchivedTestCase(TestCase):
1264+
class UploadAllArchivedTestCase(BaseTest):
12541265
@mock.patch.dict(
12551266
os.environ,
12561267
{

extlinks/settings/logging.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
},
2626
"handlers": {
2727
"nodebug_console": {
28-
"level": "WARNING",
28+
"level": "INFO",
2929
"filters": ["require_debug_false"],
3030
"class": "logging.StreamHandler",
3131
"formatter": "django.server",

requirements/django.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pymemcache==4.0.0
88
python-dotenv==1.1.0
99
sentry-sdk==2.10.0
1010
sseclient==0.0.27
11+
tenacity==9.1.2
1112
time_machine==2.14.2
1213
python-swiftclient>=4.6.0,<5.0.0
1314
keystoneauth1>=5.9.2,<6.0.0

0 commit comments

Comments
 (0)