fix(timezone): isolate cache by active profile

This commit is contained in:
IvanShang
2026-08-23 03:53:29 +08:00
committed by Teknium
parent ac7cefe0d8
commit 9d8a053ce5
2 changed files with 39 additions and 6 deletions
+21 -6
View File
@@ -27,11 +27,22 @@ except ImportError:
# Python 3.8 fallback (shouldn't be needed — Hermes requires 3.9+)
from backports.zoneinfo import ZoneInfo # type: ignore[no-redef]
# Cached state — resolved once, reused on every call.
# Cached state, keyed to the active timezone source. This process can multiplex
# profiles by switching HERMES_HOME, so a single unkeyed value would leak the
# first profile's timezone into later profile-scoped work.
# Call reset_cache() to force re-resolution (e.g. after config changes).
_cached_tz: Optional[ZoneInfo] = None
_cached_tz_name: Optional[str] = None
_cache_resolved: bool = False
_cache_identity: Optional[tuple[str, str]] = None
def _timezone_cache_identity() -> tuple[str, str]:
"""Return the active source identity for the timezone cache."""
tz_env = os.getenv("HERMES_TIMEZONE", "").strip()
if tz_env:
return ("environment", tz_env)
return ("config", str(get_config_path()))
def _resolve_timezone_name() -> str:
@@ -94,14 +105,17 @@ def _get_zoneinfo(name: str) -> Optional[ZoneInfo]:
def get_timezone() -> Optional[ZoneInfo]:
"""Return the user's configured ZoneInfo, or None (meaning server-local).
"""Return the active profile's configured ZoneInfo, or None (server-local).
Resolved once and cached. Call ``reset_cache()`` after config changes.
The cache is isolated by the active environment override or profile config
path. Call ``reset_cache()`` after editing the active config in place.
"""
global _cached_tz, _cached_tz_name, _cache_resolved
if not _cache_resolved:
global _cached_tz, _cached_tz_name, _cache_resolved, _cache_identity
cache_identity = _timezone_cache_identity()
if not _cache_resolved or _cache_identity != cache_identity:
_cached_tz_name = _resolve_timezone_name()
_cached_tz = _get_zoneinfo(_cached_tz_name)
_cache_identity = cache_identity
_cache_resolved = True
return _cached_tz
@@ -113,10 +127,11 @@ def reset_cache() -> None:
config edit or ``HERMES_TIMEZONE`` update) to force ``get_timezone()`` /
``now()`` to read the new value instead of the value cached at first use.
"""
global _cached_tz, _cached_tz_name, _cache_resolved
global _cached_tz, _cached_tz_name, _cache_resolved, _cache_identity
_cached_tz = None
_cached_tz_name = None
_cache_resolved = False
_cache_identity = None
def now() -> datetime:
+18
View File
@@ -25,6 +25,7 @@ def _reset_hermes_time_cache():
hermes_time._cached_tz = None
hermes_time._cached_tz_name = None
hermes_time._cache_resolved = False
hermes_time._cache_identity = None
# =========================================================================
@@ -86,11 +87,28 @@ class TestGetTimezone:
assert isinstance(tz, ZoneInfo)
assert str(tz) == "Europe/London"
def test_cache_isolated_by_active_profile_config(self, tmp_path, monkeypatch):
"""Switching HERMES_HOME must not reuse another profile's timezone."""
first_home = tmp_path / "first"
second_home = tmp_path / "second"
first_home.mkdir()
second_home.mkdir()
(first_home / "config.yaml").write_text("timezone: Asia/Tokyo\n", encoding="utf-8")
(second_home / "config.yaml").write_text(
"timezone: America/New_York\n", encoding="utf-8"
)
monkeypatch.delenv("HERMES_TIMEZONE", raising=False)
monkeypatch.setenv("HERMES_HOME", str(first_home))
assert str(hermes_time.get_timezone()) == "Asia/Tokyo"
# Multiplexed profile runtime scopes switch HERMES_HOME in one process.
monkeypatch.setenv("HERMES_HOME", str(second_home))
assert str(hermes_time.get_timezone()) == "America/New_York"
# =========================================================================
# execute_code child env — TZ injection
# =========================================================================