Skip to content

fix: recover from a file_hash race during upload instead of poisoning the session - #445

Merged
Abhash-Chakraborty merged 4 commits into
Abhash-Chakraborty:canaryfrom
payalrvs3:fix/upload-hash-race-session
Aug 11, 2026
Merged

fix: recover from a file_hash race during upload instead of poisoning the session#445
Abhash-Chakraborty merged 4 commits into
Abhash-Chakraborty:canaryfrom
payalrvs3:fix/upload-hash-race-session

Conversation

@payalrvs3

@payalrvs3 payalrvs3 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

_ingest_image() in the upload pipeline dedups by file_hash, which is a unique database column, but the insert wasn't wrapped for that uniqueness constraint. When two requests upload identical file content at nearly the same time, the losing request's commit raises an unhandled IntegrityError. Nothing rolls back the shared per-request session afterward, so every file processed later in that same batch fails too - even ones completely unrelated to the original collision.

Fixes #435

Type of change

  • Bug fix

Release impact

  • Patch (backward-compatible fix)

What changed

  • Wrapped the insert commit in _ingest_image (backend/src/find_api/routers/upload.py) in a try/except for IntegrityError: on a race, roll back and return the existing row as a "duplicate" result - the same outcome as if the existing-file check had simply run a few milliseconds later.
  • Added TestUploadRace in backend/tests/test_upload.py, which pre-seeds the "winning" row and fakes the existing-file check missing it once, forcing a real SQLite unique-constraint violation (not a mocked exception), then confirms the session still works for the next file in the same batch.

Screenshots / recordings (for UI changes)

N/A - backend-only change.

How to test

cd backend
uv run pytest tests/test_upload.py -v
uv run pytest tests/ -v

Manual repro: from two overlapping requests, upload a file with byte-identical content in both at roughly the same time as part of a multi-file batch. Before the fix, every file after the collision in that batch fails too; after the fix, only the colliding file reports "duplicate" and the rest upload normally.

Checklist

  • I linked the related issue
  • I ran required checks from CONTRIBUTING.md
  • I updated docs/env notes if needed (none needed)
  • My PR is scoped to a single issue
  • I followed commit message conventions
  • I am not committing secrets or local artifacts
  • This PR targets canary unless it is the maintainer promotion PR

GSSoC'26 checklist

  • I requested issue assignment before starting
  • I have meaningful commits (no spam commits)
  • I am ready to explain my implementation in review comments

Summary by CodeRabbit

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of simultaneous uploads of the same file.
    • Duplicate uploads now return the existing media record instead of failing.
    • Upload processing remains available for subsequent, distinct files after a duplicate upload attempt.
    • Unrelated upload errors continue to be reported normally.
  • Tests

    • Added coverage for concurrent duplicate uploads and continued upload functionality.

… the session

Signed-off-by: Payalrvs0310@gmail.com <Payalrvs0310@gmail.com>
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Abhash-Chakraborty, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 17 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ddf1d1ac-61ad-4588-b29d-9095a2480876

📥 Commits

Reviewing files that changed from the base of the PR and between a3e138d and 993091d.

📒 Files selected for processing (2)
  • backend/src/find_api/routers/upload.py
  • backend/tests/test_upload.py
📝 Walkthrough

Walkthrough

The upload endpoint now handles duplicate file-hash insert conflicts by rolling back, retrieving the existing media record, and returning a duplicate result. Tests simulate the race and verify that later uploads continue using the same database session.

Changes

Upload race recovery

Layer / File(s) Summary
Integrity conflict handling
backend/src/find_api/routers/upload.py
The media creation commit handles duplicate file-hash conflicts, rolls back the transaction, returns the existing media record, and re-raises unrelated integrity errors.
Race regression validation
backend/tests/test_upload.py
The tests simulate a duplicate insertion, verify the existing media ID, and confirm that a later unrelated upload succeeds. The image helper accepts a configurable color.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: abhash-chakraborty

