Skip to content

Commit 9c25afd

Browse files
giulio-leoneCopilot
andcommitted
fix(sessions): use read-only transactions for get/list operations
DatabaseSessionService.get_session() and list_sessions() are pure-read operations but currently open regular read-write transactions via _rollback_on_exception_session(). On Cloud Spanner this causes RetryAborted errors when a concurrent write commits during the read, because Spanner's OCC layer sees the read-write transaction as conflicting. Add _readonly_session() context manager that: - Marks the connection as read-only (postgresql_readonly=True) which also benefits Cloud Spanner's sqlalchemy-spanner dialect - Never commits, avoiding unnecessary write-path overhead - Still rolls back on exception to release the connection cleanly Switch get_session() and list_sessions() to use _readonly_session(). Write operations (create_session, delete_session, append_event) continue to use _rollback_on_exception_session() with explicit commits. Fixes #4771 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f8270c8 commit 9c25afd

File tree

1 file changed

+22
-2
lines changed

1 file changed

+22
-2
lines changed

src/google/adk/sessions/database_session_service.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,26 @@ async def _rollback_on_exception_session(
213213
await sql_session.rollback()
214214
raise
215215

216+
@asynccontextmanager
217+
async def _readonly_session(
218+
self,
219+
) -> AsyncIterator[DatabaseSessionFactory]:
220+
"""Yields a read-only database session for pure SELECT operations.
221+
222+
On Spanner this avoids OCC read-write transactions (which can trigger
223+
RetryAborted when conflicting with concurrent writes). On PostgreSQL
224+
the session is marked read-only. On other dialects the behaviour is
225+
equivalent to a regular session but the transaction is never committed.
226+
"""
227+
async with self.database_session_factory() as sql_session:
228+
conn = await sql_session.connection()
229+
await conn.execution_options(postgresql_readonly=True)
230+
try:
231+
yield sql_session
232+
except BaseException:
233+
await sql_session.rollback()
234+
raise
235+
216236
def _supports_row_level_locking(self) -> bool:
217237
return self.db_engine.dialect.name in (
218238
_MARIADB_DIALECT,
@@ -403,7 +423,7 @@ async def get_session(
403423
# 2. Get all the events based on session id and filtering config
404424
# 3. Convert and return the session
405425
schema = self._get_schema_classes()
406-
async with self._rollback_on_exception_session() as sql_session:
426+
async with self._readonly_session() as sql_session:
407427
storage_session = await sql_session.get(
408428
schema.StorageSession, (app_name, user_id, session_id)
409429
)
@@ -458,7 +478,7 @@ async def list_sessions(
458478
) -> ListSessionsResponse:
459479
await self._prepare_tables()
460480
schema = self._get_schema_classes()
461-
async with self._rollback_on_exception_session() as sql_session:
481+
async with self._readonly_session() as sql_session:
462482
stmt = select(schema.StorageSession).filter(
463483
schema.StorageSession.app_name == app_name
464484
)

0 commit comments

Comments
 (0)