Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/google/adk/integrations/redis/_redis_session_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from typing import Optional

from ...errors.already_exists_error import AlreadyExistsError
from ...errors.session_not_found_error import SessionNotFoundError
from ...events.event import Event
from ...platform import uuid as platform_uuid
from ...sessions import _session_util
Expand Down Expand Up @@ -339,6 +340,12 @@ async def get_user_state(
async def append_event(self, session: Session, event: Event) -> Event:
"""Appends an event to the session and synchronizes state in Redis."""
client = self._get_redis()
key = self._session_key(session.app_name, session.user_id, session.id)
# Check before any write: the app/user state deltas below are stored under
# their own keys, and the session write would otherwise create a session
# that was never stored instead of failing like the other services.
if not event.partial and not await client.exists(key):
raise SessionNotFoundError(f"Session {session.id} not found.")
event = await super().append_event(session, event)
# Stamp the session with the event's own timestamp, matching
# InMemorySessionService, SqliteSessionService and DatabaseSessionService.
Expand Down Expand Up @@ -388,8 +395,6 @@ async def append_event(self, session: Session, event: Event) -> Event:
last_update_time=session.last_update_time,
)

key = self._session_key(session.app_name, session.user_id, session.id)

async def append_to_storage(pipe: Any) -> None:
raw = await pipe.get(key)
if raw:
Expand Down
3 changes: 3 additions & 0 deletions tests/unittests/integrations/redis/_fake_redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ async def get(self, key: str) -> str | None:
return None
return self._store.get(key)

async def exists(self, *keys: str) -> int:
return sum(not self._is_expired(key) for key in keys)

async def set(
self,
key: str,
Expand Down
22 changes: 22 additions & 0 deletions tests/unittests/integrations/redis/test_redis_session_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@
from unittest import mock

from google.adk.errors.already_exists_error import AlreadyExistsError
from google.adk.errors.session_not_found_error import SessionNotFoundError
from google.adk.events.event import Event
from google.adk.events.event import EventActions
from google.adk.integrations.redis._config import RedisSessionServiceConfig
from google.adk.integrations.redis._redis_session_service import RedisSessionService
from google.adk.sessions.base_session_service import GetSessionConfig
from google.adk.sessions.session import Session
from google.genai import types
import pytest

Expand Down Expand Up @@ -806,3 +808,23 @@ async def test_cumulative_user_state_creation(session_service):
user_id="u1",
)
assert user_state == {"theme": "dark", "lang": "en"}


@pytest.mark.asyncio
async def test_append_event_to_unknown_session_writes_nothing(
session_service, fake_redis
):
"""A rejected append leaves no session, app state or user state behind."""
session = Session(app_name="app1", user_id="u1", id="never_created")
event = Event(
author="agent",
actions=EventActions(
state_delta={"count": 1, "user:score": 100, "app:status": "active"}
),
)

with pytest.raises(SessionNotFoundError):
await session_service.append_event(session, event)

assert fake_redis._store == {}
assert session.events == []
7 changes: 0 additions & 7 deletions tests/unittests/sessions/_conformance.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,6 @@ async def _make_per_agent_database(
_Backend(
'redis',
_make_redis,
divergences={
'test_append_event_to_unknown_session_raises_session_not_found': (
'Redis writes the session key unconditionally on append, so'
' appending to a session it has never stored creates one'
' instead of raising.'
),
},
),
_Backend('per_agent_database', _make_per_agent_database),
]
Expand Down
Loading