fix(timezone): make profile-keyed tz cache atomic and add cron persistence regression

Follow-up on the cherry-picked #92489 base: replace the four separate
process-global cache slots with one lock-guarded identity->(name, zone)
mapping so racing profile-scoped threads can never publish a mixed
identity/value pair (the P1 interleaving flagged in the #92489 review),
keep each profile's resolved zone hot across multiplex switches, and pin
the #97905 symptom with a real-store regression test: a foreign-process
tick (desktop multiplex ticker pattern) must persist next_run_at with the
job-owning profile's UTC offset.

Fixes #97905. Refs #88220, #92489.
This commit is contained in:
Teknium
2026-08-31 08:27:54 -07:00
parent 9d8a053ce5
commit 6cc4f3913d
3 changed files with 174 additions and 30 deletions
+60 -5
View File
@@ -21,11 +21,8 @@ import hermes_time
def _reset_hermes_time_cache():
"""Reset the hermes_time module cache (replacement for removed reset_cache)."""
hermes_time._cached_tz = None
hermes_time._cached_tz_name = None
hermes_time._cache_resolved = False
hermes_time._cache_identity = None
"""Reset the hermes_time module cache."""
hermes_time.reset_cache()
# =========================================================================
@@ -106,6 +103,64 @@ class TestGetTimezone:
monkeypatch.setenv("HERMES_HOME", str(second_home))
assert str(hermes_time.get_timezone()) == "America/New_York"
# Switching BACK must return the first profile's zone (per-identity
# entries stay hot; no single-slot ping-pong).
monkeypatch.setenv("HERMES_HOME", str(first_home))
assert str(hermes_time.get_timezone()) == "Asia/Tokyo"
def test_concurrent_profile_resolution_never_mixes_zones(
self, tmp_path, monkeypatch
):
"""Racing profile-scoped threads must never observe a foreign zone.
The multiplex cron ticker lets profile-A work (mark_job_run /
compute_next_run) overlap the ticker advancing to profile B. The
cache publication must be atomic per identity: identity A can never
be paired with profile B's ZoneInfo (#97905 review finding on
PR #92489).
"""
import threading
from hermes_constants import (
reset_hermes_home_override,
set_hermes_home_override,
)
zones = {"a": "Asia/Tokyo", "b": "America/New_York"}
homes = {}
for key, zone in zones.items():
home = tmp_path / key
home.mkdir()
(home / "config.yaml").write_text(
f"timezone: {zone}\n", encoding="utf-8"
)
homes[key] = home
monkeypatch.delenv("HERMES_TIMEZONE", raising=False)
errors = []
barrier = threading.Barrier(2)
def worker(key: str) -> None:
barrier.wait()
for _ in range(200):
token = set_hermes_home_override(str(homes[key]))
try:
tz = hermes_time.get_timezone()
if str(tz) != zones[key]:
errors.append((key, str(tz)))
return
finally:
reset_hermes_home_override(token)
threads = [
threading.Thread(target=worker, args=(key,)) for key in zones
]
for t in threads:
t.start()
for t in threads:
t.join()
assert not errors, f"foreign timezone observed: {errors}"
# =========================================================================