Skip to content

DatabaseSessionService.append_event: unnecessary FOR UPDATE lock on app_states/user_states when no state delta exists #4655

@rusherman

Description

@rusherman

Description

DatabaseSessionService.append_event() unconditionally acquires SELECT ... FOR UPDATE on both app_states and user_states tables, even when the event carries no state delta for those scopes.

This was partially addressed by #764, which fixed the unnecessary UPDATE (merge) side. However, the SELECT ... FOR UPDATE (row-level lock acquisition) still happens unconditionally.

Problem

In database_session_service.py (lines 553-578 in v1.26.0), _select_required_state is called with use_row_level_locking=use_row_level_locking for both app_states and user_states, regardless of whether the event actually has app: or user: prefixed state deltas.

storage_app_state = await _select_required_state(
    sql_session=sql_session,
    state_model=schema.StorageAppState,
    predicates=(schema.StorageAppState.app_name == session.app_name,),
    use_row_level_locking=use_row_level_locking,  # Always True on PostgreSQL
    ...
)

Since app_states is keyed by app_name alone, all concurrent append_event calls within the same app serialize on this single row lock, even if none of them have app: state deltas. In practice, the vast majority of events only carry session-scoped state, making this lock contention unnecessary.

Impact

For applications with moderate concurrency (e.g., multiple agents within the same app writing events simultaneously), this creates a significant bottleneck:

  • All append_event calls for the same app_name are serialized by the FOR UPDATE lock on app_states
  • The user_states lock similarly serializes all calls for the same (app_name, user_id) pair
  • This happens even when the event has zero state delta (e.g., plain conversation events)

Suggested Fix

Only acquire FOR UPDATE when the event actually has a delta for the corresponding scope:

# Pre-analyze deltas before the transaction
has_app_delta = bool(state_deltas.get("app")) if state_deltas else False
has_user_delta = bool(state_deltas.get("user")) if state_deltas else False

# Then in the transaction:
storage_app_state = await _select_required_state(
    sql_session=sql_session,
    state_model=schema.StorageAppState,
    predicates=(schema.StorageAppState.app_name == session.app_name,),
    use_row_level_locking=use_row_level_locking and has_app_delta,
    ...
)
storage_user_state = await _select_required_state(
    sql_session=sql_session,
    state_model=schema.StorageUserState,
    predicates=(...),
    use_row_level_locking=use_row_level_locking and has_user_delta,
    ...
)

The state delta can be extracted before the DB transaction using extract_state_delta() since it only reads from the event object and has no side effects.

Environment

  • google-adk version: 1.26.0
  • Database: PostgreSQL (with asyncpg)
  • Python: 3.12

Metadata

Metadata

Labels

services[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