Skip to content

feat(client): retry transient failures, optionally indefinitely - #1323

Merged
fatih-acar merged 1 commit into
stablefrom
fac/retry-on-error-7mwrc
Sep 8, 2026
Merged

feat(client): retry transient failures, optionally indefinitely#1323
fatih-acar merged 1 commit into
stablefrom
fac/retry-on-error-7mwrc

Conversation

@fatih-acar

@fatih-acar fatih-acar commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Generators and other long-running SDK jobs abort as soon as a single GraphQL call hits a transient infrastructure error, even though the task worker already runs the client with retry_on_failure=True. That switch only covered connection errors inside execute_graphql, gave up after 5 minutes, spun without any delay on HTTP 5xx, and never looked at REST calls or at GraphQL error envelopes. This PR turns retry_on_failure into a real transient-error policy and lets operators opt into retrying indefinitely, so a multi-hour generator survives a database failover or a network outage instead of failing.

Key changes

  • With retry_on_failure enabled, connection errors, timeouts, HTTP 500/502/503/504 responses and GraphQL errors the server flags with one of those statuses are retried. Everything else still fails fast, so a bad query or a schema error surfaces immediately even in unlimited mode.
  • A connection dropped before any response arrives is retried too. httpx reports that as RemoteProtocolError rather than a network error, so it used to escape the retry handler entirely; it is now mapped to ServerNotReachableError like any other lost connection, on all six transport paths of both clients. See the section below for how this was found.
  • Every httpx timeout is retried, not only ReadTimeout. ConnectTimeout is a handshake left hanging by a load balancer dropping packets mid-failover and maps to ServerNotReachableError; WriteTimeout (an upload the server stopped draining) and PoolTimeout map to ServerNotResponsiveError like a read timeout. Previously all three escaped the retry handler as raw httpx exceptions.
  • Retries now cover every request path: GraphQL queries and mutations, the generator's query_gql_query data collection, schema loading and the other REST endpoints. The GraphQL-envelope retry shares one time budget and attempt counter with the transport layer.
  • Exponential backoff with jitter replaces the fixed delay. retry_delay is the base and the new retry_max_delay caps it. This also fixes the 5xx busy loop.
  • max_retry_duration=0 now means retry indefinitely. On the task worker, INFRAHUB_MAX_RETRY_DURATION=0 is enough to opt in because the worker already sets retry_on_failure=True.
  • The new retry_status_codes setting controls which statuses count as transient. 500 is included because Infrahub reports some transient database errors (Neo4j transient or session errors exhausting the server-side retries) without further classification.
  • Every retry is logged with the attempt number and elapsed time, escalating from WARNING to ERROR after five minutes so an indefinitely retrying job stays visible.
  • Once the budget is exhausted the original error is raised instead of a generic "resp hasn't been initialized" error.
  • client.retry_on_failure and client.retry_delay become properties so they can still be toggled at runtime, for example by a generator.

Verified against a real failover

The policy was exercised against a real Infrahub deployment from infrahub-testcontainers (two API replicas behind HAProxy, plus Neo4j, RabbitMQ, Redis and Prefect) rather than against mocks alone. The server's /api/response-delay endpoint makes every GraphQL request sit for 10 seconds, which is a wide enough window to kill a container while a mutation is genuinely in flight.

That found the feature aborting on the exact scenario it exists for. With retry_on_failure=True and max_retry_duration=0, restarting HAProxy during a mutation killed the operation after a single attempt:

MUTATION FAILED after 10.2s: httpx.RemoteProtocolError: Server disconnected without sending a response.

A load balancer that goes away mid-request closes the socket before sending a single byte of the response, and httpx reports that as RemoteProtocolError, a ProtocolError rather than a NetworkError. Only NetworkError and ReadTimeout were mapped to the SDK exceptions the retry handler treats as transient, so a failover bypassed the handler entirely: zero retries, unlimited budget notwithstanding. With the mapping added here, the same failover only delays the mutation:

