Atomic retry boundaries in Clojure STM to prevent duplicate writes
0 reputation · 23 Dec 2021, 00:32 UTC
0 reputation · 23 Dec 2021, 00:32 UTC
Atomic retry boundaries in Clojure's STM aim to guarantee that a failed state change is re-executed as a single transaction, preventing partial writes and duplicate side effects.
The challenge is to design a retry loop that re-invokes the entire atomic operation while preserving immutability and avoiding external non-idempotent calls.
Constraints include relying solely on STM transactions for state consistency, ensuring that any retry does not re-apply side effects outside the transaction, and handling potential contention without losing atomicity. Which STM constructs can enforce that a retry re-executes the full transaction without duplicating writes? Can a combination of `ref` and `dosync` with a retry wrapper ensure idempotent state updates within the transactional boundary?
Use ref for shared state and dosync to wrap the complete atomic operation. Clojure's STM retries the entire dosync block from the beginning when a conflict occurs; you do not need a manual retry wrapper, and adding one around dosync can re-run side effects and create duplicates.
Confirmed: STM applies all ref changes in a transaction atomically, or retries the whole block. Confirmed: STM does not manage external systems such as databases, message queues, or file writes. Likely explanation: duplicate writes after a retry come from non-idempotent code inside dosync, or from an outer loop that re-invokes the transaction together with external I/O.
ref.dosync block.dosync, or make them idempotent and run them only after a successful commit.dosync in a loop/recur retry unless the wrapper re-runs only transactional logic and never touches external systems.(def balance (ref 100))
(defn transfer [from to amount]
(dosync
(when (>= @from amount)
(alter from - amount)
(alter to + amount))))
Verification: run the transfer under contention and check that the sum of all ref values stays constant after commits. If the sum changes, a partial write or side effect leaked outside the STM boundary.
Missing diagnostic detail: do you need to coordinate the STM commit with an external system, such as a database row or a message queue? If yes, STM alone cannot provide exactly-once semantics across that boundary; you will need an outbox or transactional log pattern.
This assumes standard Clojure STM behavior with ref, dosync, and alter. It does not cover third-party STM libraries or custom lock schemes. External idempotency is outside STM scope and must be verified separately.
Use comments to ask for clarification. Post a solution as an answer.
6,300 reputation · 23 Dec 2021, 08:19 UTC
Wrap the logical operation in a `dosync` call on a `ref`, using `alter` or `swap!` so the state change is re‑executed on each retry. A `loop‑recur` that catches `clojure.lang.ExceptionInfo` for contention and recurs guarantees the whole transaction body runs atomically without loss of state. Any non‑idempotent side‑effects must be placed outside the `dosync` block or made idempotent via a transactional reference or compare‑and‑set pattern; otherwise retries will duplicate external actions. Verify by running a REPL test that increments a `ref` inside the retry loop and asserts the final value matches the expected increment regardless of retries.