Poem

A rabbit guards the upload gate,
Rolls back the race before it’s late.
The duplicate finds its proper home,
While fresh files safely hop and roam.
One session stays ready—
Ears up, paws steady!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the upload race recovery and session-poisoning fix.
Description check ✅ Passed The description includes the required summary, issue link, change type, release impact, changes, testing steps, and checklist.
Linked Issues check ✅ Passed The implementation addresses issue #435 by handling the race, rolling back the session, returning a duplicate, and preserving later uploads.
Out of Scope Changes check ✅ Passed The production and test changes are limited to the linked upload race fix and its regression coverage.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@payalrvs3
payalrvs3 marked this pull request as ready for review August 10, 2026 07:10
@github-actions

Copy link
Copy Markdown

PR Context Summary

Suggested issue links

  • No strong issue match found yet.

Use Fixes #123 or Closes #123 in the PR body when one of the suggestions is the intended issue.
Manual rerun: Actions > PR Context Triage > Run workflow > set pr_number and force_review=true.

@macroscopeapp

macroscopeapp Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This bug fix modifies error handling behavior in the upload flow and both changed files are owned by Abhash-Chakraborty, not the PR author. The CODEOWNERS designation indicates the owner should review these changes.

You can customize Macroscope's approvability policy. Learn more.

@payalrvs3 payalrvs3 changed the title fix: recover from a file_hash race during upload instead of poisoning… fix: recover from a file_hash race during upload instead of poisoning the session Aug 10, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 `@backend/src/find_api/routers/upload.py`:
- Around line 298-307: Move the Media insert, IntegrityError rollback,
existing-by-file_hash lookup, and duplicate response data out of the upload
router’s _ingest_image flow into the media repository/storage layer. Expose a
repository method that returns the created or existing Media record and
preserves duplicate handling, then have _ingest_image coordinate the request
using that method without direct conflict-recovery logic.
- Around line 298-307: The IntegrityError handler around db.commit must
distinguish a Media.file_hash uniqueness conflict from other constraint
violations before returning the duplicate response. After rollback, inspect the
database error/constraint metadata and only query Media and return status
"duplicate" for the file_hash conflict; re-raise every other IntegrityError, and
add a regression test covering a different constraint failure while the hash
lookup is absent.
🪄 Autofix

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d111c8a9-a09f-4dc6-9138-19563969eda4

📥 Commits

Reviewing files that changed from the base of the PR and between 83bf03a and 94680a8.

📒 Files selected for processing (2)
  • backend/src/find_api/routers/upload.py
  • backend/tests/test_upload.py

Comment thread backend/src/find_api/routers/upload.py
payalrvs3 and others added 3 commits August 11, 2026 11:40
…plicate

Signed-off-by: Payalrvs0310@gmail.com <Payalrvs0310@gmail.com>
…hash race

The IntegrityError recovery handles the reachable trigger, but the underlying
fault is broader: both upload endpoints loop over several files on one shared
session, and *any* failed commit leaves that session inactive. The next file in
the batch then dies with PendingRollbackError regardless of what went wrong --
an unexpected constraint violation, a dropped connection, a full disk.

Both generic handlers now roll back before recording the failure, so a bad file
costs one result row instead of every row after it. Already-committed files are
untouched; rollback only discards work that never landed.

`/api/upload` needed this as much as `/api/upload/bulk` -- it also accepts a
list of files on one session, which is easy to miss because the name reads
singular.

The new test drives a non-file_hash commit failure through the bulk endpoint
and asserts the *next* file still uploads. Without the rollback it reports
`second.png` as failed.
@Abhash-Chakraborty Abhash-Chakraborty added gssoc26 Related to GirlScript Summer of Code 2026. gssoc:approved Valid GSSoC contribution approved for scoring. type:bug Bug-fix PR. GSSoC type bonus: +20 points. quality:exceptional Exceptional high-quality PR. GSSoC contributor multiplier: 1.5x. backend FastAPI, database, storage, and API work api API contract, endpoint behavior, and response shape bug Something is broken and needs to be fixed. level:intermediate GSSoC difficulty level: intermediate. Base contributor points: 35. labels Aug 11, 2026
@Abhash-Chakraborty

Copy link
Copy Markdown
Owner

Addressing the two CodeRabbit findings on this PR.

1. "Classify the violated constraint before returning duplicate" — already satisfied, no change needed.

The handler does exactly what the finding asks:

except IntegrityError as exc:
    db.rollback()
    if "file_hash" not in str(exc.orig):
        raise
    existing = db.query(Media).filter(Media.file_hash == file_hash).first()
    if existing is None:
        raise

It rolls back first, re-raises anything that is not a file_hash conflict, and re-raises again if no matching row turns up — so the exact scenario described (a different constraint failing while the hash lookup is empty) cannot be misreported as a duplicate. The requested regression test is present too: test_unrelated_integrity_error_is_not_treated_as_duplicate.

I checked the string match holds on both dialects rather than assuming: SQLite emits UNIQUE constraint failed: media.file_hash, and Postgres names the index ix_media_file_hash. Both contain the substring. The existing is None → raise check is the real safety net regardless, since it does not depend on message text.

2. "Move media insert conflict recovery out of routers/upload.py" — valid direction, declining here.

AGENTS.md does say to keep routers thin, so the suggestion is not wrong. But _ingest_image already owns the whole ingest flow in this module and has done since well before this PR; extracting a media repository layer is a refactor of existing structure, not part of the fix. AGENTS.md is equally clear the other way: "Keep the branch focused on one issue. Do not bundle opportunistic refactors." Doing it here would turn a 6-line correctness fix into a structural change riding on a bug-fix branch, which is harder to review and harder to revert.

Worth its own issue. Recording it rather than silently dropping it.

@Abhash-Chakraborty Abhash-Chakraborty left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved, with one commit pushed.

The core fix is right, and the tests are better than most. Seeding the winning row and forcing a real SQLite unique-constraint violation — rather than mocking the exception — is what makes this convincing, and the second test correctly proves an unrelated IntegrityError is re-raised instead of being laundered into a duplicate.

What I added: the issue's expected behaviour is that every other file in that batch uploads normally, and that only held for the file_hash path. The generic except Exception in both endpoints still had no rollback, so any other commit failure — an unexpected constraint, a dropped connection — still killed the rest of the batch with PendingRollbackError. That was the second half of the recommendation on #435.

Both handlers now roll back before recording the failure. /api/upload needed it as much as /api/upload/bulk: it also takes a list of files on one session, which is easy to miss because the name reads singular.

The new test drives a non-file_hash commit failure through the bulk endpoint and asserts the next file still uploads — it reports second.png as failed without the rollback.

Verified locally: ruff check/format --check clean, full backend suite 791 passed, 7 skipped.

CodeRabbit's two threads are addressed in the comment above — one was already satisfied by your code, the other is a real but out-of-scope refactor.

@Abhash-Chakraborty
Abhash-Chakraborty merged commit 3ba0f63 into Abhash-Chakraborty:canary Aug 11, 2026
27 of 29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api API contract, endpoint behavior, and response shape backend FastAPI, database, storage, and API work bug Something is broken and needs to be fixed. gssoc:approved Valid GSSoC contribution approved for scoring. gssoc26 Related to GirlScript Summer of Code 2026. level:intermediate GSSoC difficulty level: intermediate. Base contributor points: 35. quality:exceptional Exceptional high-quality PR. GSSoC contributor multiplier: 1.5x. type:bug Bug-fix PR. GSSoC type bonus: +20 points.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: Unhandled IntegrityError during upload can poison the shared DB session mid-batch

2 participants