Skip to content

Commit 5f2142b

Browse files
authored
Added request retry mechanism (#308)
1 parent 7b8f0ed commit 5f2142b

1 file changed

Lines changed: 89 additions & 28 deletions

File tree

razorpay/client.py

Lines changed: 89 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import json
33
import requests
44
import warnings
5+
import random
6+
import time
57

68
from types import ModuleType
79

@@ -34,7 +36,11 @@ class Client:
3436
"""Razorpay client class"""
3537

3638
DEFAULTS = {
37-
'base_url': URL.BASE_URL
39+
'base_url': URL.BASE_URL,
40+
'max_retries': 5,
41+
'initial_delay': 1,
42+
'max_delay': 60,
43+
'jitter': 0.25
3844
}
3945

4046
def __init__(self, session=None, auth=None, **options):
@@ -48,6 +54,11 @@ def __init__(self, session=None, auth=None, **options):
4854
self.cert_path = file_dir + '/ca-bundle.crt'
4955

5056
self.base_url = self._set_base_url(**options)
57+
self.max_retries = options.get('max_retries', self.DEFAULTS['max_retries'])
58+
self.initial_delay = options.get('initial_delay', self.DEFAULTS['initial_delay'])
59+
self.max_delay = options.get('max_delay', self.DEFAULTS['max_delay'])
60+
self.jitter = options.get('jitter', self.DEFAULTS['jitter'])
61+
self.retry_enabled = False
5162

5263
self.app_details = []
5364

@@ -66,6 +77,12 @@ def _set_base_url(self, **options):
6677
base_url = options['base_url']
6778
del(options['base_url'])
6879

80+
# Remove retry options from options if they exist
81+
options.pop('max_retries', None)
82+
options.pop('initial_delay', None)
83+
options.pop('max_delay', None)
84+
options.pop('jitter', None)
85+
6986
return base_url
7087

7188
def _update_user_agent_header(self, options):
@@ -128,16 +145,19 @@ def set_app_details(self, app_details):
128145
def get_app_details(self):
129146
return self.app_details
130147

148+
def enable_retry(self, retry_enabled=False):
149+
self.retry_enabled = retry_enabled
150+
131151
def request(self, method, path, **options):
132152
"""
133-
Dispatches a request to the Razorpay HTTP API
153+
Dispatches a request to the Razorpay HTTP API with retry mechanism
134154
"""
135155
options = self._update_user_agent_header(options)
136156

137157
# Determine authentication type
138158
use_public_auth = options.pop('use_public_auth', False)
139159
auth_to_use = self.auth
140-
160+
141161
if use_public_auth:
142162
# For public auth, use key_id only
143163
if self.auth and isinstance(self.auth, tuple) and len(self.auth) >= 1:
@@ -151,31 +171,72 @@ def request(self, method, path, **options):
151171
options['headers']['X-Razorpay-Device-Mode'] = device_mode
152172

153173
url = "{}{}".format(self.base_url, path)
154-
155-
response = getattr(self.session, method)(url, auth=auth_to_use,
156-
verify=self.cert_path,
157-
**options)
158-
if ((response.status_code >= HTTP_STATUS_CODE.OK) and
159-
(response.status_code < HTTP_STATUS_CODE.REDIRECT)):
160-
return json.dumps({}) if(response.status_code==204) else response.json()
161-
else:
162-
msg = ""
163-
code = ""
164-
json_response = response.json()
165-
if 'error' in json_response:
166-
if 'description' in json_response['error']:
167-
msg = json_response['error']['description']
168-
if 'code' in json_response['error']:
169-
code = str(json_response['error']['code'])
170-
171-
if str.upper(code) == ERROR_CODE.BAD_REQUEST_ERROR:
172-
raise BadRequestError(msg)
173-
elif str.upper(code) == ERROR_CODE.GATEWAY_ERROR:
174-
raise GatewayError(msg)
175-
elif str.upper(code) == ERROR_CODE.SERVER_ERROR: # nosemgrep : python.lang.maintainability.useless-ifelse.useless-if-body
176-
raise ServerError(msg)
177-
else:
178-
raise ServerError(msg)
174+
175+
delay_seconds = self.initial_delay
176+
177+
# If retry is not enabled, set max attempts to 1
178+
max_attempts = self.max_retries if self.retry_enabled else 1
179+
180+
for attempt in range(max_attempts):
181+
try:
182+
response = getattr(self.session, method)(url, auth=auth_to_use,
183+
verify=self.cert_path,
184+
**options)
185+
186+
if ((response.status_code >= HTTP_STATUS_CODE.OK) and
187+
(response.status_code < HTTP_STATUS_CODE.REDIRECT)):
188+
return json.dumps({}) if(response.status_code==204) else response.json()
189+
else:
190+
msg = ""
191+
code = ""
192+
json_response = response.json()
193+
if 'error' in json_response:
194+
if 'description' in json_response['error']:
195+
msg = json_response['error']['description']
196+
if 'code' in json_response['error']:
197+
code = str(json_response['error']['code'])
198+
199+
if str.upper(code) == ERROR_CODE.BAD_REQUEST_ERROR:
200+
raise BadRequestError(msg)
201+
elif str.upper(code) == ERROR_CODE.GATEWAY_ERROR:
202+
raise GatewayError(msg)
203+
elif str.upper(code) == ERROR_CODE.SERVER_ERROR: # nosemgrep : python.lang.maintainability.useless-ifelse.useless-if-body
204+
raise ServerError(msg)
205+
else:
206+
raise ServerError(msg)
207+
208+
except requests.exceptions.ConnectionError as e:
209+
if self.retry_enabled and attempt < max_attempts - 1: # Don't sleep on the last attempt
210+
# Apply exponential backoff with jitter
211+
jitter_value = random.uniform(-self.jitter, self.jitter)
212+
jittered_delay = delay_seconds * (1 + jitter_value)
213+
# Cap the delay at max_delay
214+
actual_delay = min(jittered_delay, self.max_delay)
215+
216+
print(f"ConnectionError: {e}. Retrying in {actual_delay:.2f} seconds... (Attempt {attempt + 1}/{max_attempts})")
217+
time.sleep(actual_delay)
218+
delay_seconds *= 2 # Exponential backoff for next attempt
219+
else:
220+
print(f"Connection failed." + (f" Max retries ({max_attempts}) exceeded." if self.retry_enabled else ""))
221+
raise
222+
except requests.exceptions.Timeout as e:
223+
if self.retry_enabled and attempt < max_attempts - 1: # Don't sleep on the last attempt
224+
# Apply exponential backoff with jitter
225+
jitter_value = random.uniform(-self.jitter, self.jitter)
226+
jittered_delay = delay_seconds * (1 + jitter_value)
227+
# Cap the delay at max_delay
228+
actual_delay = min(jittered_delay, self.max_delay)
229+
230+
print(f"Timeout: {e}. Retrying in {actual_delay:.2f} seconds... (Attempt {attempt + 1}/{max_attempts})")
231+
time.sleep(actual_delay)
232+
delay_seconds *= 2 # Exponential backoff for next attempt
233+
else:
234+
print(f"Request timed out." + (f" Max retries ({max_attempts}) exceeded." if self.retry_enabled else ""))
235+
raise
236+
except requests.exceptions.RequestException as e:
237+
# For other request exceptions, don't retry
238+
print(f"Request error occurred: {e}")
239+
raise
179240

180241
def get(self, path, params, **options):
181242
"""

0 commit comments

Comments
 (0)