max_retries and retry_delay constructor parameters are silently ignored
SDK version: 0.5.24 (also confirmed in 0.5.15)
Python version: 3.12
Description
The Runware / RunwareServer constructor accepts max_retries and retry_delay parameters, which are documented and stored internally, but never actually used. Both retry loops in RunwareBase ignore them entirely and use a hardcoded constant (MAX_RETRY_ATTEMPTS = 10) instead. Setting max_retries=0 or any other value has no observable effect on runtime behaviour.
Steps to reproduce
import asyncio
from runware import Runware, IVideoInference
async def main():
# Expectation: no retries. Reality: up to 10 retries will happen on ConnectionError.
runware = Runware(
api_key="your-api-key",
max_retries=0,
retry_delay=0,
)
await runware.connect()
# Submit any job that triggers a WebSocket drop mid-flight;
# the SDK will retry up to 10 times regardless of the values above.
asyncio.run(main())
The symptom we encountered: after a WebSocket drop, the SDK reconnects and resubmits the same request with the same taskUUID. The server returns a 409 conflictTaskUUID error — proving that retries did happen, despite max_retries=0.
RunwareAPIError: {
"code": "conflictTaskUUID",
"message": "Task UUID already exists",
"taskUUID": "<uuid-of-original-submission>"
}
Root cause
server.py (lines 33–48): both parameters are accepted and stored, but self._max_retries and self._retry_delay are never read again anywhere in the codebase.
# server.py
def __init__(
self,
api_key: str,
max_retries: int = 2, # ← stored...
retry_delay: int = 1, # ← stored...
):
...
self._max_retries: int = max_retries # ← ...but never used
self._retry_delay: int = retry_delay # ← ...but never used
utils.py (line 114): the actual retry count is a hardcoded module-level constant, not controllable via any constructor argument or environment variable (unlike MAX_CONCURRENT_REQUESTS, which does read from env).
# utils.py
MAX_RETRY_ATTEMPTS = 10 # hardcoded; no env-var override
base.py (lines 413 and 461): both _retry_with_reconnect and _retry_async_with_reconnect loop over MAX_RETRY_ATTEMPTS instead of self._max_retries, and use their own jitter/backoff logic instead of self._retry_delay.
# base.py — _retry_with_reconnect (line 413)
for attempt in range(MAX_RETRY_ATTEMPTS): # ← should be self._max_retries
...
if reconnected:
jitter = uniform(0.1, 0.5) * (attempt + 1)
await asyncio.sleep(jitter) # ← self._retry_delay not used
else:
delay = self._reconnection_manager.calculate_delay()
await asyncio.sleep(delay) # ← self._retry_delay not used
# base.py — _retry_async_with_reconnect (line 461)
for attempt in range(MAX_RETRY_ATTEMPTS): # ← same issue
...
Expected behaviour
max_retries=N should cap the number of retry attempts to N. max_retries=0 should mean no retries — a single attempt is made and any error is raised immediately to the caller.
retry_delay=N should control the sleep duration between retry attempts.
Suggested fix
In RunwareBase.__init__, accept and store the parameters (they could be passed down from RunwareServer), then use them in both retry loops:
# base.py — _retry_with_reconnect
for attempt in range(self._max_retries + 1): # +1 so 0 = one attempt, no retry
...
await asyncio.sleep(self._retry_delay)
# base.py — _retry_async_with_reconnect
for attempt in range(self._max_retries + 1):
...
await asyncio.sleep(self._retry_delay)
Alternatively, keep the jitter/backoff logic for the delay but at least wire self._max_retries into the loop bound.
Additional notes
retry_delay has the exact same problem: stored in RunwareServer, never referenced in RunwareBase.
MAX_RETRY_ATTEMPTS could remain as the default value when the caller does not pass max_retries, preserving backward compatibility.
max_retriesandretry_delayconstructor parameters are silently ignoredSDK version: 0.5.24 (also confirmed in 0.5.15)
Python version: 3.12
Description
The
Runware/RunwareServerconstructor acceptsmax_retriesandretry_delayparameters, which are documented and stored internally, but never actually used. Both retry loops inRunwareBaseignore them entirely and use a hardcoded constant (MAX_RETRY_ATTEMPTS = 10) instead. Settingmax_retries=0or any other value has no observable effect on runtime behaviour.Steps to reproduce
The symptom we encountered: after a WebSocket drop, the SDK reconnects and resubmits the same request with the same
taskUUID. The server returns a 409conflictTaskUUIDerror — proving that retries did happen, despitemax_retries=0.Root cause
server.py(lines 33–48): both parameters are accepted and stored, butself._max_retriesandself._retry_delayare never read again anywhere in the codebase.utils.py(line 114): the actual retry count is a hardcoded module-level constant, not controllable via any constructor argument or environment variable (unlikeMAX_CONCURRENT_REQUESTS, which does read from env).base.py(lines 413 and 461): both_retry_with_reconnectand_retry_async_with_reconnectloop overMAX_RETRY_ATTEMPTSinstead ofself._max_retries, and use their own jitter/backoff logic instead ofself._retry_delay.Expected behaviour
max_retries=Nshould cap the number of retry attempts toN.max_retries=0should mean no retries — a single attempt is made and any error is raised immediately to the caller.retry_delay=Nshould control the sleep duration between retry attempts.Suggested fix
In
RunwareBase.__init__, accept and store the parameters (they could be passed down fromRunwareServer), then use them in both retry loops:Alternatively, keep the jitter/backoff logic for the delay but at least wire
self._max_retriesinto the loop bound.Additional notes
retry_delayhas the exact same problem: stored inRunwareServer, never referenced inRunwareBase.MAX_RETRY_ATTEMPTScould remain as the default value when the caller does not passmax_retries, preserving backward compatibility.