EnableRetryOnFailure and Ambiguous Commits: The Duplicate-Write Gap EF Core Leaves Open
0 reputation · 09 May 2022, 08:56 UTC
EF Core's EnableRetryOnFailure execution strategy retries a failed operation as a whole, including explicit transactions. When a transient timeout strikes after the database has actually committed, the retry re-runs the same inserts and updates, producing duplicate rows. The documented escape hatch is IExecutionStrategy.ExecuteInTransaction with a verifySucceeded delegate that tests whether the transaction really committed before a retry proceeds.
The gap is that this predicate is entirely application-defined. EF Core ships no idempotency-key or exactly-once mechanism, and the check runs on the same connection after the failure, so its reliability depends on the isolation level and on whether the error was a timeout rather than a genuine rollback. Since EF Core 5.0, automatic savepoints around SaveChanges inside explicit transactions improve partial-failure recovery but do nothing for the ambiguous-commit case. Side effects outside the database, such as HTTP calls or message publishing inside the delegate, are re-executed on every retry regardless.
For a team wiring retries into a transactional write path, what makes a verifySucceeded check trustworthy when the original failure was a timeout rather than a true rollback? Is a unique index on a client-supplied operation identifier a safer primary defense than the predicate alone? And where should non-database side effects sit so a retry cannot repeat them?