Reflex @retry decorator and non-idempotent write operations
0 reputation · 07 Oct 2020, 15:24 UTC
0 reputation · 07 Oct 2020, 15:24 UTC
The @retry decorator in reflex.utils.retry allows event handlers and background tasks to automatically re-execute upon encountering an exception. While this ensures resilience against transient failures, the decorator operates independently of the function's side effects.
When applying this decorator to operations that perform database writes or external API calls, there is a risk of duplicating records if the failure occurs after the write is committed but before the function returns successfully. The current implementation does not provide a built-in mechanism to track the state of completed writes across retry attempts.
For applications using the stable release of Reflex, it is unclear how to best integrate idempotency guards within the @retry workflow without manually wrapping every single write operation in a conditional check.
@retry decorator support any internal state tracking to prevent duplicate execution of specific code blocks?35275 reputation · 08 Oct 2020, 00:02 UTC
No, the @retry decorator does not track internal state to prevent duplicate execution of specific code blocks. It re-invokes the entire wrapped function from the beginning on each exception. If a database write commits but a later statement (or the return path) raises, the retry runs the write again — producing duplicate rows or repeated external API calls.
The recommended pattern for write safety under exponential backoff is an idempotency key enforced by a unique constraint at the storage layer. Generate one stable key per logical operation (e.g., per form submission), persist it alongside the write, and let the database reject duplicates via INSERT ... ON CONFLICT or an upsert. Backoff timing does not affect this risk.
A generic retry wrapper has no visibility into which statements inside the function succeeded. It only sees an exception and restarts the call. This is by design: the decorator is meant for transient failures in the call (network blip, deadlock, temporary unavailability), not for compensating side effects that already occurred.
idempotency_key to the target table.await session.execute(
insert(MyModel).values(
idempotency_key=key,
payload=data
).on_conflict_do_nothing(index_elements=['idempotency_key'])
)
await session.commit()
Move the write out of the retried function. Let the retried section only perform the failure-prone call (e.g., a flaky HTTP request). Persist a status flag (pending/done) before attempting the write, then complete the write only if the flag is still pending. This still requires a unique constraint or atomic compare-and-swap to avoid races.
python -c "import reflex.utils.retry; import inspect; print(inspect.getsource(reflex.utils.retry.retry))"
reflex.utils.retry and decorator signature are version-sensitive; the above reflects typical retry-decorator behavior but was not confirmed against a specific Reflex release.Which Reflex version are you running, and does your handler execute inside a request-scoped transaction or a background task? The answer changes whether the idempotency key must survive process restarts.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.