fix(doeff-vm): EffectBase.__reduce_ex__ の copyreg 解決を PyOnceLock で 1 回きりにする - #586
Open
proboscis wants to merge 2 commits into
Open
fix(doeff-vm): EffectBase.__reduce_ex__ の copyreg 解決を PyOnceLock で 1 回きりにする#586proboscis wants to merge 2 commits into
proboscis wants to merge 2 commits into
Conversation
free-threaded CPython 3.14 では import と module 属性参照が per-object 鍵を
取るため、`__reduce_ex__` が呼び出しのたびに copyreg を import していると
常駐 runtime で import 鍵が競合点になる。2026-08-07 の実測(ACP hypha 常駐
runtime): 948 threads が import 鍵で滞留し、機体全体で +19.5 GiB の swap
押し出し。native stack は _PyMutex_LockTimed→_PyParkingLot_Park→
__psynch_cvwait、到達元は PyImport_ImportModuleLevelObject→
import_ensure_initialized と _Py_module_getattro_impl、呼び手は
doeff_vm.cpython-314t-darwin.so。
TDD 第 1 相(実装前・意図的に red):
- tests/test_effect_base_reduce_ex_hot_path.py
- 実行時の証明: warm-up 後に `__reduce_ex__` を 200 回叩き、
builtins.__import__ の呼び出しが 0 であること
(実装前 = copyreg が 200 回 → red)
- ソース上の証明: `__reduce_ex__` 本文に py.import が無く、
静的 PyOnceLock (COPYREG_NEWOBJ) 経由で解決していること
- pickle 往復の既存挙動(reduce タプルの形・全 protocol・cloudpickle・
入れ子・多スレッド同時実行)は不変であること
- .semgrep.yaml: doeff-vm-no-per-call-copyreg-resolution を新設
(py.import("copyreg") / getattr("__newobj__") を doeff-vm 系 src で禁止)
- tests/semgrep/fixtures/rust/…/python_generator_stream.rs: 改修前の形を
bad fixture として常設し、rule 発火と shipped source での非発火を assert
- docs/adr/enforcement-ledger.json: semgrep_rules 247→248
(ADR-DOE-ENFORCE-001 R5 の記帳)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
EffectBase.__reduce_ex__ の hot path から import と module 属性参照を外す。 静的 `COPYREG_NEWOBJ: PyOnceLock<Py<PyAny>>` に解決済み callable を保持し、 2 回目以降は refcount 加算だけで済ませる(free-threaded 3.14 の import 鍵・ module getattr 鍵に触れない)。返す reduce タプルの形は不変 — (copyreg.__newobj__, (cls,), __dict__)。 前相の red がすべて green: - __reduce_ex__ 200 回で builtins.__import__ 呼び出し 0(改修前 = 200) - pickle 往復(全 protocol・cloudpickle・入れ子)は不変 - 8 スレッド × 200 回同時実行でも解決結果は copyreg.__newobj__ で一致 - doeff-vm/doeff-vm-core の Rust src から py.import は 0 箇所になった ADR-DOE-ENFORCE-001 の TDD+semgrep 手順に従い、旧形は doeff-vm-no-per-call-copyreg-resolution が恒久ガードする。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
何を直したか
EffectBase.__reduce_ex__(packages/doeff-vm/src/python_generator_stream.rs)が呼び出しのたびに
py.import("copyreg")→getattr("__newobj__")していたのを、静的
COPYREG_NEWOBJ: PyOnceLock<Py<PyAny>>によるプロセス 1 回きりの解決へ変更。hot path から import と module 属性参照を外し、2 回目以降は refcount 加算だけにする。
返す reduce タプルの形は不変 —
(copyreg.__newobj__, (cls,), __dict__)。なぜ(実弾の証拠)
free-threaded CPython 3.14 では import も module 属性参照も per-object 鍵を取る。
2026-08-07・ACP の hypha 常駐 runtime で 948 threads が import 鍵で滞留し、
機体全体で +19.5 GiB の swap 押し出しが発生した。滞留スレッドの native stack は
_PyMutex_LockTimed → _PyParkingLot_Park → __psynch_cvwait、到達元はPyImport_ImportModuleLevelObject → import_ensure_initializedと_Py_module_getattro_impl、呼び手はdoeff_vm.cpython-314t-darwin.so。証跡 =
/tmp/hypha-wedge-sample-20260807.txt(11.9MB)。ACP 側には既に応答スレッドの有界化(worker pool 16 + queue 64 + 飽和 503)と起動時
import warmup +
sys.meta_path番兵が入っている(agent-control-plane PR #223)が、あれは応答経路の対症であって、鍵を生む側は上流に残っていた。doeff を使う別の
常駐 runtime・並列 worker は同じ鍵を踏みうるため、こちらで根を断つ。
受入条件と shipped test の 1:1 対応
__reduce_ex__の hot path に import が無いことを code で示すtests/test_effect_base_reduce_ex_hot_path.py::TestReduceExHotPathHasNoImport::test_source_has_no_per_call_import(本文にpy.import(が無い)/::test_source_caches_newobj_in_once_lock(静的PyOnceLock経由)/::test_no_import_machinery_per_call(実行時: 200 回叩いてbuiltins.__import__呼び出し 0)::TestEffectBasePickle::test_reduce_ex_shape/test_pickle_roundtrip/test_pickle_roundtrip_all_protocols/test_cloudpickle_roundtrip/test_nested_effect、および既存のtests/test_pyclass_pickle.py(8 件)が全通uv run pytest(正典 gate)= 1308 passed, 86 skipped, 0 failed(618s、CPython 3.14t)make lintのローカル実測を下記に添付Verification deviations: なし。
TDD + semgrep(AGENTS.md の手順)
409bda6b, 実装前・意図的に red): 改修前は 200 回の__reduce_ex__がcopyregを 200 回 import していた(__import__実測)。ソース 2 件も red。
.semgrep.yamlにdoeff-vm-no-per-call-copyreg-resolutionを新設。py.import("copyreg")とgetattr("__newobj__")を doeff-vm / doeff-vm-core のsrc で禁止。改修前の形を bad fixture として
tests/semgrep/fixtures/rust/packages/doeff-vm/src/python_generator_stream.rsに常設し、発火(6,7 行目) と 出荷中ソースでの非発火 の両方を assert
(
tests/semgrep/test_vm_failfast_semgrep_rules.pyの 2 件)。docs/adr/enforcement-ledger.jsonのsemgrep_rulesを 247→248 に記帳(ADR-DOE-ENFORCE-001 R5)。
da187e32): 実装。全 red が green。ローカル検証(実測ログ)
make lintは最後のlint-packagesでpackages/doeff-agenticの pyright が35 errors で落ちるが、これは本 PR と無関係の既存 red(
@dogenerator の戻り型注釈まわり)。本 PR は
doeff-agenticの Python ソースを 1 行も触っていない(
git diff --stat参照)。射程外(意図的に触っていない)
sys.meta_path番兵(着地済み)py.import箇所の一括置換 — なお本修正後、packages/*/srcの Rust からpy.import/PyModule::importは 0 箇所になった(grep 実測)ので、同種の残件は現状ない。
発注元: ACP の調査席 trace-python-memory-job(検収書
decision-hypha-bff-thread-wedge-acceptance-2026-08-10.html付録『上流へのfollow-up(未起票)』の裁定起票)。
🤖 Generated with Claude Code