transaction/1 retry loop vs idempotent retractall/assertz for non-duplicating writes
0 reputation · 30 Sept 2023, 14:37 UTC
0 reputation · 30 Sept 2023, 14:37 UTC
A design is needed to retry a write goal that can fail transiently while guaranteeing the dynamic fact is present exactly once and external side effects are not duplicated.
The constraint is SWI-Prolog database semantics. transaction/1 runs a goal atomically and rolls back assert/retract changes on failure, so a retry loop around transaction/1 cannot leave partial duplicate facts. transaction/1 does not undo external side effects such as file I/O, so those effects may still run more than once on re-execution.
A manual retry loop with repeat and plain assertz/1 duplicates a clause on each successful attempt unless the write is made idempotent, for example with retractall/1 before assertz/1. Idempotent retractall/assertz changes clause ordering, which matters for first-match code.
With these documented behaviors, the unresolved decision is whether retry should re-execute the whole transaction for simplicity and safe DB state, or only the failing external effect to avoid repeating I/O but requiring splitting the goal and tracking completion markers.
Does transaction/1 re-execute the entire enclosed goal on a concurrent update, and can that cause non-idempotent external effects to run multiple times? Should the retry loop be placed outside transaction/1 to avoid re-running external effects, or inside to preserve atomic rollback? How can completion be tracked without relying on clause order changes from retractall/assertz?
Yes. transaction/1 re-executes the entire enclosed goal when the transaction fails and you retry. Database changes are rolled back, but external side effects such as file writes or network calls are not. A retry loop around transaction/1 will therefore repeat non-idempotent I/O.
The retry loop belongs outside transaction/1, but the external effect should not be inside the retried transaction. Split the goal: commit the database intent first, then perform the irreversible side effect once.
transaction/1 is the atomic unit for the dynamic database. If a concurrent update causes failure, the database is restored, but the goal is re-run from the top on the next attempt. Putting a retry loop inside transaction/1 is not coherent because the transaction already defines the failure and rollback boundary.
transaction/1, assert or update a keyed marker such as pending_effect(Id).This keeps the retried block limited to database work. The external step runs only after a successful commit.
retractall/1 followed by assertz/1 guarantees a single clause but moves it to the end of the predicate, which can break first-match logic. Use membership-based tracking instead:
done(Id) tested with done(Id).library(assoc) value stored in a single dynamic fact and replaced atomically.Both make presence independent of clause order.
Before adding a retry loop, confirm that the transient failure is a concurrency conflict rather than a deterministic error. If the same goal always fails for the same reason, retrying will not help and the design should fix the underlying conflict instead.
Because transaction internals can shift across SWI-Prolog versions, verify with a minimal test: a transaction that asserts a fact and appends to a temporary file, forced to fail once and succeed on retry. Confirm the file receives two appends while the database holds at most one copy of the fact. If your application is multi-threaded, also verify that two concurrent transactions cannot both pass a \+ done(Id) guard before either commits; if they can, serialize the write with a mutex or a single-writer queue.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.