Skip to content

DatabaseSessionService: session row persists in storage without its user_state row ("User state missing" on later append_event) #6823

Description

@hungubqn0310

Describe the bug

Under real production traffic, a small but persistent fraction of sessions end up with a row in the sessions table but no corresponding row in user_states for that (app_name, user_id). create_session() is supposed to create both atomically in one transaction, so this should not be possible — but it is happening continuously in production.

The symptom surfaces later, whenever anything calls append_event() on that session to apply a state update: DatabaseSessionService correctly raises, per _select_required_state():

ValueError: User state missing for app_name='mmvn_b2c_agent', user_id='<id>'. Session state tables should be initialized by create_session.

...but by that point the session has already been silently created without its user_state row, so every future state update for that user fails permanently until the row is manually backfilled.

Environment

  • google-adk version: 2.3.0 (checked latest is 2.7.0; CHANGELOG 2.4.0–2.7.0 reviewed, no entry appears related)
  • Database: PostgreSQL 16 (via asyncpg)
  • Deployment: FastAPI via get_fast_api_app(..., auto_create_session=False), uvicorn, Docker
  • Session creation goes exclusively through ADK's REST endpoints — no app code touches StorageUserState directly.

Evidence from production

SELECT count(DISTINCT s.user_id) FROM sessions s LEFT JOIN user_states us ON us.app_name=s.app_name AND us.user_id=s.user_id WHERE us.user_id IS NULL;
-- 2665 (of 19529 users = 13.6%)

Ongoing since 2025-10-30 through 2026-08-19, ~5-10/day. Reproduces on staging too (7/30 users).

What I've ruled out

  • Collation/index corruption (forced seq scan still 0 rows)
  • DB migration gap (legacy DB also missing these rows)
  • App code deleting rows (full repo grep, nothing)
  • Stale library bug (both envs on 2.3.0, SAVEPOINT logic looks correct on read)

Reproduction attempts (inconclusive)

  1. 8x concurrent POST same session_id: 1 wins (200, consistent), 7 get raw HTTP 500 (see related bug), no orphan ever.
  2. 8x concurrent POST same user, distinct session_ids: all succeed, 1 user_state row, no orphan.
  3. Raw-socket abrupt disconnect (40x): 0 orphans — local request completes faster than ASGI can detect disconnect and cancel.

Leading hypothesis: task-cancellation-during-commit race under real network latency, not reproducible on localhost. Related prior issue: #3328.

Related bug found while reproducing

Same session_id race → loser hits raw IntegrityError → HTTP 500 with leaked SQL, instead of clean AlreadyExistsError/409.

Expected behavior

  • create_session() should never leave an orphaned session without its state row, under any concurrency/disconnect scenario.
  • Same-session_id race should give 409, not 500.

Metadata

Metadata

Labels

request clarification[Status] The maintainer need clarification or more information from the authorservices[Component] This issue is related to runtime services, e.g. sessions, memory, artifacts, etc

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions