Skip to content

fix(doeff-vm): EffectBase.__reduce_ex__ の copyreg 解決を PyOnceLock で 1 回きりにする - #586

Open
proboscis wants to merge 2 commits into
mainfrom
feat/impl-doeff-vm-reduce-ex-imports-copyreg-per-call-86fac7
Open

fix(doeff-vm): EffectBase.__reduce_ex__ の copyreg 解決を PyOnceLock で 1 回きりにする#586
proboscis wants to merge 2 commits into
mainfrom
feat/impl-doeff-vm-reduce-ex-imports-copyreg-per-call-86fac7

Conversation

@proboscis

Copy link
Copy Markdown
Owner

何を直したか

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 対応

発注の受入条件 対応する shipped test
(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)
(2) pickle 往復の既存挙動が変わらない ::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 件)が全通
(3) doeff の既存テスト suite が緑 uv run pytest(正典 gate)= 1308 passed, 86 skipped, 0 failed(618s、CPython 3.14t)
(4) upstream 規律: ローカル検証で緑を確認してから main へ 本 PR は未 merge。上記 suite + make lint のローカル実測を下記に添付

Verification deviations: なし。

TDD + semgrep(AGENTS.md の手順)

  • 第 1 相(commit 409bda6b, 実装前・意図的に red): 改修前は 200 回の
    __reduce_ex__copyreg を 200 回 import していた(__import__ 実測)。
    ソース 2 件も red。
  • 第 2 相: .semgrep.yamldoeff-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.jsonsemgrep_rules を 247→248 に記帳
    (ADR-DOE-ENFORCE-001 R5)。
  • 第 3 相(commit da187e32): 実装。全 red が green。

ローカル検証(実測ログ)

uv run pytest                → 1308 passed, 86 skipped, 34 warnings in 618.28s
uv run pytest <本件 4 file>  → 37 passed
make lint-ruff               → All checks passed!
make lint-pyright (doeff/)   → 0 errors
make lint-semgrep            → Ran 197 rules on 885 files: 0 findings(rule 総数 248 を load)
cargo check (doeff-vm)       → 通過(新規 warning なし)
cargo fmt --check            → 本 PR の変更ファイルは差分なし

make lint は最後の lint-packagespackages/doeff-agentic の pyright が
35 errors で落ちるが、これは本 PR と無関係の既存 red(@do generator の
戻り型注釈まわり)。本 PR は doeff-agentic の Python ソースを 1 行も触っていない
(git diff --stat 参照)。

射程外(意図的に触っていない)

  • ACP 側の worker pool / warmup / sys.meta_path 番兵(着地済み)
  • 他の py.import 箇所の一括置換 — なお本修正後、packages/*/src の Rust から
    py.import / PyModule::import0 箇所になった(grep 実測)ので、
    同種の残件は現状ない。

発注元: ACP の調査席 trace-python-memory-job(検収書
decision-hypha-bff-thread-wedge-acceptance-2026-08-10.html 付録『上流への
follow-up(未起票)』の裁定起票)。

🤖 Generated with Claude Code

proboscis and others added 2 commits August 10, 2026 07:14
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant