fix(transfers): prevent aborted transaction during concurrent auto-matching#2472
fix(transfers): prevent aborted transaction during concurrent auto-matching#2472cleanjunc wants to merge 2 commits into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthrough
ChangesConcurrent Transfer Matching Fix
Sequence Diagram(s)sequenceDiagram
participant Job as Sync Job (loser)
participant Matcher as auto_match_transfers!
participant Helper as find_or_create_transfer!
participant Outer as Transfer.transaction
participant SP as Savepoint (requires_new: true)
participant PG as PostgreSQL
Job->>Matcher: auto_match_transfers!
Matcher->>Outer: BEGIN
loop each candidate
Matcher->>Helper: find_or_create_transfer!(match)
Helper->>SP: SAVEPOINT sp1
SP->>PG: INSERT INTO transfers ...
PG-->>SP: ERROR unique violation
SP-->>Helper: ROLLBACK TO SAVEPOINT sp1
Helper->>Helper: rescue RecordNotUnique
Helper-->>Matcher: return nil
Matcher->>PG: UPDATE entries SET kind = ...
PG-->>Matcher: OK
end
Matcher->>Outer: COMMIT
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/models/family/auto_transfer_matchable.rb`:
- Around line 78-87: The rescue block in the find_or_create_transfer! method
only catches ActiveRecord::RecordNotUnique, but the validation-path race
condition can also raise ActiveRecord::RecordInvalid when a concurrent job
commits a transfer with matching inflow_transaction_id and
outflow_transaction_id between the validation SELECT and the INSERT. Modify the
rescue clause to catch both ActiveRecord::RecordNotUnique and
ActiveRecord::RecordInvalid exceptions, handling both cases identically by doing
nothing (allowing the savepoint rollback while keeping the transaction intact),
since candidate transfers already satisfy the necessary constraints via the
matching SQL.
In `@test/models/family/auto_transfer_matchable_test.rb`:
- Around line 22-55: The test
concurrentUniqueIndexRaceDoesNotPoisonTheSurroundingTransaction currently only
exercises the RecordNotUnique error path through a real insert collision, but
does not cover the RecordInvalid path that can occur from the same race
condition referenced in the root cause code in
app/models/family/auto_transfer_matchable.rb. Add a companion test case using a
similar monkey-patch approach on Transfer.find_or_create_by! that instead
triggers a validation failure (such as creating a transfer with invalid
attributes) to ensure that the validation-path race condition also does not
poison the surrounding transaction and allows kinds to be written correctly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 87404fb4-93ff-40dd-8e4a-d1a399d81d62
📒 Files selected for processing (2)
app/models/family/auto_transfer_matchable.rbtest/models/family/auto_transfer_matchable_test.rb
Summary
Concurrent syncs of the same family could both reach
Family#auto_match_transfers!and race on the unique index of thetransferstable. On PostgreSQL the losing insert aborted the whole surrounding transaction, and the existingrescue ActiveRecord::RecordNotUniquecould not recover it, so the nextupdate!raisedPG::InFailedSqlTransactionand the remaining candidates were skipped.Root cause
find_or_create_by!ran directly inside the outerTransfer.transaction. A unique violation aborts the active PostgreSQL transaction, and catching the Ruby exception does not reset that state, so every write after the rescue fails.Fix
The insert now runs inside its own savepoint via
Transfer.transaction(requires_new: true), extracted into a smallfind_or_create_transfer!helper. When the race is lost, only that statement rolls back to the savepoint, theRecordNotUniqueis rescued, and the surrounding transaction stays healthy so the loop continues normally.Testing
Added a regression test that reproduces a genuine unique violation during matching (a real failing insert rather than a stubbed raise, since only a real failure aborts the transaction) and asserts the run completes and still writes the transfer kinds.
bin/rails test test/models/family/auto_transfer_matchable_test.rbCloses #2471
Summary by CodeRabbit
Bug Fixes
Tests