Skip to content

fix: SQLite upsert race condition in save_or_update_jargon#205

Merged
EterUltimate merged 1 commit into
NickCharlie:mainfrom
YumemiDream:fix/sqlite-upsert-race-condition
Jun 13, 2026
Merged

fix: SQLite upsert race condition in save_or_update_jargon#205
EterUltimate merged 1 commit into
NickCharlie:mainfrom
YumemiDream:fix/sqlite-upsert-race-condition

Conversation

@YumemiDream

@YumemiDream YumemiDream commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Description:

Wrap the SELECT→INSERT/UPDATE logic in a retry loop so that when
concurrent requests trigger a UNIQUE constraint violation on INSERT,
the retry finds the record the other request just committed and takes
the UPDATE path instead.

Fixes: sqlite3.IntegrityError: UNIQUE constraint failed: jargon.chat_id, jargon.content

Summary

修复 save_or_update_jargon 在 SQLite 模式下的并发竞争问题。当多个请求同时触发同一个 (chat_id, content)
黑话词汇时,原逻辑因 TOCTOU 竞争导致重复 INSERT 并抛出 IntegrityError。通过捕获异常并重试一次,使重试的 SELECT
能命中并发请求刚提交的记录并走 UPDATE 分支。

Changes

  • 在 SQLite 路径的 SELECT→INSERT/UPDATE 逻辑外层增加重试循环(最多 2 次)
  • 捕获 IntegrityError 后自动重试,重试时 SELECT 可命中已提交记录并走 UPDATE
  • 添加并发竞争时的 debug 日志,便于排查

Related Issues

Type of Change

  • Bug fix
  • New feature
  • Refactoring (no functional changes)
  • Documentation update
  • Other:

Checklist

  • Code follows the project's coding style
  • Self-reviewed the code changes
  • No new warnings or errors introduced
  • Tested on SQLite mode
  • Tested on MySQL mode (if applicable)

Summary by Sourcery

Handle concurrent upserts of jargon records in SQLite without raising UNIQUE constraint errors.

Bug Fixes:

  • Wrap the SQLite SELECT→INSERT/UPDATE jargon upsert logic in a bounded retry loop to avoid IntegrityError on concurrent INSERTs.

Enhancements:

  • Add debug logging when SQLite upsert retries due to UNIQUE constraint conflicts to aid diagnosing concurrency issues.

Wrap the SELECT→INSERT/UPDATE logic in a retry loop so that when
concurrent requests trigger a UNIQUE constraint violation on INSERT,
the retry finds the record the other request just committed and takes
the UPDATE path instead.

Fixes: sqlite3.IntegrityError: UNIQUE constraint failed: jargon.chat_id, jargon.content
@sourcery-ai

sourcery-ai Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds a small retry loop around the SQLite-specific SELECT→INSERT/UPDATE path in save_or_update_jargon so that concurrent upserts that hit a UNIQUE constraint on INSERT will retry, re-select the just-committed row, and fall back to the UPDATE path instead of failing with IntegrityError.

Sequence diagram for SQLite retry logic in save_or_update_jargon

sequenceDiagram
    actor Caller
    participant JargonFacade
    participant SQLite

    Caller->>JargonFacade: save_or_update_jargon(chat_id, content, jargon_data)
    loop attempts up to 2
        JargonFacade->>JargonFacade: get_session()
        JargonFacade->>SQLite: SELECT Jargon WHERE chat_id, content
        SQLite-->>JargonFacade: record not found
        JargonFacade->>SQLite: INSERT Jargon using _jargon_insert_values
        alt UNIQUE constraint violated
            SQLite-->>JargonFacade: IntegrityError
            JargonFacade-->>JargonFacade: catch IntegrityError
            JargonFacade-->>JargonFacade: _logger.debug("并发竞争导致 UNIQUE 冲突,重试")
            note over JargonFacade: next loop iteration (retry)
        else insert succeeds
            SQLite-->>JargonFacade: commit
            JargonFacade-->>Caller: return new_record.id
        end
    end
    alt after retry, row exists
        JargonFacade->>JargonFacade: get_session()
        JargonFacade->>SQLite: SELECT Jargon WHERE chat_id, content
        SQLite-->>JargonFacade: existing record
        JargonFacade->>SQLite: UPDATE Jargon fields
        SQLite-->>JargonFacade: commit
        JargonFacade-->>Caller: return record.id
    else final attempt still IntegrityError
        JargonFacade-->>Caller: raise IntegrityError
    end
Loading

File-Level Changes

Change Details Files
Add a bounded retry loop around the SQLite SELECT→INSERT/INSERT logic to handle UNIQUE constraint IntegrityError caused by concurrent upserts.
  • Wrap the existing async session block for the SQLite jargon upsert path in a for-attempt loop with max_attempts=2.
  • Catch IntegrityError around the whole SELECT→INSERT/UPDATE transaction and, on the first failure, log a debug message about a concurrent UNIQUE conflict and retry the operation.
  • Preserve the previous SELECT-then-UPDATE-or-INSERT logic inside the loop so that a successful retry will see the concurrently inserted row and take the UPDATE path, while still re-raising the IntegrityError if all attempts fail.
services/database/facades/jargon_facade.py

Possibly linked issues

  • #unknown: PR changes save_or_update_jargon to handle UNIQUE conflicts via retry, fixing the reported SQLite IntegrityError behavior

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai 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.

Hey - I've left some high level feedback:

  • The retry logic currently treats all IntegrityError instances as concurrent UNIQUE conflicts; consider checking the error code/message and only retrying on UNIQUE violations so that other integrity issues (e.g., NOT NULL, foreign key) are not silently retried and obscured.
  • Since this block is explicitly the SQLite path, it may be simpler and more robust to replace the manual SELECT→INSERT/UPDATE + retry pattern with SQLite’s native UPSERT (ON CONFLICT(chat_id, content) DO UPDATE) via SQLAlchemy’s insert().on_conflict_do_update, which inherently avoids this race.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The retry logic currently treats all IntegrityError instances as concurrent UNIQUE conflicts; consider checking the error code/message and only retrying on UNIQUE violations so that other integrity issues (e.g., NOT NULL, foreign key) are not silently retried and obscured.
- Since this block is explicitly the SQLite path, it may be simpler and more robust to replace the manual SELECT→INSERT/UPDATE + retry pattern with SQLite’s native UPSERT (`ON CONFLICT(chat_id, content) DO UPDATE`) via SQLAlchemy’s `insert().on_conflict_do_update`, which inherently avoids this race.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@EterUltimate EterUltimate left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM. CI is green and the SQLite upsert race-condition fix has been reviewed.

@EterUltimate EterUltimate merged commit f316f59 into NickCharlie:main Jun 13, 2026
4 checks passed
@YumemiDream YumemiDream deleted the fix/sqlite-upsert-race-condition branch June 16, 2026 11:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants