Major refactor: scheduling, shared event loop, CLI extraction, observability#40
Merged
Conversation
Phase 0 of the major refactor. Fixes three confirmed bugs and adds two observability improvements, with regression tests for each. Bug fixes: - ExecutionManager.run() now cleans up _worker_jobs and _msgs via try/finally on cancellation/timeout (previously leaked stale entries, corrupting LEAST_BUSY allocation) - Sync and async worker functions now catch exceptions and send error UP_RUN_RESPONSE instead of crashing the worker loop or hanging the client forever - _msg_recv_task_func silently discards orphaned responses for cancelled jobs instead of raising KeyError - netrun validate now surfaces resolution errors as warnings instead of silently swallowing them Observability: - faulthandler + SIGUSR1 registered in Net.init() for stack dumps on hang - gc.collect() + peak RSS logging (DEBUG level) between retry attempts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 1 of the major refactor. Thread pool workers no longer create per-worker event loops. Instead, a single shared event loop runs on a dedicated daemon thread per process. Async node functions are submitted via run_coroutine_threadsafe(), fixing three classes of production bugs: - asyncio.create_subprocess_exec no longer hangs (shared loop has child watcher) - Nested async works naturally (no nested run_until_complete) - asyncio.Lock/Event work across concurrent workers (same event loop) Key changes: - NetFuncPreprocessor gets dual wrapper: __call__ (async, for SingleWorkerPool) and call_for_sync_worker (sync, for ThreadPool/MultiprocessPool) - Sync user functions run directly in worker threads (no event loop overhead) - Async user functions dispatch to shared loop, worker thread blocks on result - ExecutionManager creates/owns the shared loop lifecycle - Each multiprocess subprocess creates its own process-local shared loop - _func_runner falls back to shared loop for async when no preprocessor is set Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 2 of the major refactor. Three additive API features that address
production pain without changing existing behavior.
ctx.state:
- Mutable dict on NodeExecutionContext preserved across retries
- Same dict instance reused across retry attempts for the same epoch
- Cleared on final success or permanent failure
- Lets retry-heavy nodes cache expensive state (e.g., 10GB DataFrames)
depends_on:
- New field on NodeExecutionConfig: depends_on: list[str]
- Node epoch can't start until all named dependencies have completed
at least one epoch successfully
- Enforced in _filter_startable_epochs (scheduler-level gating)
- Replaces manual control-edge wiring for ordering constraints
resources:
- Named semaphores for bounded concurrency and mutual exclusion
- Net-level: NetConfig.resources defines capacity (e.g., {"heavy_mem": 1})
- Node-level: NodeExecutionConfig.resources declares requirements
- Acquired when epoch starts, released in finally block
- Replaces the heavy-pool workaround (single-worker pool for mutex)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 3 of the major refactor. Introduces BasePool abstract base class that provides concrete recv(), try_recv(), start(), close(), and health monitoring. ThreadPool, MultiprocessPool, and SingleWorkerPool now extend BasePool, implementing only pool-specific logic. BasePool provides: - Unified recv/try_recv with error checking and timeout handling - Lazy recv task startup with _create_recv_loops() override - Health monitoring via _check_worker_health() override - Worker silence detection (configurable threshold, logs warnings) - Common lifecycle management (start/close with proper ordering) - Context manager support Pool migrations: - SingleWorkerPool: 451 -> 310 lines (-31%) - ThreadPool: 458 -> 323 lines (-29%) - MultiprocessPool: 1102 -> 905 lines (-18%) Overrides close() for SHUTDOWN_COMPLETE protocol - RemotePoolClient: unchanged (different architecture) ExecutionManager cleanup: - Removed _thread_worker_func and _multiprocess_worker_func wrappers (inline functools.partial at call sites) - Replaced O(n) round-robin list with O(1) counter Net reduction: -518 lines across pool+EM (853 added, 1371 removed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 4 of the major refactor. Cleans up config/runtime drift and consolidates scattered epoch state into a single data structure. Config fields removed (unused or partially wired): - NodeExecutionConfig: defer_net_actions, capture_prints, print_flush_interval, print_buffer_max_size - NetConfig: dead_letter_callback, dead_letter_path - PoolConfig: capture_prints, print_flush_interval These fields were defined and resolved but never read at runtime, creating misleading configuration knobs. Epoch state consolidated: - Moved structured_logs from _epoch_log_buffers dict onto _EpochState - Moved net_actions from _epoch_net_actions dict onto _EpochState - Moved retry_state (ctx.state) from _epoch_states dict onto _EpochState - Removed three separate dicts, all state now lives on the epoch record - Easier to reason about epoch lifecycle, less error-prone cleanup Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds 6 tests covering gaps identified in the feature test audit: - test_ctx_state_cleared_on_success: verify retry_state cleaned up after success - test_depends_on_multiple_dependencies: node C depends on both A and B - test_depends_on_none_is_noop: no depends_on works as before - test_resources_multi_slot: 2-slot resource allows bounded concurrency - test_resources_undefined_raises: referencing undefined resource errors - test_silence_detection_warns: worker silence triggers warning log Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds config-time validation to catch depends_on misconfigurations that would otherwise cause silent deadlocks: - Circular dependency detection via topological sort in Net.__init__ (A depends_on B, B depends_on A → ValueError with clear message) - Invalid node name detection (depends_on non-existent node → ValueError) - Updated field docstrings with edge case documentation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously, when a worker crashed, BasePool.recv() raised WorkerCrashed which killed _msg_recv_task_func — the central message receiver for the entire pool. This made ALL workers in the pool unreachable, not just the crashed one. Now: - BasePool.recv_raw() returns error messages without raising, for use by ExecutionManager's message receiver - _msg_recv_task_func uses recv_raw() and routes pool-level errors to the specific jobs running on the affected worker - _msg_worker_map tracks msg_id -> worker_id for error routing - _recv_msg detects routed error messages and raises to the caller - Other workers remain fully functional after a single worker crash BasePool.recv() is unchanged — external callers still get exceptions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New sample project (14_web_scraping_pipeline) demonstrating every major netrun feature in a single, realistic pipeline: [load_config] → [validate] → [broadcast] → 3 async scrapers → [join] → [analyze] → [export] Features demonstrated: - Async node functions on thread pool (shared event loop) - ctx.state: caches computation across retries - depends_on: setup_output waits for load_config without data edge - resources: http_conn=2 limits concurrent scrapes to 2 of 3 - Broadcast factory: fans config to parallel scrapers - Join factory: collects results from all scrapers - Retries: scrape_stocks simulates intermittent 503 failures - Structured logging (ctx.log) with key=value fields - Node variables (ctx.vars) for runtime-configurable URLs - Output queues for result collection - Signals (epoch_finished/epoch_failed) on all nodes - Thread pool with 3 workers + main pool Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…to sample
Replaces the fragile pattern of mutating func_preprocessor._shared_loop
from worker threads with a clean thread-local (_worker_state.shared_loop).
Why: The preprocessor must stay picklable for multiprocess pools. Previously,
thread pool workers set _shared_loop on the shared preprocessor instance,
which made it unpicklable if a multiprocess pool tried to serialize it later.
The __getstate__ band-aid was masking this design issue.
Now: _worker_state (threading.local) holds the shared loop. The preprocessor's
call_for_sync_worker reads it from thread-local when dispatching async functions.
No execution state stored on the preprocessor — it stays fully picklable.
Sample project updated:
- Added multiprocess pool ("compute") for the analyze node
- Added retain_epoch_logs for full log visibility
- Added net.logs.print_all() showing all node output including multiprocess
- All 3 pool types now demonstrated: main, thread, multiprocess
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove dead code and simplify the ExecutionManager protocol: - Remove NetProtocolKeys enum (defined channel-based protocol that was never implemented — deferred actions replaced it entirely) - Remove send_channel feature (Net always passed False; bidirectional channel during execution was never used) - Merge UP_RUN_STARTED into UP_RUN_RESPONSE (eliminates one protocol message round-trip per execution; started timestamp now included in the single response) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…aid/dry-run
Prompt to recreate this work:
Extract the CLI from `netrun` into a new sibling Python package called
`netrun-cli` (following the `netrun-utils` pattern), and add three new CLI
commands.
Background: the CLI has a clean boundary — its only imports from netrun are
`netrun.net.config` and `netrun.tools`, and nothing in `netrun` imports from
`netrun.cli`. So extraction is safe.
Steps:
1. Create `netrun-cli/` at the repo root with:
- `pyproject.toml` declaring `netrun-cli` v0.5.0 depending on
`netrun>=0.5.0`, `typer>=0.15.0`, `tomli-w>=1.0`. Register
`[project.scripts] netrun = "netrun_cli._app:app_main"`. Use the same
ruff/pytest config and uv source pattern as `netrun-utils`.
- `nblite.toml` with the standard `nbs -> pts -> lib` pipeline, exporting
to `src/netrun_cli` and `src/tests`.
- Empty `src/netrun_cli/__init__.py` (hatchling needs it to find the
package; nblite does not auto-generate it).
2. Move the seven CLI source files from `netrun/pts/netrun/10_cli/*.pct.py`
to `netrun-cli/pts/netrun_cli/`. In each file:
- Change `#|default_exp cli._foo` to `#|default_exp _foo`.
- Change `from netrun.cli._foo import ...` to `from netrun_cli._foo import ...`.
- Leave `netrun.net.config.*`, `netrun.tools.*` imports untouched — those
are external dependencies on the `netrun` package.
- Leave hardcoded strings (`name="netrun"` in the Typer app, the
`.netrun.json/.netrun.toml` extensions, GitHub repo refs in
`download_agents`, `netrun.node_factories.broadcast` warning) untouched.
3. Move the two CLI test files from `netrun/pts/tests/10_cli/` to
`netrun-cli/pts/tests/`. In each:
- Update `#|default_exp` (drop the `cli.` prefix).
- Update `from netrun.cli._app import app` -> `from netrun_cli._app import app`.
- In `test_cli.pct.py`, the SAMPLE_DIR path traversal goes from `.parent`
5 times to 4 times (one fewer directory level).
- Replace all `netrun.cli._download_agents` mock-patch strings with
`netrun_cli._download_agents` (~5 occurrences).
4. Run `nbl export --reverse && nbl export` in `netrun-cli/` to populate
`nbs/` and `src/`.
5. Run `uv sync` in `netrun-cli/`. Then `uv pip install -e .` to install the
`netrun` script entry point.
6. Delete `netrun/pts/netrun/10_cli/`, `netrun/pts/tests/10_cli/`,
`netrun/src/netrun/cli/`, `netrun/src/tests/cli/`, and any matching
directories under `netrun/nbs/`.
7. Edit `netrun/pyproject.toml`:
- Remove `[project.scripts] netrun = "netrun.cli._app:app_main"`.
- Remove `cli = ["typer>=0.15.0", "tomli-w>=1.0"]` from
`[project.optional-dependencies]`.
- Replace `"netrun[cli]"` in the dev dependency group with `"netrun-cli"`.
- Add `netrun-cli = { path = "../netrun-cli", editable = true }` under
`[tool.uv.sources]`.
Then run `uv sync` in `netrun/`.
8. Add three new CLI features in `netrun-cli/pts/netrun_cli/02_commands.pct.py`:
- `node <name> --edges`: when the flag is set, include an `edges` key in
the JSON output with `incoming` (list of `{source: "node.port", port: ...}`)
and `outgoing` (list of `{port: ..., target: "node.port"}`). Iterate
`net_config.graph.edges` and filter by `target_node == name` /
`source_node == name`.
- `structure --format=mermaid|json` (default `json` for back-compat):
when `format=mermaid`, emit `graph LR`, then one node line per node
`<id>["<name>"]`, then one edge line per edge
`<src> -->|"<src_port> → <tgt_port>"| <tgt>`. Sanitise IDs by replacing
spaces and hyphens with underscores. Reject any other format with
`typer.Exit(1)`.
- `dry-run`: new command. Topologically sort the nodes (Kahn's algorithm)
using data edges (skip `dependency=True`) plus `depends_on` entries as
ordering edges. Emit JSON: `source_nodes` (no incoming), `sink_nodes`
(no outgoing), `execution_order` (each entry has `node`, optionally
`pools`, `depends_on`, `resources` from execution_config), and
`total_nodes` / `total_edges`.
Register `dry-run` in `00_app.pct.py` alongside the other top-level
commands.
9. Add tests in `netrun-cli/pts/tests/test_cli.pct.py` covering all three:
- `test_node_edges_incoming` / `test_node_edges_outgoing` /
`test_node_edges_none` / `test_node_no_edges_by_default`
- `test_structure_mermaid` (verify `graph LR`, node decls, edge labels) /
`test_structure_json_default` / `test_structure_bad_format`
- `test_dry_run_basic` / `test_dry_run_execution_order` /
`test_dry_run_sink_nodes` / `test_dry_run_depends_on_and_resources`
(the last builds a tmp config with depends_on + resources to verify
ordering and metadata appear in the output)
- Add `assert "dry-run" in result.stdout` to `test_help`.
10. Re-export and run: `cd netrun-cli && nbl export --reverse && nbl export
&& uv run pytest src/tests/ -v`. All 61 tests should pass.
11. Verify `cd netrun && uv run pytest src/tests/ -q`. All 1305 tests should
pass (down from 1355 — the 50 CLI tests moved out).
12. Smoke-test the new features against
`sample_projects/00_basic_net_project/main.netrun.json`:
- `.venv/bin/netrun node add --edges -c <path>` shows edges
- `.venv/bin/netrun structure --format mermaid -c <path>` emits Mermaid
- `.venv/bin/netrun dry-run -c <path>` shows topo order
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ples
Prompt to recreate this work:
After completing Phases 0-5 of the major refactor (bug fixes, shared event
loop, ctx.state/depends_on/resources, BasePool extraction, CLI extraction
with --edges/--format=mermaid/dry-run), audit and update all repo
documentation against the current code state. Also add focused sample
projects for the three new scheduling/state features.
Steps:
1. Delete `netrun/PROJECT_SPEC.md` (file already gone, but scrub all
references): remove the line in `netrun/README.md` documentation list,
the line in `README.md` documentation list, the `**See netrun/PROJECT_SPEC.md
for the full specification.**` line in `CLAUDE.md`, the
`│ ├── PROJECT_SPEC.md # Full specification` line in CLAUDE.md's
repo structure, and the `# Additional execution options (from PROJECT_SPEC.md)`
comment in `netrun/pts/netrun/06_net/00_config/01_nodes.pct.py`.
2. Update `CLAUDE.md`:
- Reword the project structure intro to "split into the following
components" (was "two main components") and add a `### netrun-cli (CLI)`
subsection describing it as a separate package.
- Remove "CLI for validation, inspection, and config conversion" bullet
from the netrun feature list.
- Update the Repository Structure ASCII tree: remove `PROJECT_SPEC.md`
line, insert a `netrun-cli/` block under `netrun/` showing the same
pts/src structure plus pyproject.toml note.
- Remove the CLI entry from "Current Implementation Status" (line "8.
**CLI** (`netrun.cli`) - ..."). Renumber remaining items. Add a closing
line: "The CLI lives in the separate **`netrun-cli`** package — see
"netrun-cli specifics" below."
- In the ExecutionManager usage example, remove the `send_channel=False,`
line and the `print(result.print_buffer)` line. After the example, add
a paragraph about the shared event loop dispatch model and a paragraph
about worker crash isolation. In the Protocol Keys list, remove
`UP_RUN_STARTED` and `UP_PRINT_BUFFER`; add `UP_SEND_FUNCTION_RESPONSE`.
- Augment the "Key Classes" list under Net Module: bold the new fields
(`resources` on NetConfig; `depends_on`, `resources` on
NodeExecutionConfig; `ctx.state` on NodeExecutionContext).
- Append `, scheduling constraints (depends_on, resources),
retry-persistent state (ctx.state)` to the Net "Features:" line.
- Add a "#### Scheduling: depends_on and resources" subsection (with
code example showing net-level `resources={"gpu":1, "http_conn":4}`
and node-level `depends_on=["preprocess"], resources={"gpu":1}`, plus
mutual-exclusion / bounded-concurrency patterns).
- Add a "#### Retry-persistent state: ctx.state" subsection with the
`pd.read_parquet` cache example and lifecycle/locality notes.
- Delete the entire "### CLI (`netrun.cli`)" section.
- Add a new "## netrun-cli specifics" top-level section after "### Core"
covering: editing workflow, install/run instructions, a command table
listing every command including new ones, and a "Adding a new command"
four-step recipe.
3. Update root `README.md`:
- In the netrun component description, replace "a CLI" mention with
scheduling + ctx.state language.
- Add a `### [netrun-cli](netrun-cli/) — CLI` component section after
netrun, mentioning it depends on netrun for config models.
- Add `netrun-cli/` to the Repository Structure ASCII tree.
- Add a "### netrun-cli" Quick Start block with `uv sync`,
`.venv/bin/netrun --help`, and example commands against
`sample_projects/00_basic_net_project`.
- Remove the PROJECT_SPEC.md line from the Documentation list.
4. Update `netrun/README.md`:
- Expand the Key Features list: replace the old factories bullet with
"function/broadcast/join factories"; add bullets for "Scheduling
constraints (depends_on, resources)", "Retry-persistent state
(ctx.state)", "Shared event loop", "Worker crash isolation". Drop the
CLI bullet.
- Add a closing line: "The CLI is shipped as a separate package: see
[netrun-cli](../netrun-cli/)."
- Remove the PROJECT_SPEC.md line from the Documentation list.
5. Update inline docstrings (.pct.py files; auto-export to .py via nblite):
- `netrun/pts/netrun/05_execution_manager.pct.py`:
- Add a class-level docstring on `ExecutionManager` covering the
single-terminal-event protocol, worker crash isolation, and the
shared event loop model (one daemon thread per process scope).
- Update `__init__`'s docstring to list pool types as types
(`ThreadPool`, `MultiprocessPool`, `SingleWorkerPool`,
`RemotePoolClient`), not strings.
- Rewrite `run()`'s docstring to emphasise: targets a specific worker,
single `UP_RUN_RESPONSE` carries result+timestamps, returns
`JobResult`, raises whatever the user fn raises. Drop any mention of
`send_channel` or `UP_RUN_STARTED`.
- Add a docstring to `run_allocate()` documenting the candidate-list
expansion (pool ID -> all workers; tuple -> specific worker), the
three allocation methods (ROUND_ROBIN/RANDOM/LEAST_BUSY), and the
empty-list ValueError.
- `netrun/pts/netrun/06_net/01_net/00_context.pct.py`: replace the
`NodeExecutionContext` class docstring with one that explicitly
documents `ctx.state` — what it is (per-epoch dict), retry semantics
(same instance reused, cleared on success/permanent fail), locality
(per worker for multiprocess/remote, picklability requirement), and
a `pd.read_parquet`-style example. Note that `state` is NOT deferred
unlike packet operations.
- `netrun/pts/netrun/06_net/01_net/02_net.pct.py`: expand the `Net`
class docstring to enumerate the major responsibilities (pool
lifecycle, scheduling via depends_on+resources, retries with
ctx.state, timeouts, cache/file-storage replay, output queues,
signals/controls, structured logging, lifecycle callbacks). Note the
shared event loop model and worker crash isolation. Mention the
`async with Net(config) as net:` (and sync) context-manager usage.
- `netrun/pts/netrun/06_net/00_config/01_nodes.pct.py`: remove the
"# Additional execution options (from PROJECT_SPEC.md)" comment line
before the `defer_init` field.
Then run `cd netrun && nbl export --reverse && nbl export` to propagate.
6. Add three focused sample projects under `sample_projects/`:
- `15_ctx_state/`: Single node `expensive_pipeline` that simulates a
0.5s heavy data load on first attempt, caches it in `ctx.state["heavy"]`,
then runs flaky logic that fails on attempt 1 and succeeds on attempt
2 (reading `ctx.state["attempts"]` and incrementing). Config has
`retries: 2`. The print logs should show "Loading heavy data" once and
"Reusing heavy data from previous attempt — no reload" on retry.
Output queue `results` collects `pipeline.out`.
- `16_depends_on/`: Three nodes `step_a`, `step_b`, `step_c` with
**no data edges** between them. `step_b.execution_config.depends_on
= ["step_a"]` and `step_c.execution_config.depends_on = ["step_b"]`.
Each node takes a `trigger` parameter. main.py injects `["go"]` to all
three triggers up front; output queue `all_done` collects all three
`out` ports. Verify the completion order matches `[A, B, C]`.
- `17_resources/`: Three sleeping (0.2s) `gpu_job_1/2/3` nodes on a
`threads` thread pool with `num_workers: 3`. Net-level
`resources: {"gpu": 1}`; each node declares
`execution_config.resources: {"gpu": 1}`. Inject all triggers up front.
The total wall-clock time must be ~0.6s (serial), not ~0.2s (parallel).
Each sample needs a `pyproject.toml` (standard form: `dependencies =
["netrun"]`, `[tool.uv.sources] netrun = { path = "../../netrun",
editable = true }`), a `nodes.py`, a `main.netrun.json`, and a `main.py`.
Note: in `main.netrun.json`, `output_queues` lives at the **top level**,
not nested under `graph`.
7. Run sample 14, 15, 16, 17 with `uv run python main.py` to confirm they
work. Confirm:
- 15 prints `Result: 499542` (= sum(0..999) + 42 = 499500 + 42)
- 16 prints `Completion order: ['A complete', 'B complete', 'C complete']`
- 17 reports total wall-clock ~0.6s
8. Run the full test suites: `cd netrun && uv run pytest src/tests/ -q`
(1305 pass) and `cd netrun-cli && uv run pytest src/tests/ -q` (61 pass).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prompt to recreate this work:
The netrun-ui CI tests `test_config_schema.py::TestRegistrationCompleteness`
detect drift between the Pydantic config models in `netrun` and the field
registrations in `netrun-ui/src/lib/configFieldRegistrations.ts`. After the
major netrun refactor (Phases 0-5), three registrations are missing and six
are stale.
Steps:
1. In `netrun-ui/src/lib/configFieldRegistrations.ts`:
- Remove stale registrations:
* `NetConfig.dead_letter_path`
* `NetConfig.dead_letter_callback`
* `NodeExecutionConfig.defer_net_actions`
* `NodeExecutionConfig.capture_prints`
* `NodeExecutionConfig.print_flush_interval`
* `NodeExecutionConfig.print_buffer_max_size`
- Add missing registrations (all `'auto'`):
* `NetConfig.resources`
* `NodeExecutionConfig.depends_on`
* `NodeExecutionConfig.resources`
2. In `netrun-ui/tests/test_config_schema.py::test_complex_field`,
`dead_letter_callback` no longer exists. Replace it with `default_signals`
(a `dict[str, ...]` field still on NetConfig that classifies as
`FieldCategory.COMPLEX`).
3. Verify: `cd netrun-ui && uv run pytest tests/test_config_schema.py -q` —
all 47 tests should pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Empirically verify every Phase 0–5 feature in PR #40 against the live runtime (parallel verification subagents driving repro scripts), then fix the issues that surfaced: 1. ctx.state did not persist across multiprocess/remote retries — every retry sent the parent's still-empty dict because worker mutations never round-tripped back. Add a `state` field to NodeExecutionResult, populate it in `_get_execution_result()`, and copy it back into `_epochs[epoch_id].retry_state` after each attempt so the next retry sees prior-attempt mutations. 2. Non-picklable exceptions raised in worker code (or carried in NodeExecutionResult.exception) silently hung the parent on multiprocess pools — `channel.send(e)` blew up on PicklingError, the bare-except swallowed it, and no terminal UP_RUN_RESPONSE arrived. Add `to_picklable_exception()` helper that returns the original `e` if picklable or a `RuntimeError` shim that preserves the type name and repr; apply it in both EM worker error paths and the preprocessor wrapper that catches user exceptions. Replace the silent `except: pass` with a logged error so future failures are diagnosable. 3. `netrun validate` reported broken factory imports as warnings with exit 0, hiding real config breakage in CI. Promote resolution failures to `errors` with non-zero exit. Also temporarily add the config's `project_root_path` to `sys.path` during resolution so factories resolve the same way they will at runtime, and clean matching modules out of `sys.modules` afterwards so back-to-back validates of different sample projects don't pick up cached symbols. 4. Disabled-dep silent hang — when a node's `depends_on` referenced a node with `enabled=False`, the dependent waited forever with no diagnostic. Emit a logging.WARNING at Net construction listing dependents of disabled nodes (so users running with controls can still enable at runtime). 5. Undeclared-resource silent skip — `_get_allowed_epochs` short-circuited the resource check when `NetConfig.resources` was empty, ignoring undeclared node-level `resources` keys. Validate at Net construction that every node-level resource name is declared in `NetConfig.resources`; remove the now-unreachable runtime guard. Adds regression tests for each fix: - test_ctx_state_persists_across_retries_multiprocess - test_unpicklable_exception_does_not_hang - test_validate_reports_resolution_failure (updated to assert exit≠0) - test_depends_on_disabled_node_warns - test_resources_undefined_raises (updated for construction-time check) Tests: netrun 1308/1308 (was 1305 + 3 new), netrun-cli 61/61. Samples 14/15/16/17 run cleanly on the fix branch. Co-Authored-By: Claude Opus 4.7 (1M context) <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.
Summary
Implements the full refactor plan in
_dev/refactor_plan.md, Phases 0-5. Touches the execution model, scheduling, pool internals, the CLI, and documentation.Phase 0 — Bug fixes + observability hooks
ExecutionManager.run()now cleans up_worker_jobsinfinallyso timeouts/cancellation don't leak stale entries._async_worker_func._handle_runalways emits a terminal event — uncaught user exceptions no longer hang the manager.netrun validateno longer silently swallows resolution failures.faulthandler.register(SIGUSR1, all_threads=True)at Net start;gc.collect()+ RSS delta tracking between retries.Phase 1 — Shared event loop
asyncioloop per process (daemon thread) replaces per-worker loops. Workers submit coroutines viarun_coroutine_threadsafe().asyncio.Lockall just work — no morenest_asyncioworkarounds.Phase 2 — Scheduling constraints + retry state
ctx.state: per-epoch mutable dict that persists across retries. Cache expensive precomputation so retries don't redo it.NodeExecutionConfig.depends_on: list[str]: directional ordering without data edges. Cycles and unknown nodes raiseValueErrorat Net construction.NodeExecutionConfig.resources: dict[str, int]+NetConfig.resources: named semaphores. Replaces the heavy-pool workaround for mutual exclusion / bounded concurrency / GPU scheduling.Phase 3 — Pool/RPC/EM cleanup
BasePoolfor shared pool lifecycle.send_channelparameter,UP_RUN_STARTED(merged intoUP_RUN_RESPONSE),UP_PRINT_BUFFER,NetProtocolKeysenum.depends_onfor cycles + invalid references.Phase 4 — Config cleanup
Phase 5 — CLI extraction + new commands
netrun-clipackage at the repo root. Same nblite workflow asnetrun. Thenetrunscript is now installed vianetrun-cli's[project.scripts]. Thenetrunpackage no longer ships a CLI orcliextras group.netrun node <name> --edges— show incoming/outgoing edges for a node.netrun structure --format=mermaid— emit a Mermaid topology diagram (renders in GitHub, VSCode, etc.).netrun dry-run— show topological execution order, source/sink nodes, pool assignments, and scheduling constraints (no node code runs).Documentation audit
CLAUDE.md(repo root) for: separatednetrun-clipackage, scheduling constraints,ctx.state, shared event loop, worker crash isolation, removed protocol elements.README.md(repo root) andnetrun/README.mdfor the same.ExecutionManager,Net, andNodeExecutionContext.netrun/PROJECT_SPEC.mdreferences.sample_projects/:15_ctx_state— retry-persistent caching withctx.state.16_depends_on— three nodes with no data edges, ordering enforced bydepends_on.17_resources— three jobs serialized through a 1-slot resource on a 3-worker thread pool (proves serialization via wall-clock timing).Stats
Test plan
cd netrun && uv run pytest src/tests/ -q— all 1,305 tests passcd netrun-cli && uv run pytest src/tests/ -q— all 61 tests pass (50 inherited + 11 new for--edges,--format=mermaid,dry-run)14_web_scraping_pipeline) — comprehensive showcase using all new featuressample_projects/00_basic_net_projectMigration notes
For downstream consumers:
netrun[cli]extras with a separatenetrun-clidependency.resources={"name": 1}; replace ad-hoc retry caches (module-level vars,threading.local) withctx.state; removenest_asyncio.apply()workarounds.Net,NetConfig,NodeExecutionContext, factory protocol,on_epoch_*callbacks,run_until_blocked, output queues, structured logging — all unchanged.🤖 Generated with Claude Code