From 8a72aa5b446cf20271cbce715bde75a021682f7e Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Wed, 28 Oct 2026 12:43:59 +0800 Subject: [PATCH] API loader shares one module object per file (issue #132, BUG-1482) _load_local_module always re-executed the file as _jyotish_local_, so engine, dasha, varga and others ran as extra copies with their own caches. It now reuses scripts. / when that file is already imported, else imports it under its plain name so later imports share it; the private copy remains only as a fallback. Call sites unchanged. Probe after one consultation and one chart: loader-made copies 19 -> 0. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 5 +++ docs/BUG_HISTORY.md | 15 +++++++++ scripts/jyotish_api_server.py | 14 ++++++++ tests/test_local_module_reuse_20261028.py | 39 +++++++++++++++++++++++ 4 files changed, 73 insertions(+) create mode 100644 tests/test_local_module_reuse_20261028.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f4607d7b..e2967428 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 印度占星 Skill 更新日志 +## 2026-10-28 — 排盘服务里同一份代码只留一份 + +- 排盘服务内部不再给同一个计算文件另起私有副本(以前一次咨询加一次排盘后有 19 份),各处共用同一份和它的缓存,省内存、少重复计算。结果不变。 +- Skill 版本不变(BUG-1482,issue #132)。 + ## 2026-10-28 — 收起第一批没人用的台账脚本 - 删掉 11 个只被自己测试用到的 closure 类台账脚本和它们的测试;它们以前生成的数据文件保留。功能和结果都不变。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index c5171a1a..94a55312 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -20232,3 +20232,18 @@ - 关联:体检第 16 条(python-hygiene 数量护栏)。 - 复发自:无 - 修复版本:分支 `codex/frontend-dead-files-20261028`(与 #139 同一分支,两个独立提交)。 + +## BUG-1482 | 排盘服务的加载器给同一个 Python 文件另起一份私有副本,缓存互不相通 + +- 状态:resolved(代码 + 回归测试;待部署) +- 首次发现 / 最近更新:2026-10-28 / 2026-10-28(issue #118 代码体检第 20 条 → #132) +- 影响面:`scripts/jyotish_api_server.py` `_load_local_module`(97 处调用点不动)。 +- 现象:一次咨询加一次排盘之后,同一个文件在进程里有多份:引擎、大运、Narayana、分盘等 19 个文件由加载器以 `_jyotish_local_<名>` 另起一份,又被别处以 `scripts.<名>` 或 `<名>` 正常 import 一份(`dasha_analyzer`、`jaimini`、`jyotish_engine`、`narayana_dasha`、`varga`、`domain_calculation_service` 各 3 份)。每份都有自己的模块级缓存,彼此不相通,多占内存、多算一遍。 +- 触发条件:任何一次经由加载器的计算。 +- 根因:加载器总是用 `spec_from_file_location` 以私有名字重新执行文件,不看 `sys.modules` 里是否已经有同一个文件。 +- 修复:加载器先查 `scripts.<名>`、`<名>` 是否已载入同一文件,有就复用;没有就按普通名字 `import`(脚本目录本来就在 `sys.path` 上),之后别处的普通 import 直接拿到同一份;只有普通名字解析到别的文件时才退回旧的私有副本。只改加载器,不动调用点。普通名字还能被子进程按名字找到(私有名字不能),为 #133 多进程打底。 +- 验证:同一段「一次咨询 + 一次排盘」探针:重复文件 32 → 19、多出的副本 38 → 19,加载器造出的私有副本 19 → 0。剩下的 19 份是 `scripts.X` 与 `X` 两种普通 import 并存,与加载器无关,不在本单范围。新测试 `tests/test_local_module_reuse_20261028.py`。凡引用加载器 / 计算服务的 41 个测试文件,改前改后各跑一遍,失败清单一致(都只有 `test_active_rectification_api.py::test_long_real_conversation_reaches_vedastro_after_local_range_is_narrow`,staging 上本来就失败)。`test_api_server_growth_contract.py` 通过。 +- 防复发:上述测试断言加载器不再产生 `_jyotish_local_` 副本、与普通 import 拿到同一个模块对象。 +- 关联:#133(重计算多进程)。 +- 复发自:无 +- 修复版本:分支 `codex/local-module-reuse-20261028`。 diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py index 69035703..558ec742 100644 --- a/scripts/jyotish_api_server.py +++ b/scripts/jyotish_api_server.py @@ -2536,6 +2536,20 @@ def _load_local_module(module_name): return cached relative = module_name.replace('.', os.sep) module_path = os.path.join(SCRIPTS_DIR, f'{relative}.py') + # issue #132: one copy per file. Reuse what `import scripts.X` / `import X` + # already loaded, else import it under its plain name so later imports + # share it; a private `_jyotish_local_X` copy kept its own caches. + for loaded_name in (f'scripts.{module_name}', module_name): + loaded = sys.modules.get(loaded_name) + loaded_file = getattr(loaded, '__file__', None) + if loaded_file and os.path.realpath(loaded_file) == os.path.realpath(module_path): + _LOCAL_MODULE_CACHE[module_name] = loaded + return loaded + if os.path.isfile(module_path) and SCRIPTS_DIR in sys.path: + module = importlib.import_module(module_name) + if os.path.realpath(getattr(module, '__file__', '') or '') == os.path.realpath(module_path): + _LOCAL_MODULE_CACHE[module_name] = module + return module spec = importlib.util.spec_from_file_location(f'_jyotish_local_{module_name}', module_path) if not spec or not spec.loader: raise ImportError(f'Cannot load local module: {module_name}') diff --git a/tests/test_local_module_reuse_20261028.py b/tests/test_local_module_reuse_20261028.py new file mode 100644 index 00000000..f4bf394f --- /dev/null +++ b/tests/test_local_module_reuse_20261028.py @@ -0,0 +1,39 @@ +"""Issue #132: the API server's loader shares one module object per file.""" + +from __future__ import annotations + +import sys + +import scripts.jyotish_api_server as server + + +def test_the_loader_reuses_a_module_that_is_already_imported(): + import scripts.dasha_common as dasha_common + + server._LOCAL_MODULE_CACHE.pop("dasha_common", None) + assert server._load_local_module("dasha_common") is dasha_common + + +def test_a_fresh_load_is_the_plain_module_later_imports_get(): + for name in ("gulika", "scripts.gulika", "_jyotish_local_gulika"): + sys.modules.pop(name, None) + server._LOCAL_MODULE_CACHE.pop("gulika", None) + loaded = server._load_local_module("gulika") + import gulika + + assert loaded is gulika + assert "_jyotish_local_gulika" not in sys.modules + + +def test_no_private_copies_after_a_consultation_and_a_chart(): + service = server.JyotishComputeService() + body = {"year": 1990, "month": 3, "day": 14, "hour": 9, "minute": 30, "lat": 31.23, "lon": 121.47, "tz": 8, + "ayanamsa": "lahiri", "question": "事业如何", "theme": ["career"], "skip_vedastro_main_entry_overview": True, + "defer_optional_external_evidence": True} + for compute in (service._compute_consultation_workflow, service._compute_chart): + try: + compute(dict(body)) + except Exception: + pass # the module graph is what is measured, not the payload + private = sorted(name for name in sys.modules if name.startswith("_jyotish_local_")) + assert private == [], private