diff --git a/CHANGELOG.md b/CHANGELOG.md index ca6775c6..d787a4b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ 1. [#714](https://github.com/influxdata/influxdb-client-python/pull/714): Implement `WriteApi.flush()` to force pending batch writes without disposing the API (mirrors C#/Java batch writer). 1. [#715](https://github.com/influxdata/influxdb-client-python/pull/715): Accept numeric zero for `RangeThreshold.min`/`max` and greater/lesser `value` (identity checks vs `None`, not truthiness); cover create-check serialization. 1. [#720](https://github.com/influxdata/influxdb-client-python/pull/720): Prevent the `api_client` class from raising exceptions while the Python interpreter clears the client state during shutdown. +1. [#725](https://github.com/influxdata/influxdb-client-python/pull/725): Start the `WritesRetry` `max_retry_time` at a request's first failure, so a strategy passed as `retries` to `InfluxDBClient` keeps retrying after `max_retry_time` has passed since it was created. ## 1.50.0 [2026-01-23] diff --git a/influxdb_client/client/write/retry.py b/influxdb_client/client/write/retry.py index 2c4b359e..169d2b3c 100644 --- a/influxdb_client/client/write/retry.py +++ b/influxdb_client/client/write/retry.py @@ -51,7 +51,10 @@ def __init__(self, jitter_interval=0, max_retry_delay=125, exponential_base=2, m self.max_retry_delay = max_retry_delay self.max_retry_time = max_retry_time self.exponential_base = exponential_base - self.retry_timeout = datetime.now() + timedelta(seconds=max_retry_time) + # Deadline for retrying the current request. It is set when a request + # first fails, not here: one instance can serve many requests, e.g. when + # passed as ``retries`` to ``InfluxDBClient``. + self.retry_timeout = None self.retry_callback = retry_callback def new(self, **kw): @@ -118,10 +121,15 @@ def get_retry_after(self, response): def increment(self, method=None, url=None, response=None, error=None, _pool=None, _stacktrace=None): """Return a new Retry object with incremented retry counters.""" - if self.retry_timeout < datetime.now(): + retry_timeout = self.retry_timeout + if retry_timeout is None: + # First failure of a request: its retry time starts now. + retry_timeout = datetime.now() + timedelta(seconds=self.max_retry_time) + if retry_timeout <= datetime.now(): raise MaxRetryError(_pool, url, error or ResponseError("max_retry_time exceeded")) new_retry = super().increment(method, url, response, error, _pool, _stacktrace) + new_retry.retry_timeout = retry_timeout if response is not None: parsed_error = InfluxDBError(response=response) diff --git a/tests/test_WritesRetry.py b/tests/test_WritesRetry.py index 8c61d92c..bcfb8d34 100644 --- a/tests/test_WritesRetry.py +++ b/tests/test_WritesRetry.py @@ -55,6 +55,33 @@ def test_backoff_max_time(self): self.assertEqual("max_retry_time exceeded", exception.reason.args[0]) + def test_max_retry_time_starts_at_first_failure(self): + # A strategy passed as ``retries`` to InfluxDBClient lives as long as the client. + retry = NonRandomMinWritesRetry(max_retry_time=1) + time.sleep(1.5) + + # The first failure of a request made after max_retry_time is still retried. + retry = retry.increment() + self.assertEqual(retry.is_exhausted(), False) + self.assertEqual(retry.total, 4) + + def test_max_retry_time_is_per_request(self): + base = NonRandomMinWritesRetry(max_retry_time=1) + + first_request = base.increment() + time.sleep(1.5) + with self.assertRaises(MaxRetryError) as cm: + first_request.increment() + self.assertEqual("max_retry_time exceeded", cm.exception.reason.args[0]) + + # Another request made with the same strategy gets its own max_retry_time. + second_request = base.increment() + self.assertEqual(second_request.total, 4) + + def test_max_retry_time_zero_disables_retry(self): + with self.assertRaises(MaxRetryError): + WritesRetry(max_retry_time=0).increment() + def test_backoff_start_range(self): retry = NonRandomMinWritesRetry(total=5, retry_interval=1, exponential_base=2, max_retry_delay=550)