Crystal manual retry around Channel or HTTP write can duplicate side effects
0 reputation · 12 Mar 2023, 04:56 UTC
0 reputation · 12 Mar 2023, 04:56 UTC
Goal: decide whether application-level retry logic can safely re-execute a Crystal write after a transient failure without creating duplicate side effects.
Crystal's standard library provides Fiber and Channel for concurrency, and HTTP::Client for outbound requests, but it does not provide a built-in idempotency key, transactional outbox, or exactly-once retry primitive. A manual begin/rescue/retry loop may re-run the write after a network timeout even when the original operation already succeeded server-side.
Channel behavior adds uncertainty: an unbuffered send blocks until a receiver is available, and sending to a closed channel raises Channel::ClosedError. HTTP::Client does not retry automatically, so any retry policy is application-defined.
Assumes a recent Crystal release; exact fiber scheduler and Channel semantics should be confirmed against the installed compiler version because behavior may differ across releases.
Can a manual retry wrapper detect that a write already completed before re-executing it? What store-side uniqueness or fencing mechanism is required to make retries idempotent? Does the chosen Crystal version's Channel or HTTP::Client implementation expose any retry-safe completion signal?
35275 reputation · 12 Mar 2023, 12:21 UTC
When HTTP::Client raises a timeout or a fiber is interrupted during Channel#send, the client cannot distinguish between a request that never reached the destination and one that succeeded server-side but lost its acknowledgment. Retrying in that state can duplicate side effects such as database inserts, charges, or messages.
HTTP::Client does not retry automatically; every retry is application-defined.Channel send blocks until a receiver is available, and an interrupted send leaves the receipt state indeterminate.Channel::ClosedError does not indicate whether the receiver consumed the message before closure.Move uniqueness to the storage layer so retries are safe:
Idempotency-Key header.def safe_write(payload, idempotency_key)
attempts = 0
begin
response = http_client.post("https://api.example.com/write",
headers: {"Idempotency-Key" => idempotency_key},
body: payload)
return response
rescue HTTP::TimeoutError, IOError => e
attempts += 1
retry if attempts < 3
raise e
end
end
This pattern assumes the server honors the idempotency key. Without server-side deduplication, retrying a non-idempotent write is unsafe regardless of Crystal version.
Version assumption: this describes recent Crystal standard-library behavior. Confirm your installed compiler version because fiber scheduler and Channel semantics can differ across releases.
Diagnostic detail that changes the recommendation: do you control the receiving service's storage layer, or is the endpoint a third-party API that cannot store idempotency keys?
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.