Skip to content

retry_interval_seconds is stored but never used, so retries fire with no delay (regression from 0.21.0) #140

Description

@toolshedlabs-hash

Summary

retry_interval_seconds is stored and then never read, so retries fire back to back with no
delay. Every mention of it in the installed 2.0.0 package:

configuration.py:185  retry_interval_seconds (float): The interval in seconds between retries.
configuration.py:217  self.retry_interval_seconds = config_dict.get("retry_interval_seconds", 1.0)

sync/api_call.py::_execute_request recurses straight into the next attempt:

except _SERVER_ERRORS as server_error:
    self.node_manager.set_node_health(node, is_healthy=False)
    return self._execute_request(
        method, endpoint, entity_type, as_json,
        last_exception=server_error, num_retries=num_retries + 1, **kwargs,
    )

async_/api_call.py is the same. 0.21.0 did sleep, at api_call.py:125-127:

logger.debug('Sleeping for {} and retrying...'.format(self.config.retry_interval_seconds))
time.sleep(self.config.retry_interval_seconds)

so the 1.0 rewrite dropped it. 1.0.3, 1.3.0 and 2.0.0 all lack it.

Reproduction

typesense 2.0.0, Python 3.14.0, fresh venv. A loopback HTTP server that answers every request
with 503 and records the arrival time. One node in nodes, num_retries=3,
connection_timeout_seconds=5, client.collections.retrieve().

how the interval was set attempts the server saw gaps between them total
retry_interval_seconds=3 4 0.001, 0.000, 0.000 0.003s
interval_seconds=3 4 0.002, 0.000, 0.000 0.003s
nothing passed (default 1.0) 4 0.000, 0.000, 0.000 0.002s

No delay at all. Same measurement, same script, against 0.21.0:

attempts gaps total
retry_interval_seconds=3 4 3.019, 3.010, 3.004 12.056s
nothing passed (default 1.0) 4 1.004, 1.003, 1.008 4.021s

I ran the old version first because a table of zeros could just as easily mean my harness was
not measuring anything. The old client sleeps.

Practical effect is that a node returning 503 or timing out gets four requests inside three
milliseconds instead of over three seconds, and set_node_health marks it unhealthy on the way
through, so all four land before the node has any chance to recover.

A second one, related, if you want it

interval_seconds is in ConfigDict and in the class docstring, and nothing reads it.
retry_interval_seconds is what __init__ reads and it is not in ConfigDict. So the key that
type checks is ignored and the key that works fails type checking. That is the same shape as #73,
which you fixed for connection_timeout_seconds in March 2025 by adding the key to the TypedDict.

timeout_seconds has a third version of it. It is in ConfigDict marked deprecated, and passing
it logs

Deprecation warning: timeout_seconds is now renamed to connection_timeout_seconds

and then the value is dropped. Against a server that sleeps 8 seconds before responding,
timeout_seconds=30 gives up after 3.07s with a ReadTimeout (the 3.0 default), while
connection_timeout_seconds=30 completes at 8.02s. A message saying an option was renamed reads
as though the old spelling still works.

Suggested fix

For the retry gap, in both sync/api_call.py and async_/api_call.py, before the recursive call:

time.sleep(self.config.retry_interval_seconds)

(await asyncio.sleep(...) in the async one.)

For the config keys, either read both spellings or drop the unread ones from ConfigDict:

self.retry_interval_seconds = config_dict.get(
    "retry_interval_seconds", config_dict.get("interval_seconds", 1.0)
)
self.connection_timeout_seconds = config_dict.get(
    "connection_timeout_seconds", config_dict.get("timeout_seconds", 3.0)
)

Happy to open a PR.

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