Skip to content

fix(state): serialise BoundWrapper.func by value when not importable by reference - #887

Open
pmrv wants to merge 2 commits into
mainfrom
claude/festive-brahmagupta-qpx6c6
Open

fix(state): serialise BoundWrapper.func by value when not importable by reference#887
pmrv wants to merge 2 commits into
mainfrom
claude/festive-brahmagupta-qpx6c6

Conversation

@pmrv

@pmrv pmrv commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Closes #840.

Problem

BoundWrapper relied on default dataclass pickling for func, which
references it by (module, qualname). That only resolves in a
ProcessPoolExecutor worker under the fork start method, where the worker
inherits the parent's already-imported objects. Under spawn/forkserver
(default on macOS/Windows, and on Linux 3.14+) the worker is a fresh
interpreter that must import func to find it, so anything defined in
__main__, a notebook, or a local closure raised
AttributeError/PicklingError at load time and brought the whole pool down
as BrokenProcessPool. There was no error at the submit site — the first
sign of trouble was a dead worker.

Fix

BoundWrapper.__reduce__ now serialises func into a standalone bytes
payload up front, instead of leaving it for the outer pickler to serialise:

  • If func is picklable by reference (the common case — a module-level or
    @fleche-decorated function), it's pickled with plain pickle, exactly as
    before. No cloudpickle dependency is introduced for this path.
  • Otherwise it falls back to cloudpickle, serialising func by value, with
    a clear TypeError if cloudpickle isn't installed.

Pre-serialising the payload means it survives regardless of which pickler
carries the outer BoundWrapper — stdlib pickle (ProcessPoolExecutor,
any start method) or cloudpickle (SshCache, executorlib) alike.

Testing

  • tests/unit/fleche/test_bound_wrapper.py: new cases for a locally-defined
    (non-importable) function pickling by value via cloudpickle, the clear
    error when cloudpickle is missing, and confirming a module-level function
    still round-trips without cloudpickle at all.
  • tests/regression/test_issue_840.py: end-to-end reproduction — a local
    closure shipped through a real spawn-context ProcessPoolExecutor.
    Verified this fails with AttributeError: Can't pickle local object ...
    on main and passes with this fix.
  • Full suite: pytest tests/ — 1724 passed (the only failures are 10
    pre-existing notebook-execution tests that fail in this sandbox because the
    jupyter console script isn't installed, unrelated to this change).
  • ty check src/ — clean.

Generated by Claude Code

claude added 2 commits August 22, 2026 03:15
…by reference

BoundWrapper relied on default dataclass pickling, which references func by
(module, qualname). That only resolves in a ProcessPoolExecutor worker under
the 'fork' start method, where the worker inherits the parent's already-
imported objects. Under 'spawn'/'forkserver' (default on macOS/Windows, and
on Linux 3.14+) the worker is a fresh interpreter that must import func to
find it, so anything defined in __main__, a notebook, or a local closure
raised AttributeError/PicklingError and brought the pool down as
BrokenProcessPool.

__reduce__ now serialises func into a standalone bytes payload up front:
plain pickle when func is importable by reference (unchanged, no cloudpickle
needed), else cloudpickle by value with a clear error if cloudpickle isn't
installed. Pre-serialising means the payload survives regardless of which
pickler carries the outer BoundWrapper (stdlib pickle via
ProcessPoolExecutor, or cloudpickle via SshCache/executorlib).

Closes #840

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nhem4ftatpXhjwtxDnLLaV
…round-trip a closure

test-minimum-deps CI resolves cloudpickle to its declared floor (2.0.0),
which can't reconstruct even a bare closure's code object on this Python
(TypeError: code() argument 13 must be str, not int) — the same class of
cloudpickle/Python version-skew gap test_wrap_executor_cloudpickle_lock.py
already skips around. Add the same defensive skip to the two new #840 tests
so they report an environment limitation instead of failing outright.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nhem4ftatpXhjwtxDnLLaV
pmrv added a commit that referenced this pull request Aug 22, 2026
…for #840 (#888)

Scheduled audit of `AGENTS.md` and its linked guides against the tracker
state as of 2026-08-22. No code landed on `main` since the previous
audit (#882), so the Quick Reference and Architecture claims are
unchanged; the delta is issue-tracker state only.

- Add the 2026-08-21 refactor cohort under open proposals: #883
(`_raw_sub_digests` hardcodes built-in `Digested` subtypes, breaking
`gc()`/`count_reuses()` for `register_destructurer` extensions — the one
correctness gap of the batch), #884 (sqlite URL handling hand-rolled
twice and drifted on `~`-expansion), #885 (`XDG_CACHE_HOME` companion to
#868), #886 (`benchmark_storage.py` five-op timing harness duplicated).
- Record PR #887 in flight against bug #840 (`BoundWrapper.__reduce__`
pre-serialises `func`: `pickle` by reference, `cloudpickle` by value).
- Split the single Bugs/Feature-request paragraph into one-item-per-line
sub-bullets so independent updates stop colliding in merges.

Verified still open and accurately described: PRs #797, #837, #873-#878,
#879, #881.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01J6LqSMaWKWzDfF7hF51Syq)_

