From 72694a7fcd7101adb379e23fae786ac0700fc753 Mon Sep 17 00:00:00 2001 From: g97iulio1609 Date: Sat, 28 Feb 2026 13:39:25 +0100 Subject: [PATCH 1/2] fix(sessions): skip unnecessary FOR UPDATE lock on app/user state rows 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. 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 when they only carry session-scoped state (the vast majority of events). Fix: pre-analyze the event's state_delta before acquiring locks and only use FOR UPDATE when the corresponding scope actually has changes. This also avoids a redundant call to extract_state_delta later in the method. Fixes #4655 --- .../adk/sessions/database_session_service.py | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/google/adk/sessions/database_session_service.py b/src/google/adk/sessions/database_session_service.py index 24f525bae0..bff79b40a5 100644 --- a/src/google/adk/sessions/database_session_service.py +++ b/src/google/adk/sessions/database_session_service.py @@ -550,11 +550,25 @@ async def append_event(self, session: Session, event: Event) -> Event: if storage_session is None: raise ValueError(f"Session {session.id} not found.") + # Pre-analyze state deltas to determine which scopes actually need + # write locks. Most events carry only session-scoped state (or no + # state at all), so acquiring FOR UPDATE on app_states / user_states + # unnecessarily serializes all concurrent append_event calls. + has_app_delta = False + has_user_delta = False + state_deltas = None + if event.actions and event.actions.state_delta: + state_deltas = _session_util.extract_state_delta( + event.actions.state_delta + ) + has_app_delta = bool(state_deltas.get("app")) + has_user_delta = bool(state_deltas.get("user")) + 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, + use_row_level_locking=use_row_level_locking and has_app_delta, missing_message=( "App state missing for app_name=" f"{session.app_name!r}. Session state tables should be " @@ -568,7 +582,7 @@ async def append_event(self, session: Session, event: Event) -> Event: schema.StorageUserState.app_name == session.app_name, schema.StorageUserState.user_id == session.user_id, ), - use_row_level_locking=use_row_level_locking, + use_row_level_locking=use_row_level_locking and has_user_delta, missing_message=( "User state missing for app_name=" f"{session.app_name!r}, user_id={session.user_id!r}. " @@ -599,11 +613,8 @@ async def append_event(self, session: Session, event: Event) -> Event: storage_events = [e async for e in result] session.events = [e.to_event() for e in storage_events] - # Extract state delta - if event.actions and event.actions.state_delta: - state_deltas = _session_util.extract_state_delta( - event.actions.state_delta - ) + # Apply state deltas (already extracted above for lock scoping) + if state_deltas is not None: app_state_delta = state_deltas["app"] user_state_delta = state_deltas["user"] session_state_delta = state_deltas["session"] From 2abcbdff86c52910e1df536c228c90c563b43687 Mon Sep 17 00:00:00 2001 From: g97iulio1609 Date: Sat, 28 Feb 2026 13:43:25 +0100 Subject: [PATCH 2/2] refactor: simplify state delta extraction to conditional expression Addresses review feedback: use a single conditional expression instead of separate variable initializations and if block. --- .../adk/sessions/database_session_service.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/google/adk/sessions/database_session_service.py b/src/google/adk/sessions/database_session_service.py index bff79b40a5..a81f1a855a 100644 --- a/src/google/adk/sessions/database_session_service.py +++ b/src/google/adk/sessions/database_session_service.py @@ -554,15 +554,13 @@ async def append_event(self, session: Session, event: Event) -> Event: # write locks. Most events carry only session-scoped state (or no # state at all), so acquiring FOR UPDATE on app_states / user_states # unnecessarily serializes all concurrent append_event calls. - has_app_delta = False - has_user_delta = False - state_deltas = None - if event.actions and event.actions.state_delta: - state_deltas = _session_util.extract_state_delta( - event.actions.state_delta - ) - has_app_delta = bool(state_deltas.get("app")) - has_user_delta = bool(state_deltas.get("user")) + state_deltas = ( + _session_util.extract_state_delta(event.actions.state_delta) + if event.actions and event.actions.state_delta + else None + ) + has_app_delta = bool(state_deltas and state_deltas.get("app")) + has_user_delta = bool(state_deltas and state_deltas.get("user")) storage_app_state = await _select_required_state( sql_session=sql_session,