From d02fc6ccd71b5b2e8b1dc6a9f2d3c584fc8d31c4 Mon Sep 17 00:00:00 2001 From: Alexey Shamrin Date: Thu, 23 Oct 2025 14:10:55 +0300 Subject: [PATCH 1/5] instances.create: expose retry settings, add exponential backoff --- CHANGELOG.md | 8 ++++++++ datacrunch/instances/instances.py | 26 ++++++++++++++++++-------- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce30412..b10f174 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Added `max_wait_time`, `initial_interval`, `max_interval`, `backoff_coefficient` keyword arguments to `instances.create()` + +### Changed + +- Cap retry interval to 5 seconds during `instances.create()` and add exponential backoff + ## [1.14.0] - 2025-08-15 ### Added diff --git a/datacrunch/instances/instances.py b/datacrunch/instances/instances.py index ce67c43..8346f02 100644 --- a/datacrunch/instances/instances.py +++ b/datacrunch/instances/instances.py @@ -123,7 +123,12 @@ def create(self, is_spot: bool = False, contract: Optional[Contract] = None, pricing: Optional[Pricing] = None, - coupon: Optional[str] = None) -> Instance: + coupon: Optional[str] = None, + *, + max_wait_time: float = 60, + initial_interval: float = 0.5, + max_interval: float = 5, + backoff_coefficient: float = 2.0) -> Instance: """Creates and deploys a new cloud instance. Args: @@ -141,6 +146,10 @@ def create(self, contract: Optional contract type for the instance. pricing: Optional pricing model for the instance. coupon: Optional coupon code for discounts. + max_wait_time: Maximum total wait for the instance to start provisioning, in seconds (default: 60) + initial_interval: Initial interval, in seconds (default: 0.5) + max_interval: The longest single delay allowed between retries, in seconds (default: 5) + backoff_coefficient: Coefficient to calculate the next retry interval (default 2.0) Returns: The newly created instance object. @@ -169,20 +178,21 @@ def create(self, id = self._http_client.post(INSTANCES_ENDPOINT, json=payload).text # Wait for instance to enter provisioning state with timeout - MAX_WAIT_TIME = 60 # Maximum wait time in seconds - POLL_INTERVAL = 0.5 # Time between status checks - - start_time = time.time() + interval = min(initial_interval, max_interval) + start = time.monotonic() + deadline = start + max_wait_time while True: instance = self.get_by_id(id) if instance.status != InstanceStatus.ORDERED: return instance - if time.time() - start_time > MAX_WAIT_TIME: + now = time.monotonic() + if now >= deadline: raise TimeoutError( - f"Instance {id} did not enter provisioning state within {MAX_WAIT_TIME} seconds") + f"Instance {id} did not enter provisioning state within {max_wait_time:.1f} seconds") - time.sleep(POLL_INTERVAL) + time.sleep(min(interval, deadline - now)) + interval = min(interval * backoff_coefficient, max_interval) def action(self, id_list: Union[List[str], str], action: str, volume_ids: Optional[List[str]] = None) -> None: """Performs an action on one or more instances. From c11b9b21e29b105be44ab585dd998f46feb2c997 Mon Sep 17 00:00:00 2001 From: Alexey Shamrin Date: Thu, 23 Oct 2025 14:14:43 +0300 Subject: [PATCH 2/5] remove unnecessary variable --- datacrunch/instances/instances.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/datacrunch/instances/instances.py b/datacrunch/instances/instances.py index 8346f02..d615f6e 100644 --- a/datacrunch/instances/instances.py +++ b/datacrunch/instances/instances.py @@ -179,8 +179,7 @@ def create(self, # Wait for instance to enter provisioning state with timeout interval = min(initial_interval, max_interval) - start = time.monotonic() - deadline = start + max_wait_time + deadline = time.monotonic() + max_wait_time while True: instance = self.get_by_id(id) if instance.status != InstanceStatus.ORDERED: From 68ef37a3f183fe2acd10aa1077ef68bd42d1fe45 Mon Sep 17 00:00:00 2001 From: Alexey Shamrin Date: Thu, 23 Oct 2025 15:18:59 +0300 Subject: [PATCH 3/5] increase default max_wait_time to 180 seconds --- CHANGELOG.md | 2 +- datacrunch/instances/instances.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b10f174..3808896 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Cap retry interval to 5 seconds during `instances.create()` and add exponential backoff +- Cap `instances.create()` retry interval to 5 seconds; add exponential backoff; increase default `max_wait_time` from 60 to 180 seconds ## [1.14.0] - 2025-08-15 diff --git a/datacrunch/instances/instances.py b/datacrunch/instances/instances.py index d615f6e..42d178b 100644 --- a/datacrunch/instances/instances.py +++ b/datacrunch/instances/instances.py @@ -125,7 +125,7 @@ def create(self, pricing: Optional[Pricing] = None, coupon: Optional[str] = None, *, - max_wait_time: float = 60, + max_wait_time: float = 180, initial_interval: float = 0.5, max_interval: float = 5, backoff_coefficient: float = 2.0) -> Instance: From 147706131c783b32304e49f600720a7b6b81126f Mon Sep 17 00:00:00 2001 From: Alexey Shamrin Date: Thu, 23 Oct 2025 15:43:45 +0300 Subject: [PATCH 4/5] refactor exponential backoff loop --- datacrunch/instances/instances.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/datacrunch/instances/instances.py b/datacrunch/instances/instances.py index 42d178b..5676d64 100644 --- a/datacrunch/instances/instances.py +++ b/datacrunch/instances/instances.py @@ -1,4 +1,5 @@ import time +import itertools from typing import List, Union, Optional, Dict, Literal from dataclasses import dataclass from dataclasses_json import dataclass_json @@ -178,9 +179,8 @@ def create(self, id = self._http_client.post(INSTANCES_ENDPOINT, json=payload).text # Wait for instance to enter provisioning state with timeout - interval = min(initial_interval, max_interval) deadline = time.monotonic() + max_wait_time - while True: + for i in itertools.count(): instance = self.get_by_id(id) if instance.status != InstanceStatus.ORDERED: return instance @@ -190,8 +190,8 @@ def create(self, raise TimeoutError( f"Instance {id} did not enter provisioning state within {max_wait_time:.1f} seconds") - time.sleep(min(interval, deadline - now)) - interval = min(interval * backoff_coefficient, max_interval) + interval = min(initial_interval * backoff_coefficient ** i, max_interval, deadline - now) + time.sleep(interval) def action(self, id_list: Union[List[str], str], action: str, volume_ids: Optional[List[str]] = None) -> None: """Performs an action on one or more instances. From 8c3f23f617b2cf1d93da0f5b0ad0f4dcee4ff0d8 Mon Sep 17 00:00:00 2001 From: Alexey Shamrin Date: Thu, 23 Oct 2025 16:31:42 +0300 Subject: [PATCH 5/5] fix docstring --- datacrunch/instances/instances.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datacrunch/instances/instances.py b/datacrunch/instances/instances.py index 5676d64..1cb8c2c 100644 --- a/datacrunch/instances/instances.py +++ b/datacrunch/instances/instances.py @@ -147,7 +147,7 @@ def create(self, contract: Optional contract type for the instance. pricing: Optional pricing model for the instance. coupon: Optional coupon code for discounts. - max_wait_time: Maximum total wait for the instance to start provisioning, in seconds (default: 60) + max_wait_time: Maximum total wait for the instance to start provisioning, in seconds (default: 180) initial_interval: Initial interval, in seconds (default: 0.5) max_interval: The longest single delay allowed between retries, in seconds (default: 5) backoff_coefficient: Coefficient to calculate the next retry interval (default 2.0)