Skip to content

Commit 58da6ca

Browse files
committed
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
1 parent 8ddddc0 commit 58da6ca

1 file changed

Lines changed: 18 additions & 7 deletions

File tree

src/google/adk/sessions/database_session_service.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -550,11 +550,25 @@ async def append_event(self, session: Session, event: Event) -> Event:
550550
if storage_session is None:
551551
raise ValueError(f"Session {session.id} not found.")
552552

553+
# Pre-analyze state deltas to determine which scopes actually need
554+
# write locks. Most events carry only session-scoped state (or no
555+
# state at all), so acquiring FOR UPDATE on app_states / user_states
556+
# unnecessarily serializes all concurrent append_event calls.
557+
has_app_delta = False
558+
has_user_delta = False
559+
state_deltas = None
560+
if event.actions and event.actions.state_delta:
561+
state_deltas = _session_util.extract_state_delta(
562+
event.actions.state_delta
563+
)
564+
has_app_delta = bool(state_deltas.get("app"))
565+
has_user_delta = bool(state_deltas.get("user"))
566+
553567
storage_app_state = await _select_required_state(
554568
sql_session=sql_session,
555569
state_model=schema.StorageAppState,
556570
predicates=(schema.StorageAppState.app_name == session.app_name,),
557-
use_row_level_locking=use_row_level_locking,
571+
use_row_level_locking=use_row_level_locking and has_app_delta,
558572
missing_message=(
559573
"App state missing for app_name="
560574
f"{session.app_name!r}. Session state tables should be "
@@ -568,7 +582,7 @@ async def append_event(self, session: Session, event: Event) -> Event:
568582
schema.StorageUserState.app_name == session.app_name,
569583
schema.StorageUserState.user_id == session.user_id,
570584
),
571-
use_row_level_locking=use_row_level_locking,
585+
use_row_level_locking=use_row_level_locking and has_user_delta,
572586
missing_message=(
573587
"User state missing for app_name="
574588
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:
599613
storage_events = [e async for e in result]
600614
session.events = [e.to_event() for e in storage_events]
601615

602-
# Extract state delta
603-
if event.actions and event.actions.state_delta:
604-
state_deltas = _session_util.extract_state_delta(
605-
event.actions.state_delta
606-
)
616+
# Apply state deltas (already extracted above for lock scoping)
617+
if state_deltas is not None:
607618
app_state_delta = state_deltas["app"]
608619
user_state_delta = state_deltas["user"]
609620
session_state_delta = state_deltas["session"]

0 commit comments

Comments
 (0)