Co-authored-by: Claude <noreply@anthropic.com>
pmrv added a commit that referenced this pull request Aug 23, 2026
…theme (#889)

Scheduled AGENTS.md audit, 2026-08-23.

No code has landed on `main` since the 2026-08-21 audit (63a0584), and
the issue/PR state recorded in `agents/DEVELOPING.md` is still current:
in-flight PRs #873#878, #881, #887, and #797 all remain open; no new
issues since #883#886. Spot-checks of the Quick Reference and
Architecture claims against `src/` all pass (public `__all__`,
`_lazy_default`/`_sticky_set`/`_hard_set`, `PreparedCall`/two-phase
save, `register_storage`, `_CACHE_TEMPLATES`,
`Runtime.cputime`/`systime`, module line counts quoted in #789/#832).
`AGENTS.md` and `agents/USAGE.md` need no changes.

One gap found: the performance theme names "pooled file handles in
`bagofholding_file.py`" as a fix candidate without noting that draft PR
#786 (open since 2026-07-23) already implements it, and PR #804 (open
since 2026-07-31) — the benchmark-harness fix that removes the
always-evicting `SizeLimitedCache(max_size=10)` config — was recorded
nowhere. Both are now listed under the perf theme as
check-before-duplicating entries, added as separate paragraphs to keep
future edits conflict-free.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01H9uSJpX8SLJrtYbc2ZaSv5

---
_Generated by [Claude
Code](https://claude.ai/code/session_01H9uSJpX8SLJrtYbc2ZaSv5)_

Co-authored-by: claude[bot] <claude[bot]@users.noreply.github.com>
pmrv added a commit that referenced this pull request Aug 28, 2026
…qlFile fsync cheap fix (#898)

Weekly AGENTS.md/DEVELOPING.md audit. Only one change since the
2026-08-27 pass (#897): issue #625's perf audit was refreshed the same
morning.

- Update the perf-audit pointer in `agents/DEVELOPING.md` from
"refreshed 2026-08-20" to 2026-08-27 and record the run's verdict (no
new source-caused regressions; flagged rows are the chronic
`BagOfHoldingH5File` per-op open cost or noise).
- Record the still-open SQL-side hot spot the refreshed audit re-flags:
`Sql` fsyncs once per key on `save`/`evict` because
`_configure_sqlite_pragmas` (`src/fleche/storage/sql.py:190-227`) sets
`journal_mode=WAL` but no `synchronous` pragma; the cheap fix (`PRAGMA
synchronous=NORMAL`) has been flagged in every audit since 2026-05-07.
Verified against the source — the function sets only `foreign_keys` and
`journal_mode`.

Everything else checked and current: no merges to `main` since #897;
open PRs (#873, #874, #887, #892, #894, #896, #797, #786, #804) and
issues (#893, #895, #625) are all already recorded.
`eisenforschung/landau` was audited in the same pass and needs no update
(nothing landed since its 2026-08-25 pass; in-flight PRs
#391/#394/#395/#414/#422 unchanged; spot-checked claims hold).

---
_Generated by [Claude
Code](https://claude.ai/code/session_012SCLy9y7aUR7F44Q7UviGo)_

Co-authored-by: Claude <noreply@anthropic.com>
pmrv added a commit that referenced this pull request Aug 29, 2026
…racker items (#904)

Scheduled AGENTS.md audit. Changes since the 2026-08-27 pass:

- Release-state claims updated: PR #793 (two-phase save) and PR #843
(lock-free pickle-family backends) shipped in 0.22.0 on 2026-08-28
(release PR #837); the three "unreleased as of 2026-08-27" qualifiers
are gone. Noted that `fix(query)` #894 is an ancestor of the 0.22.0 tag
(verified via `git tag --contains c82c691`) even though the generated
changelog entry omits it.
- PR #894 (`latest()`/`oldest()` raise `ValueError` when no matching
call carries `Runtime` metadata, `IndexError` on empty), PR #892 (gc
mid-sweep guards + 441-case digest product collapse, suite 1738 → 1299),
and docs-audit PRs #873/#874 moved from the in-flight section to
decisions landed; the in-flight section is now a one-liner pointing at
the five PRs still open (#887, #804, #797, #786, #523), each covered
under its theme.
- New tracker items recorded: the 2026-08-28 refactor cohort #899#902,
bug #903 (`put()` `filelock.Timeout` fatality on ≤0.21.2,
dedup-concentrated lock contention; resolved for pickle-family by
0.22.0's atomic rename, bagofholding multi-bag still exposed per #893),
and the #895 resolution (documented limitation per the 2026-08-28
maintainer comment). #900 cross-referenced at the `pypi-publish.yml`
line in the workflows list.
- The #893 bullet's `lock_timeout` mitigation sentence rephrased to the
current state: the `FutureWarning` drop covers only pickle-family
configs, a bagofholding `values.lock_timeout` still works on 0.22.0
(checked against `config.py:466`).
- USAGE.md: query section states the `latest()`/`oldest()`
`ValueError`/`IndexError` contract (verified against
`query.py:149-188`).

landau's AGENTS.md was audited in the same pass and left unchanged — no
code or tracker movement since its 2026-08-24 update (only docs commits
on main; issues #423#425 and PRs #422/#414/#395/#394/#391 unchanged).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_014myhh6xKWC2VhQeB92mGSn

---
_Generated by [Claude
Code](https://claude.ai/code/session_014myhh6xKWC2VhQeB92mGSn)_

Co-authored-by: Claude <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.

BoundWrapper only survives ProcessPoolExecutor under the fork start method

2 participants