WARNING infrahub_sdk: Transient failure on .../graphql/main: Unable to connect to 'http://localhost:18000'.
                     Retry 1 in 0.9s (10s elapsed, no time limit)
INFO  httpx: HTTP Request: POST .../graphql/main "HTTP/1.1 200 OK"

A failover reaches the client in one of two shapes, depending on which side of the load balancer goes away. Both are now covered:

Restarted What the client sees Retried by
HAProxy The connection dropped before any response (RemoteProtocolError) The mapping added in this PR
The API servers behind it HTTP 502 for the request in flight, 503 while they boot retry_status_codes, which already worked

The at-least-once caveat below turned out not to be theoretical. The first run used a plain create and the retry came back with Violates uniqueness constraint 'name': the server keeps processing a request whose client has gone away, so attempt one had already written the node before the socket closed. The integration tests use save(allow_upsert=True) for that reason.

Caveats

  • A mutation whose connection was lost may have been applied by the server before the retry, as observed above. Upserts are idempotent. A bare create that already succeeded fails on retry with a non-transient error, which is raised.
  • Because 500 is in the default set, a genuine bug that returns 500 is retried until the budget expires (5 minutes by default). Remove 500 from retry_status_codes to fail fast on it.
  • A per-generator opt-in in .infrahub.yml needs an Infrahub-side change to forward the flag to the client and is left as a follow-up.

Documentation

  • docs/docs/python-sdk/guides/client.mdx: new "Retry transient failures" section under advanced use cases, listing a dropped connection among the transient failures.
  • Regenerated docs/docs/python-sdk/reference/config.mdx and docs/docs/python-sdk/sdk_ref/infrahub_sdk/client.mdx.
  • Changelog fragments changelog/+transient-retry.added.md, changelog/+retry-5xx-busy-loop.fixed.md and changelog/+retry-connection-dropped-mid-request.fixed.md.

Test plan

  • uv run pytest tests/unit/sdk/test_retry.py: 78 tests covering classification, backoff, budget and unlimited mode, the shared budget across layers, the opt-in default, runtime toggling, environment plumbing and async/sync parity. 12 of them are new and drive a lost connection through httpx itself, so the RemoteProtocolError mapping is covered without needing Docker.
  • uv run pytest tests/integration/test_retry_on_failover.py: 4 tests against the deployment described above, 3m30s. Three restart HAProxy mid-mutation (retries disabled, async retry-forever, sync retry-forever) and all three fail without the mapping, so they are a real regression test rather than a demonstration. The fourth restarts the API servers instead and asserts the retry happened on a 502, not merely that the mutation eventually succeeded.
  • Two details that reviewers may wonder about are documented in the test file: the load balancer is published on a pinned host port, because Docker assigns a new one on every container start and the client would otherwise retry against a dead address; and the response delay lives in the memory of each API worker process, so the test that restarts them re-applies it in teardown.
  • uv run invoke lint-code passes. uv run invoke lint-docs passes rumdl; vale was not available locally.
  • Full unit suite passes apart from the known pre-existing tests/unit/ctl failures.

🤖 Generated with Claude Code


Summary by cubic

Turns retry_on_failure in the infrahub_sdk client into a real transient-error policy. Previously it retried only connection errors inside execute_graphql, capped at 5 minutes, and skipped REST calls, GraphQL error envelopes, the no-delay 5xx busy loop, and connections dropped before a response arrived. Now connection errors, dropped connections, every httpx timeout, HTTP 500/502/503/504 responses, and GraphQL envelopes flagged with those statuses are retried on every request path—multipart uploads and streamed downloads included—with exponential backoff and jitter, optionally forever.

