Skip to content

max_retries and retry_delay constructor parameters are stored but never used — retry count is hardcoded to 10 #320

Description

@JoffreyPlouvier

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions