From 4b1e803f16f8967d380cfaf57226a66ac6e3758d Mon Sep 17 00:00:00 2001 From: g97iulio1609 Date: Sat, 28 Feb 2026 13:39:25 +0100 Subject: [PATCH 1/3] 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 | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/google/adk/sessions/database_session_service.py b/src/google/adk/sessions/database_session_service.py index eed1d9eae6..866fdb34c8 100644 --- a/src/google/adk/sessions/database_session_service.py +++ b/src/google/adk/sessions/database_session_service.py @@ -564,6 +564,20 @@ 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, @@ -622,7 +636,7 @@ async def append_event(self, session: Session, event: Event) -> Event: storage_user_state.state = ( storage_user_state.state | state_deltas["user"] ) - if state_deltas["session"]: + if state_deltas and state_deltas["session"]: storage_session.state = ( storage_session.state | state_deltas["session"] ) From 4f2febacb3c926735d47fc06593d3a68d7c8f30d Mon Sep 17 00:00:00 2001 From: g97iulio1609 Date: Sat, 28 Feb 2026 13:43:25 +0100 Subject: [PATCH 2/3] 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 866fdb34c8..7c141fc6d7 100644 --- a/src/google/adk/sessions/database_session_service.py +++ b/src/google/adk/sessions/database_session_service.py @@ -568,15 +568,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, From 8db8a51aaf269beb22463406bbbde1d9407f038c Mon Sep 17 00:00:00 2001 From: giulio-leone <6887247+giulio-leone@users.noreply.github.com> Date: Fri, 6 Mar 2026 06:49:14 +0100 Subject: [PATCH 3/3] fix: address Gemini review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/google/adk/sessions/database_session_service.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/google/adk/sessions/database_session_service.py b/src/google/adk/sessions/database_session_service.py index 7c141fc6d7..c8e044001a 100644 --- a/src/google/adk/sessions/database_session_service.py +++ b/src/google/adk/sessions/database_session_service.py @@ -536,11 +536,7 @@ async def append_event(self, session: Session, event: Event) -> Event: is_sqlite = self.db_engine.dialect.name == _SQLITE_DIALECT use_row_level_locking = self._supports_row_level_locking() - state_delta = ( - event.actions.state_delta - if event.actions and event.actions.state_delta - else {} - ) + state_delta = event.actions.state_delta if event.actions.state_delta else {} state_deltas = _session_util.extract_state_delta(state_delta) has_app_delta = bool(state_deltas["app"]) has_user_delta = bool(state_deltas["user"]) @@ -570,7 +566,7 @@ async def append_event(self, session: Session, event: Event) -> Event: # unnecessarily serializes all concurrent append_event calls. state_deltas = ( _session_util.extract_state_delta(event.actions.state_delta) - if event.actions and event.actions.state_delta + if event.actions.state_delta else None ) has_app_delta = bool(state_deltas and state_deltas.get("app"))