From 56c96ebec73dc73dac7e5233019ae95b6dcfb4c5 Mon Sep 17 00:00:00 2001 From: Srinivas Chippagiri Date: Thu, 5 Nov 2026 02:04:28 -0800 Subject: [PATCH] fix: allow a minimum retry wait of 0 ms random_time() and jitter() pick the wait with random.randrange(minimum, maximum), which raises ValueError on an empty range. With min_wait_in_ms set to 0 both bounds are 0, so the first retry crashed instead of retrying immediately. Return the minimum when the range is empty. Signed-off-by: Srinivas Chippagiri --- openfga_sdk/api_client.py | 3 +++ openfga_sdk/oauth2_common.py | 6 +++++- openfga_sdk/sync/api_client.py | 3 +++ test/backoff_test.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 test/backoff_test.py diff --git a/openfga_sdk/api_client.py b/openfga_sdk/api_client.py index 4c521bcf..079d09d9 100644 --- a/openfga_sdk/api_client.py +++ b/openfga_sdk/api_client.py @@ -38,6 +38,9 @@ def random_time(loop_count, min_wait_in_ms) -> float: """ minimum = math.ceil(2**loop_count * min_wait_in_ms) maximum = math.ceil(2 ** (loop_count + 1) * min_wait_in_ms) + # randrange() raises on an empty range, e.g. when min_wait_in_ms is 0 + if maximum <= minimum: + return minimum / 1000 return random.randrange(minimum, maximum) / 1000 diff --git a/openfga_sdk/oauth2_common.py b/openfga_sdk/oauth2_common.py index 71562a6a..6eb538ca 100644 --- a/openfga_sdk/oauth2_common.py +++ b/openfga_sdk/oauth2_common.py @@ -19,7 +19,11 @@ def jitter(loop_count, min_wait_in_ms): """ minimum = math.ceil(2**loop_count * min_wait_in_ms) maximum = math.ceil(2 ** (loop_count + 1) * min_wait_in_ms) - jitter = random.randrange(minimum, maximum) / 1000 + # randrange() raises on an empty range, e.g. when min_wait_in_ms is 0 + if maximum <= minimum: + jitter = minimum / 1000 + else: + jitter = random.randrange(minimum, maximum) / 1000 # If running in pytest, set jitter to 0 to speed up tests if "pytest" in sys.modules: diff --git a/openfga_sdk/sync/api_client.py b/openfga_sdk/sync/api_client.py index af0e67e3..10f15f75 100644 --- a/openfga_sdk/sync/api_client.py +++ b/openfga_sdk/sync/api_client.py @@ -37,6 +37,9 @@ def random_time(loop_count, min_wait_in_ms) -> float: """ minimum = math.ceil(2**loop_count * min_wait_in_ms) maximum = math.ceil(2 ** (loop_count + 1) * min_wait_in_ms) + # randrange() raises on an empty range, e.g. when min_wait_in_ms is 0 + if maximum <= minimum: + return minimum / 1000 return random.randrange(minimum, maximum) / 1000 diff --git a/test/backoff_test.py b/test/backoff_test.py new file mode 100644 index 00000000..d1ed32bd --- /dev/null +++ b/test/backoff_test.py @@ -0,0 +1,29 @@ +import unittest + +from openfga_sdk import api_client +from openfga_sdk.oauth2_common import jitter +from openfga_sdk.sync import api_client as sync_api_client + + +class TestBackoff(unittest.TestCase): + """The retry wait helpers accept a minimum wait of 0""" + + def test_random_time_with_zero_min_wait(self): + for loop_count in range(3): + self.assertEqual(api_client.random_time(loop_count, 0), 0) + self.assertEqual(sync_api_client.random_time(loop_count, 0), 0) + + def test_random_time_stays_in_range(self): + for loop_count in range(3): + for random_time in (api_client.random_time, sync_api_client.random_time): + wait = random_time(loop_count, 100) + self.assertGreaterEqual(wait, 2**loop_count * 100 / 1000) + self.assertLess(wait, 2 ** (loop_count + 1) * 100 / 1000) + + def test_jitter_with_zero_min_wait(self): + for loop_count in range(3): + self.assertEqual(jitter(loop_count, 0), 0) + + +if __name__ == "__main__": + unittest.main()