Skip to content

Commit a018a9a

Browse files
Retry Garmin stat downloads on transient failures (#308)
1 parent f4c53f0 commit a018a9a

2 files changed

Lines changed: 74 additions & 3 deletions

File tree

garmindb/download.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,18 @@ def __get_stat(self, stat_function, directory, date, days, overwrite):
113113
download_date = date + datetime.timedelta(days=day)
114114
# always overwrite for yesterday and today since the last download may have been a partial result
115115
delta = datetime.datetime.now().date() - download_date
116-
stat_function(directory, download_date, overwrite or delta.days <= self.download_days_overlap)
116+
stat_overwrite = overwrite or delta.days <= self.download_days_overlap
117+
for attempt in range(1, 6):
118+
try:
119+
stat_function(directory, download_date, stat_overwrite)
120+
break
121+
except Exception as e:
122+
if attempt == 5:
123+
root_logger.error("Failed to download %s after %d attempts: %s", download_date, attempt, e)
124+
else:
125+
backoff_seconds = attempt * 5
126+
root_logger.warning("Retrying %s after error on attempt %d/%d: %s", download_date, attempt, 5, e)
127+
time.sleep(backoff_seconds)
117128
# pause for a second between every page access
118129
time.sleep(1)
119130

@@ -296,8 +307,9 @@ def __get_hrv_day(self, directory, day, overwrite=False):
296307
url = f'{self.garmin_connect_hrv_url}/{date_str}'
297308
try:
298309
self.save_json_to_file(json_filename, self.garmin.connectapi(url), overwrite)
299-
except GarminConnectAuthError as e:
300-
root_logger.error("Exception getting daily summary %s", e)
310+
except Exception as e:
311+
root_logger.error("Exception getting hrv for %s: %s", date_str, e)
312+
raise
301313

302314
def get_hrv(self, directory, date, days, overwrite):
303315
"""Download the heart rate variability (HRV) data from Garmin Connect and save to a JSON file."""

test/test_download.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""Tests for download retry behavior."""
2+
3+
__author__ = "Tom Goetz"
4+
__copyright__ = "Copyright Tom Goetz"
5+
__license__ = "GPL"
6+
7+
import datetime
8+
import unittest
9+
from unittest import mock
10+
11+
from garmindb.download import Download
12+
13+
14+
class FakeConfig:
15+
"""Minimal config stub for Download tests."""
16+
17+
def get_session_file(self):
18+
return "/tmp/garth_session"
19+
20+
def get_garmin_base_domain(self):
21+
return "garmin.com"
22+
23+
24+
class TestDownload(unittest.TestCase):
25+
def setUp(self):
26+
self.download = Download(FakeConfig())
27+
28+
def test_get_stat_retries_until_success(self):
29+
attempts = []
30+
31+
def flaky_stat(directory, day, overwrite):
32+
attempts.append((directory, day, overwrite))
33+
if len(attempts) < 3:
34+
raise RuntimeError("temporary HRV gap")
35+
36+
start_date = datetime.date(2026, 3, 1)
37+
with mock.patch("garmindb.download.time.sleep"):
38+
self.download._Download__get_stat(flaky_stat, "/tmp/hrv", start_date, 1, False)
39+
40+
self.assertEqual(len(attempts), 3)
41+
self.assertEqual(attempts[0][0], "/tmp/hrv")
42+
self.assertEqual(attempts[0][1], start_date)
43+
44+
def test_get_stat_stops_after_five_failures(self):
45+
attempts = []
46+
47+
def always_fail(directory, day, overwrite):
48+
attempts.append((directory, day, overwrite))
49+
raise RuntimeError("persistent HRV gap")
50+
51+
start_date = datetime.date(2026, 3, 1)
52+
with mock.patch("garmindb.download.time.sleep"):
53+
self.download._Download__get_stat(always_fail, "/tmp/hrv", start_date, 1, False)
54+
55+
self.assertEqual(len(attempts), 5)
56+
57+
58+
if __name__ == "__main__":
59+
unittest.main(verbosity=2)

0 commit comments

Comments
 (0)