fix(sessions): raise AlreadyExistsError on concurrent create_session races - #6843
Open
hungubqn0310 wants to merge 1 commit into
Open
fix(sessions): raise AlreadyExistsError on concurrent create_session races#6843hungubqn0310 wants to merge 1 commit into
hungubqn0310 wants to merge 1 commit into
Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
Author
|
recheck |
…races The has_user_provided_id existence check in DatabaseSessionService.create_session() is not atomic with the insert that follows it: two concurrent callers can both pass the check and then race the same INSERT on (app_name, user_id, session_id). The loser saw a raw, unhandled IntegrityError instead of a clean error. Wrap the insert flush in try/except IntegrityError and raise AlreadyExistsError, mirroring the SAVEPOINT pattern _get_or_create_state already uses for app_state/user_state races. Verified against both sqlite+aiosqlite and postgres+asyncpg with a concurrent create_session() reproduction: 5/5 trials on each backend now raise a clean AlreadyExistsError instead of a raw IntegrityError/UniqueViolationError. This does not address the separate orphaned user_state row issue also reported in google#6823 -- _rollback_on_exception_session rolls back the whole transaction on any exception (including the AlreadyExistsError raised here), so this particular race shouldn't be able to leave a row behind on its own. That deeper issue needs more data to root-cause. Related: google#6823
hungubqn0310
force-pushed
the
fix/session-create-integrity-error
branch
from
August 21, 2026 04:27
c2940a8 to
f6851e8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Link to Issue or Description of Change
Problem:
DatabaseSessionService.create_session()'s up-front existence check(
has_user_provided_id and await sql_session.get(...)) is not atomic withthe
INSERTthat follows it. Two concurrent callers using the samecaller-provided
(app_name, user_id, session_id)can both pass the checkand then race the same insert. The loser gets a raw, unhandled
IntegrityError/UniqueViolationErrorinstead of the documentedAlreadyExistsError.Solution:
Wrap the
sql_session.flush()around the session insert intry/except IntegrityError, raisingAlreadyExistsError— the samepattern
_get_or_create_statealready uses (via a SAVEPOINT) for theanalogous
app_state/user_staterace.This does not address the separate orphaned
user_staterow issue alsoreported in #6823.
_rollback_on_exception_sessionrolls back the wholetransaction on any exception (including the
AlreadyExistsErrorraisedhere), so this particular race shouldn't be able to leave a row behind on
its own — it only replaces a confusing raw
IntegrityErrorwith a clean,documented error. The orphan issue needs more data to root-cause.
Testing Plan
Unit Tests:
Added
test_create_session_concurrent_same_id_raises_already_exists_errorin
tests/unittests/sessions/test_session_service.py: two concurrentcreate_session()calls for the same caller-providedsession_id(afterpre-warming
app_state/user_statewith an unrelated session, to isolatethis race from the separate, already-guarded
_get_or_create_staterace).Asserts exactly one caller succeeds and the other raises
AlreadyExistsError,and that exactly one session row survives.
The 2 failures (
test_import_loading.py::test_entry_point_loads_only_allowlisted_packages[agent|runner])are pre-existing and unrelated (an
httpx2eager-import allowlist check) —reproduced identically on
mainwithout this change.Manual End-to-End (E2E) Tests:
Additionally reproduced and verified the race outside the unit test, against
two real backends, before writing the fix into the codebase:
sqlite3.IntegrityError: UNIQUE constraint failed: sessions.app_name, sessions.user_id, sessions.id.the patch raised
sqlalchemy.dialects.postgresql.asyncpg.IntegrityErrorwrapping
asyncpg.exceptions.UniqueViolationError: duplicate key value violates unique constraint "sessions_pkey"(the 5th trial's slowercoroutine legitimately hit the existing early-exists check before reaching
the insert).
With the patch applied, 5/5 trials on both backends raised a clean
AlreadyExistsErrorinstead, with exactly one session surviving each time.Checklist
Additional context
Pinned versions in the project where this was first observed (from
uv.lock):google-adk==2.3.0,asyncpg==0.31.0,sqlalchemy==2.0.44.