Skip to content

Commit 3f621ae

Browse files
Xuan Yangcopybara-github
authored andcommitted
fix: treat SQLite database update time as UTC for session's last update time
Fixes #1180 We are using `func.now()` to set the `onupdate` time for db, when SQLAlchemy generates the SQL to build the database, it actually translates `func.now()` into `NOW()` or `CURRENT_TIMESTAMP`. The value it returns depends on the database server settings. For example, if the global/default timezone for a db is set to be UTC, the update time will be set to be a UCT time; if the global time zone for a db is set to be a local time zone (e.g. America/Los_Angeles), the update time will be a local time. Normally, the best practice is to set database server to use UTC. Applications will convert it into different time zones as needed. For SQLite, there is no way to config the default timezone, it will just treat it as UTC. But because it is a naive datetime (with no timezone info), python will assume it is a local time and then covert it into a UTC, which is why we see the bug (e.g. we create a session at 2025-06-17 12:49:33 local time, but when we read the session, its last update time is 2025-06-17 19:49:33 local time). The solution is converting the native datatime to be timezone aware before `.timestamp()`. The change in this CL only affects SQLite database. PiperOrigin-RevId: 776654443
1 parent 4e765ae commit 3f621ae

File tree

2 files changed

+36
-11
lines changed

2 files changed

+36
-11
lines changed

src/google/adk/sessions/database_session_service.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import copy
1717
from datetime import datetime
18+
from datetime import timezone
1819
import json
1920
import logging
2021
from typing import Any
@@ -144,6 +145,21 @@ class StorageSession(Base):
144145
def __repr__(self):
145146
return f"<StorageSession(id={self.id}, update_time={self.update_time})>"
146147

148+
@property
149+
def _dialect_name(self) -> Optional[str]:
150+
session = inspect(self).session
151+
return session.bind.dialect.name if session else None
152+
153+
@property
154+
def update_timestamp_tz(self) -> datetime:
155+
"""Returns the time zone aware update timestamp."""
156+
if self._dialect_name == "sqlite":
157+
# SQLite does not support timezone. SQLAlchemy returns a naive datetime
158+
# object without timezone information. We need to convert it to UTC
159+
# manually.
160+
return self.update_time.replace(tzinfo=timezone.utc).timestamp()
161+
return self.update_time.timestamp()
162+
147163

148164
class StorageEvent(Base):
149165
"""Represents an event stored in the database."""
@@ -412,7 +428,7 @@ async def create_session(
412428
user_id=str(storage_session.user_id),
413429
id=str(storage_session.id),
414430
state=merged_state,
415-
last_update_time=storage_session.update_time.timestamp(),
431+
last_update_time=storage_session.update_timestamp_tz,
416432
)
417433
return session
418434

@@ -473,7 +489,7 @@ async def get_session(
473489
user_id=user_id,
474490
id=session_id,
475491
state=merged_state,
476-
last_update_time=storage_session.update_time.timestamp(),
492+
last_update_time=storage_session.update_timestamp_tz,
477493
)
478494
session.events = [e.to_event() for e in reversed(storage_events)]
479495
return session
@@ -496,7 +512,7 @@ async def list_sessions(
496512
user_id=user_id,
497513
id=storage_session.id,
498514
state={},
499-
last_update_time=storage_session.update_time.timestamp(),
515+
last_update_time=storage_session.update_timestamp_tz,
500516
)
501517
sessions.append(session)
502518
return ListSessionsResponse(sessions=sessions)
@@ -529,13 +545,13 @@ async def append_event(self, session: Session, event: Event) -> Event:
529545
StorageSession, (session.app_name, session.user_id, session.id)
530546
)
531547

532-
if storage_session.update_time.timestamp() > session.last_update_time:
548+
if storage_session.update_timestamp_tz > session.last_update_time:
533549
raise ValueError(
534550
"The last_update_time provided in the session object"
535551
f" {datetime.fromtimestamp(session.last_update_time):'%Y-%m-%d %H:%M:%S'} is"
536552
" earlier than the update_time in the storage_session"
537-
f" {storage_session.update_time:'%Y-%m-%d %H:%M:%S'}. Please check"
538-
" if it is a stale session."
553+
f" {datetime.fromtimestamp(storage_session.update_timestamp_tz):'%Y-%m-%d %H:%M:%S'}."
554+
" Please check if it is a stale session."
539555
)
540556

541557
# Fetch states from storage
@@ -577,7 +593,7 @@ async def append_event(self, session: Session, event: Event) -> Event:
577593
session_factory.refresh(storage_session)
578594

579595
# Update timestamp with commit time
580-
session.last_update_time = storage_session.update_time.timestamp()
596+
session.last_update_time = storage_session.update_timestamp_tz
581597

582598
# Also update the in-memory session
583599
await super().append_event(session=session, event=event)

tests/unittests/sessions/test_session_service.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from datetime import datetime
16+
from datetime import timezone
1517
import enum
1618

1719
from google.adk.events import Event
@@ -66,10 +68,17 @@ async def test_create_get_session(service_type):
6668
assert session.id
6769
assert session.state == state
6870
assert (
69-
await session_service.get_session(
70-
app_name=app_name, user_id=user_id, session_id=session.id
71-
)
72-
== session
71+
session.last_update_time
72+
<= datetime.now().astimezone(timezone.utc).timestamp()
73+
)
74+
75+
got_session = await session_service.get_session(
76+
app_name=app_name, user_id=user_id, session_id=session.id
77+
)
78+
assert got_session == session
79+
assert (
80+
got_session.last_update_time
81+
<= datetime.now().astimezone(timezone.utc).timestamp()
7382
)
7483

7584
session_id = session.id

0 commit comments

Comments
 (0)