@@ -125,6 +125,13 @@ def __init__(self, **kwargs):
125125 - **impersonate** (str): Name of user to impersonate.
126126 - **re_connect_on_session_timeout** (bool): Attempt to reconnect once if session is timed out.
127127 - **re_connect_on_remote_disconnect** (bool): Attempt to reconnect once if connection is aborted by remote end.
128+ - **remote_disconnect_max_retries** (int): Maximum number of retry attempts after remote disconnect (default: 5).
129+ - **remote_disconnect_retry_delay** (float): Initial delay in seconds before first retry attempt (default: 1).
130+ - **remote_disconnect_max_delay** (float): Maximum delay cap in seconds between retry attempts (default: 30).
131+ - **remote_disconnect_backoff_factor** (float): Multiplier for exponential backoff between retry attempts (default: 2).
132+ - **async_polling_initial_delay** (float): Initial polling delay in seconds for async operations (default: 0.1).
133+ - **async_polling_max_delay** (float): Maximum polling delay cap in seconds for async operations (default: 1.0).
134+ - **async_polling_backoff_factor** (float): Multiplier for exponential backoff in async polling (default: 2).
128135 - **proxies** (dict): Dictionary with proxies, e.g. {'http': 'http://proxy.example.com:8080', 'https': 'http://secureproxy.example.com:8090'}.
129136 - **ssl_context**: User-defined SSL context.
130137 - **cert** (str|tuple): (Optional) If string, path to SSL client cert file (.pem). If tuple, ('cert', 'key') pair.
@@ -158,6 +165,13 @@ def __init__(self, **kwargs):
158165 self ._pool_connections = int (kwargs .get ("pool_connections" , self .DEFAULT_POOL_CONNECTIONS ))
159166 self ._re_connect_on_session_timeout = kwargs .get ("re_connect_on_session_timeout" , True )
160167 self ._re_connect_on_remote_disconnect = kwargs .get ("re_connect_on_remote_disconnect" , True )
168+ self ._remote_disconnect_max_retries = int (kwargs .get ("remote_disconnect_max_retries" , 5 ))
169+ self ._remote_disconnect_retry_delay = float (kwargs .get ("remote_disconnect_retry_delay" , 1 ))
170+ self ._remote_disconnect_max_delay = float (kwargs .get ("remote_disconnect_max_delay" , 30 ))
171+ self ._remote_disconnect_backoff_factor = float (kwargs .get ("remote_disconnect_backoff_factor" , 2 ))
172+ self ._async_polling_initial_delay = float (kwargs .get ("async_polling_initial_delay" , 0.1 ))
173+ self ._async_polling_max_delay = float (kwargs .get ("async_polling_max_delay" , 1.0 ))
174+ self ._async_polling_backoff_factor = float (kwargs .get ("async_polling_backoff_factor" , 2 ))
161175 # is retrieved on demand and then cached
162176 self ._sandboxing_disabled = None
163177 # optional verbose logging to stdout
@@ -390,7 +404,7 @@ def _poll_async_response(self, async_id: str, timeout: float, cancel_at_timeout:
390404 """
391405 Poll for async operation completion
392406 """
393- for wait in RestService .wait_time_generator (timeout ):
407+ for wait in self .wait_time_generator (timeout ):
394408 response = self .retrieve_async_response (async_id )
395409 if response .status_code in [200 , 201 ]:
396410 return response
@@ -431,52 +445,77 @@ def _handle_remote_disconnect(
431445 ** kwargs ,
432446 ):
433447 """
434- Handle remote disconnect errors with reconnection and retry logic
448+ Handle remote disconnect errors with reconnection and retry logic using exponential backoff.
449+
450+ Retries up to `remote_disconnect_max_retries` times with capped exponential backoff delay.
451+ The delay is calculated as: min(delay * backoff_factor^(attempt-1), max_delay).
435452 """
436- warnings .warn (f"Connection aborted due to remote disconnect. Attempting to reconnect : { original_error } " )
453+ warnings .warn (f"Connection aborted due to remote disconnect: { original_error } " )
437454
438- try :
439- # Reconnect
440- self ._manage_http_adapter ()
441- self .connect ()
455+ for attempt in range (1 , self ._remote_disconnect_max_retries + 1 ):
456+ # Calculate delay with exponential backoff: delay * backoff_factor^(attempt-1), capped at max_delay
457+ current_delay = min (
458+ self ._remote_disconnect_retry_delay * (self ._remote_disconnect_backoff_factor ** (attempt - 1 )),
459+ self ._remote_disconnect_max_delay
460+ )
442461
443- # Only retry if idempotent
444- if not idempotent :
445- warnings .warn (
446- f"Successfully reconnected but not retrying { method .upper ()} request (idempotent={ idempotent } )"
447- )
448- raise original_error
462+ warnings .warn (
463+ f"Retry attempt { attempt } /{ self ._remote_disconnect_max_retries } "
464+ f"after { current_delay :.1f} s delay..."
465+ )
449466
450- warnings . warn ( f"Successfully reconnected. Retrying { method . upper () } request..." )
467+ time . sleep ( current_delay )
451468
452- # Retry the request using the same execution path
453- if not async_requests_mode :
454- response = self ._execute_sync_request (method = method , url = url , data = data , timeout = timeout , ** kwargs )
455- else :
456- response = self ._execute_async_request (
457- method = method ,
458- url = url ,
459- data = data ,
460- timeout = timeout ,
461- cancel_at_timeout = cancel_at_timeout ,
462- return_async_id = return_async_id ,
463- ** kwargs ,
464- )
469+ try :
470+ # Reconnect
471+ self ._manage_http_adapter ()
472+ self .connect ()
473+
474+ # Only retry if idempotent
475+ if not idempotent :
476+ warnings .warn (
477+ f"Successfully reconnected but not retrying { method .upper ()} request (idempotent={ idempotent } )"
478+ )
479+ raise original_error
465480
466- # Verify and encode response
467- self .verify_response (response = response )
468- response .encoding = encoding
469- return response
481+ warnings .warn (f"Successfully reconnected. Retrying { method .upper ()} request..." )
482+
483+ # Retry the request using the same execution path
484+ if not async_requests_mode :
485+ response = self ._execute_sync_request (method = method , url = url , data = data , timeout = timeout , ** kwargs )
486+ else :
487+ response = self ._execute_async_request (
488+ method = method ,
489+ url = url ,
490+ data = data ,
491+ timeout = timeout ,
492+ cancel_at_timeout = cancel_at_timeout ,
493+ return_async_id = return_async_id ,
494+ ** kwargs ,
495+ )
496+
497+ # Verify and encode response
498+ self .verify_response (response = response )
499+ response .encoding = encoding
500+ return response
470501
471- except TM1pyTimeout :
472- # Re-raise timeout exceptions as-is
473- raise
474- except TM1pyRestException :
475- # Re-raise TM1 exceptions as-is
476- raise
477- except Exception as retry_error :
478- warnings .warn (f"Failed to reconnect or retry after remote disconnect: { retry_error } " )
479- raise original_error
502+ except TM1pyTimeout :
503+ # Re-raise timeout exceptions as-is
504+ raise
505+ except TM1pyRestException :
506+ # Re-raise TM1 exceptions as-is
507+ raise
508+ except Exception as retry_error :
509+ warnings .warn (
510+ f"Retry attempt { attempt } /{ self ._remote_disconnect_max_retries } failed: { retry_error } "
511+ )
512+ continue
513+
514+ # All retries exhausted
515+ warnings .warn (
516+ f"All { self ._remote_disconnect_max_retries } retry attempts failed after remote disconnect"
517+ )
518+ raise original_error
480519
481520 def connect (self ):
482521 if "session_id" in self ._kwargs :
@@ -1257,17 +1296,30 @@ def build_response_from_binary_response(data: bytes) -> Response:
12571296
12581297 return requests_response
12591298
1260- @staticmethod
1261- def wait_time_generator (timeout : int ):
1262- yield 0.1
1263- yield 0.3
1264- yield 0.6
1299+ def wait_time_generator (self , timeout : float ):
1300+ """
1301+ Generate wait times for async polling with capped exponential backoff.
1302+
1303+ Uses configurable parameters:
1304+ - async_polling_initial_delay: Starting delay
1305+ - async_polling_max_delay: Maximum delay cap
1306+ - async_polling_backoff_factor: Multiplier for each iteration
1307+
1308+ Default behavior (0.1s initial, 1.0s max, 2x factor) produces:
1309+ 0.1s -> 0.2s -> 0.4s -> 0.8s -> 1.0s -> 1.0s -> ...
1310+ """
1311+ delay = self ._async_polling_initial_delay
1312+ elapsed = 0.0
1313+
12651314 if timeout :
1266- for _ in range (1 , int (timeout )):
1267- yield 1
1315+ while elapsed < timeout :
1316+ yield delay
1317+ elapsed += delay
1318+ delay = min (delay * self ._async_polling_backoff_factor , self ._async_polling_max_delay )
12681319 else :
12691320 while True :
1270- yield 1
1321+ yield delay
1322+ delay = min (delay * self ._async_polling_backoff_factor , self ._async_polling_max_delay )
12711323
12721324 def _determine_ssl_based_on_base_url (self ) -> bool :
12731325 if self ._base_url .startswith ("https" ):
0 commit comments