Key changes

  • New retry_max_delay caps the backoff; retry_status_codes tunes which statuses count as transient (500 is in the default set because Infrahub reports some transient DB errors without classification). A zero retry_delay or retry_max_delay is rejected, since either made every retry a tight loop.
  • GraphQL-envelope retries share one time budget and attempt counter with transport-level retries, and each retry is logged, escalating from WARNING to ERROR after five minutes.
  • Budget exhaustion raises the original error instead of a generic "resp hasn't been initialized" error.
  • retry_on_failure and retry_delay can now be toggled at runtime.
  • A connection dropped before any response arrives (the shape a failover takes) is now mapped to ServerNotReachableError and retried; ConnectTimeout maps there too and the remaining timeouts (write, pool) to ServerNotResponsiveError. Opt-in integration tests (INFRAHUB_TESTING_FAILOVER=1) restart HAProxy mid-mutation to cover it, with the sync case time-bounded.
  • A multipart mutation answered with a transient GraphQL error envelope retries with the file rewound before each attempt; non-seekable streams (pipes, sockets) are copied to a temp file first (off the event loop in the async client) so a retried send carries the full body. Cancelling an async upload now returns at once, leaving the worker thread to close the temp file.
  • A streamed download interrupted mid-body is reported as ServerNotResponsiveError, restarted on the shared retry budget, and the partial file is removed after a failed attempt. Streamed transient responses are only pre-read when a retry will actually happen, so a spent budget returns the response open instead of fetching its body. A destination that cannot be opened is left untouched, and a failed cleanup never replaces the transfer error.

Caveats

  • A timed-out mutation may have been applied before the retry; upserts are idempotent, but a bare create that succeeded fails on retry with a non-transient error.
  • Remove 500 from retry_status_codes if you want genuine bugs to fail fast instead of retrying until the budget expires.
  • Per-generator opt-in via .infrahub.yml is left as a follow-up.

Written for commit a0a185d. Summary will update on new commits.

Review in cubic

@fatih-acar fatih-acar added the type/feature New feature or request label Sep 4, 2026
@github-actions github-actions Bot added the type/documentation Improvements or additions to documentation label Sep 4, 2026
@codecov

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.57143% with 19 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
infrahub_sdk/file_handler.py 82.75% 8 Missing and 2 partials ⚠️
infrahub_sdk/client.py 94.19% 7 Missing and 2 partials ⚠️
@@            Coverage Diff             @@
##           stable    #1323      +/-   ##
==========================================
+ Coverage   84.24%   84.88%   +0.63%     
==========================================
  Files         147      148       +1     
  Lines       13068    13288     +220     
  Branches     1940     1955      +15     
