-
Notifications
You must be signed in to change notification settings - Fork 3k
Description
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_eventcalls for the sameapp_nameare serialized by theFOR UPDATElock onapp_states - The
user_stateslock 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-adkversion: 1.26.0- Database: PostgreSQL (with
asyncpg) - Python: 3.12