Loud fail for missing build tools; fix worker payload serialization on editable installs; mode-independent run-stats block counts - #70
Conversation
…lding daisy if necessary utilities are missing
The serializer fix was only covered incidentally (tests that fail only under editable installs); test_worker_serialization.py now forces the by-value failure mode deterministically with a synthetic module, and covers the daisy-module-global shape, the __main__ end-to-end path, the eager-failure guidance, and _PicklableLocal. Thread-mode block counting regained a pinned test after the run-stats tests were unpinned to the default subprocess mode. Rust side: unit tests for RunTally counting and the build_run_stats merge (including synthetic external-worker entries), bookkeeper registration lifecycle and recycled-port hygiene, and the TCP integration test now asserts external registered workers appear in run stats. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxYd8bCjgZLoESSe3smbwF
dd8cb9f to
9f6bf9f
Compare
|
Closing as superseded. This PR bundled three unrelated changes — its own title announced all three, and
They share no files, so they can be reviewed in any order. #74's description notes one ordering caveat Two things this PR got wrong that are fixed in the split1. It broke the docs CI, and I hadn't noticed. Commit 357d040 converted So the 2. The claimed test results did not hold in a clean dev environment. This PR's description says Also found while splitting, and worth separate issues
Happy to open any of those as PRs if useful. |
Pure `ruff --fix` pass over the Python sources with --select I001,F401,F811,RUF100,UP035,PYI029,PYI041. No behaviour changes: import sorting, unused-import removals, duplicate-import dedups, redundant noqa removals, typing.Callable -> collections.abc.Callable, and two .pyi stub cleanups (redundant __repr__ declarations; float|int -> float). Rebased from v2.0_patch onto v2.0 now that #70 is closed in favour of #72/#73/#74. Regenerated rather than cherry-picked, so the two hunks that only existed via #70 (build_wrapper.py, tests/test_worker_serialization.py) are simply absent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Alternative to PR #70's custom dill Pickler subclass (branch v2-bugfix-worker-serialization): rather than teaching dill to pickle modules by reference, use the serializer whose DEFAULT is exactly that. dill pickles any module outside site-packages by value — entire __dict__ — so a block function that references its own project package as a global (import mypkg; mypkg.helper()) fails the payload if that package holds any unpicklable module-level object, even an untouched one. Reproduced on a normal non-editable install: TypeError: cannot pickle '_struct.Struct' object. cloudpickle pickles importable modules by reference and reserves by-value for __main__ — the exact split daisy needs, since workers replicate the parent's sys.path and remote cluster workers re-import anyway. Deletes the need for a hand-maintained dispatch override and for the _PicklableLocal workaround. Trade-off, documented in the module docstring, MIGRATION.md and CHANGELOG: cloudpickle refuses threading primitives and write-mode file handles (directly or via a bound method's self), where dill silently shipped a lock that synchronized nothing. daisy now raises at submit time naming both fixes (construct inside the block function, or use worker_processes=False where locks are real). This exposed two vacuous tests: test_resources' inline task builders measured cross-worker concurrency with in-process counters while running subprocess workers, so the parent observed peak=0 and 'peak <= budget' asserted nothing. Pinned to thread workers like the module's factory already was, so they measure again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three things: a loud, actionable failure when build tooling is missing; a fix for subprocess-worker payload serialization breaking on editable/source-tree installs; and correct
blocks_processedrun stats across all worker execution modes.1. Loud fail for missing install tools (
357d040)Building daisy from source requires a C linker and the Rust toolchain, and the stock maturin error when they're missing is cryptic.
build_wrapper.pynow wraps maturin's PEP 517 hooks (build_wheel/build_editable) and checks forcc/gcc/clangandcargoup front, failing with per-platform install instructions instead.pyproject.tomlalso gainsdev,examples, anddocsextras so pip users get the same dependency groups uv users had.2. Fix: subprocess-worker payloads no longer break on editable installs (
ce3f488)Symptom: with daisy installed editable (or from a source tree),
run_blockwisefailed at startup withTypeError: cannot pickle '_thread._local' objectfor any 1-arg block function that references thedaisymodule as a global (e.g.block.status = daisy.BlockStatus.SUCCESS).Root cause: subprocess workers (the default execution mode) serialize the block function with
dill.dumps(..., recurse=True), which ships the globals the function references. dill pickles any module outsidesys.prefix/site-packages by value — its entire__dict__— so one unpicklable module-level object anywhere in the namespace fails the whole payload. daisy itself contains two (daisy.logging._active, athreading.local, anddaisy._worker_processes._LEN, astruct.Struct). Wheel installs never trip this, which is why CI stayed green; editable installs always do.Fix:
_serializenow uses adill.Picklersubclass that pickles every importable module by reference (_make_modules_by_ref_pickler). This is sound because the child replicates the parent'ssys.pathbefore deserializing (read_payload), so anything importable in the parent is importable in the child. Only__main__(and__mp_main__) keep dill's by-value path — the child's__main__is the worker shim, so script/notebook globals must ride in the payload, and they still do (recurse=Trueis load-bearing and untouched). This also fixes the same failure for user packages installed editable with unpicklable module-level state.Also:
daisy.logging._activeis now a_PicklableLocal(pickles as a fresh, empty local) as defense-in-depth for other transports.Task(worker_processes=False)) instead of a bare dill traceback.3. Fix:
blocks_processedcounted server-side, correct in every mode (ce3f488)Symptom: the Resource Utilization report showed
blocks 0per task (and empty per-worker counts) for subprocess-mode runs — the default — andtests/test_run_stats.pyhad to pin tests to thread mode. This was the documented gap inRUN_STATS.md.Root cause:
WorkerStats.blocks_processedwas incremented only inside the in-process worker thread's block loop. Subprocess shim workers and external cluster workers process blocks where the stats layer never looked.Fix: counting moves to the server, where every worker's block returns already arrive over TCP:
Register { task_id, worker_id }, sent byClient::connectright after the connection opens (appended as the last enum variant, so existing message discriminants are unchanged on the wire).ReleaseBlockorBlockFailed) increments aRunTallycounter per task and per worker.build_run_statsmerges the per-worker counts into the exit-channelWorkerStatsby(task_id, worker_id); registered workers with no thread in the server process (fully external workers) get synthetic entries carrying their block counts.Semantics notes:
Internal API change:
Client::connectand_rs.SyncClientnow take aworker_id(both internal;daisy.Clientreads it fromDAISY_CONTEXTas before).docs/source/design/RUN_STATS.mdis updated — the "known gap" section is replaced by the Register design, withdaisy.profile_blockkept as the future path for per-block CPU/RSS.Tests (
9f6bf9f)tests/test_worker_serialization.py(new): forces the by-value failure mode deterministically with a synthetic module carrying athreading.local+struct.Struct(so coverage doesn't depend on install layout), the daisy-module-global shape,__main__-defined function end-to-end through real subprocess workers, eager-failure guidance, and_PicklableLocalround-trip.tests/test_run_stats.py: the previously pinned tests now run the default subprocess mode and theirblocks_processedassertions pass; a new pinned test keeps thread-mode counting covered.RunTallycounting and thebuild_run_statsmerge (including synthetic external-worker entries and the unregistered-client fallback); bookkeeper registration lifecycle + recycled-port hygiene; the TCP integration test now asserts externally-connected registered workers appear in run stats.cargo test -p daisy-core46 passed;pytest tests/204 passed, 1 xfailed (the intentionaldaisy.messagesdocumentation xfail).🤖 Generated with Claude Code
https://claude.ai/code/session_01KxYd8bCjgZLoESSe3smbwF