==========================================
+ Hits        11009    11279     +270     
+ Misses       1494     1441      -53     
- Partials      565      568       +3     
Flag Coverage Δ
integration-tests 38.76% <29.71%> (-0.43%) ⬇️
python-3.10 57.74% <74.85%> (+0.74%) ⬆️
python-3.11 57.74% <74.85%> (+0.74%) ⬆️
python-3.12 57.74% <74.85%> (+0.74%) ⬆️
python-3.13 57.72% <74.85%> (+0.72%) ⬆️
python-3.14 57.74% <74.85%> (+0.74%) ⬆️
python-filler-3.12 23.77% <22.00%> (+0.07%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
infrahub_sdk/config.py 91.56% <100.00%> (+0.10%) ⬆️
infrahub_sdk/retry.py 100.00% <100.00%> (ø)
infrahub_sdk/client.py 84.04% <94.19%> (+4.27%) ⬆️
infrahub_sdk/file_handler.py 87.56% <82.75%> (+3.61%) ⬆️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 4, 2026

Copy link
Copy Markdown

Deploying infrahub-sdk-python with  Cloudflare Pages  Cloudflare Pages

Latest commit: a0a185d
Status: ✅  Deploy successful!
Preview URL: https://b26954fe.infrahub-sdk-python.pages.dev
Branch Preview URL: https://fac-retry-on-error-7mwrc.infrahub-sdk-python.pages.dev

View logs

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 11 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread infrahub_sdk/client.py
Comment thread infrahub_sdk/client.py

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 6 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread tests/integration/test_retry_on_failover.py Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 1 file (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread tests/integration/test_retry_on_failover.py

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 8 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread infrahub_sdk/client.py Outdated
Comment thread infrahub_sdk/client.py Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread tests/integration/test_retry_on_failover.py

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 3 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread infrahub_sdk/client.py

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread infrahub_sdk/client.py Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread infrahub_sdk/client.py Outdated
Comment thread infrahub_sdk/config.py
Comment on lines +66 to +78
retry_delay: int = Field(
default=5,
ge=0,
description=(
"Base delay in seconds before retrying a request that failed with a transient error. "
"The delay doubles after every attempt, with jitter, up to the maximum retry delay."
),
)
retry_max_delay: int = Field(
default=60,
ge=0,
description="Maximum delay in seconds between two retries of a request that failed with a transient error.",
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both of these settings use ge=0, and compute_backoff returns min(max_delay, ...), so either zero makes every retry sleep 0 seconds. Managed to make 20000+ attempts in one second against a persistent 502.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid point, thanks for measuring it. Both settings are now gt=0 in 9e4f8ce; as integers that makes 1 second the floor. max_retry_duration keeps ge=0 since 0 means unlimited there.

Comment thread infrahub_sdk/client.py Outdated
Comment on lines +115 to +119
while not task.done():
try:
await asyncio.wait({task})
except asyncio.CancelledError:
continue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if the source stream never returns from read()? It feels like things could get stuck here even at default config, with retries off.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as before this PR: httpx reads the source synchronously while sending the body, so a read() that never returns hung the upload already, and in the async client the event loop with it. The copy moves that read to a worker thread. It runs regardless of the retry settings because the 429 backoff and the 401 re-login also re-send the body. What did get worse was cancellation, which waited for the worker: in 9e4f8ce a cancel surfaces at once and the worker closes the temporary file itself once its pending read returns.

Comment thread infrahub_sdk/retry.py Outdated
TRANSIENT_EXCEPTIONS = (ServerNotReachableError, ServerNotResponsiveError)
"""Client-side failures (connection error, read timeout) that are always considered transient."""

CONNECTION_LOST_EXCEPTIONS = (httpx.NetworkError, httpx.RemoteProtocolError)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should ConnectTimeout, WriteTimeout and PoolTimeout be in that sequence to be retried too? WriteTimeout is used by the upload path this PR tries to harden.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes to all three, in 9e4f8ce. ConnectTimeout joins the connection-lost set (a handshake left hanging by a load balancer dropping packets), and every catch site now takes httpx.TimeoutException, so write and pool timeouts map to ServerNotResponsiveError like read timeouts. Covered for both clients.

Comment thread infrahub_sdk/client.py Outdated
Comment on lines 1745 to 1746
except CONNECTION_LOST_EXCEPTIONS as exc:
raise ServerNotReachableError(address=self.address) from exc

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the right exception for a connection lost mid-download?

The catch around the yield now includes RemoteProtocolError, so a transfer truncated while the caller reads the body surfaces as "Unable to connect to ..." with no retry, and file_handler leaves a partial file with no hint, even though the server was reachable in the first place.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it was not. In 9e4f8ce a body breaking off raises ServerNotResponsiveError saying the connection was lost while reading the response, the file handler restarts the download on the shared retry budget when retries are on, and the partial file is removed either way.

Comment thread changelog/+transient-retry.added.md Outdated
@@ -0,0 +1 @@
With `retry_on_failure` enabled, the client now retries every transient failure instead of only connection errors: a connection dropped before any response arrives (the shape a load balancer failover or a server restart takes on an in-flight request), read timeouts, HTTP `500`/`502`/`503`/`504` responses and GraphQL errors the server flags with one of those statuses (for example a database that became unavailable mid-run). Retries apply to every request path, including multipart uploads, streamed downloads and REST endpoints such as `query_gql_query` used by generators, and use exponential backoff with jitter (`retry_delay` as the base, capped by the new `retry_max_delay`) instead of retrying HTTP 5xx responses in a tight loop without any delay. The new `retry_status_codes` setting tunes which statuses count as transient, and `max_retry_duration=0` retries indefinitely so long-running generators can survive an outage rather than abort. Retries are logged with the attempt number and elapsed time, escalating from `WARNING` to `ERROR` after five minutes, and once the budget is exhausted the original error is raised instead of a generic one.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can probably be shorten without losing its meaning.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shortened to two sentences in 9e4f8ce.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 7 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread infrahub_sdk/retry.py Outdated
@fatih-acar
fatih-acar marked this pull request as ready for review September 7, 2026 09:18
@fatih-acar
fatih-acar requested a review from a team as a code owner September 7, 2026 09:18
Comment on lines +313 to +321
try:
async with await anyio.Path(dest).open("wb") as f:
async for chunk in resp.aiter_bytes(chunk_size=65536):
await f.write(chunk)
bytes_written += len(chunk)
return bytes_written
except ServerNotReachableError:
self._client.log.error(f"Unable to connect to {self._client.address}")
raise
except BaseException:
await anyio.Path(dest).unlink(missing_ok=True)
raise
return bytes_written

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A failed open("wb") now also triggers the unlink, so a pre-existing dest gets deleted by an attempt that never wrote to it (to reproduce: dest with mode 0444, open raises PermissionError, the file is gone, on stable it survived). The file could be opened before the try, and the unlink wrapped in suppress(OSError) so a failed cleanup does not replace the real error. Same in the sync version of this code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, the open should not have been inside the try. In 980932f the file is opened first so a dest that cannot be opened is left untouched, and the unlink runs after the file is closed, inside suppress(OSError), so the transfer error stays the one raised. Same in the sync handler, with tests on both clients for a pre-existing dest that cannot be opened and for a cleanup that fails.

while True:
try:
return await self._download_to_file(url=url, dest=dest, retry_state=retry_state)
except (ServerNotReachableError, ServerNotResponsiveError) as exc:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tuple of error classes could be replaced by TRANSIENT_EXCEPTIONS

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was TRANSIENT_EXCEPTIONS at first, but ruff's DOC501 cannot resolve an imported tuple against the documented Raises entries and flags the bare raise, whereas the same pattern passes in retry.py because the tuple is local there. Listing the two classes keeps the except clause aligned with the docstring without a noqa.

retry_on_failure only covered connection errors inside execute_graphql,
gave up after max_retry_duration, spun without delay on HTTP 5xx, and
never looked at REST calls or at GraphQL error envelopes, so a generator
running for hours aborted on the first transient infrastructure error.

A TransientRetryHandler now drives every request path of both clients:
GraphQL queries and mutations, REST endpoints such as query_gql_query and
schema loading, multipart uploads and streamed downloads. It retries
connection errors, connections dropped before a response arrives (the
shape a load balancer failover takes on an in-flight request), every
httpx timeout, HTTP 500/502/503/504 responses and GraphQL envelopes whose
errors all carry one of those statuses, sharing one time budget and
attempt counter across the transport and envelope layers. Anything else
still fails fast.

Retries use exponential backoff with jitter from retry_delay up to the
new retry_max_delay; a zero for either is rejected since it would turn
every retry into a tight loop. retry_status_codes tunes which statuses
count as transient, and max_retry_duration=0 retries indefinitely. Each
retry is logged with its attempt number and elapsed time, escalating from
WARNING to ERROR after five minutes, and once the budget is spent the
original error is raised. retry_on_failure and retry_delay are properties
so they can be toggled at runtime.

Uploads from a stream that cannot be rewound are copied once to a
temporary file, off the event loop in the async client, so a retried send
carries the full body. A streamed download whose connection drops mid-body
raises ServerNotResponsiveError, is restarted on the shared budget by the
file handler, and never leaves a partial file behind.

Opt-in integration tests (INFRAHUB_TESTING_FAILOVER=1) restart HAProxy and
the API servers mid-mutation against a real deployment.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@fatih-acar
fatih-acar force-pushed the fac/retry-on-error-7mwrc branch from 980932f to a0a185d Compare September 8, 2026 14:23
@fatih-acar
fatih-acar merged commit ff7a597 into stable Sep 8, 2026
21 checks passed
@fatih-acar
fatih-acar deleted the fac/retry-on-error-7mwrc branch September 8, 2026 15:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type/documentation Improvements or additions to documentation type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants