Skip to content

Fix: fall back to sequential extraction when the process pool cannot start - #3497

Open
curtismu7 wants to merge 1 commit into
Graphify-Labs:v8from
curtismu7:fix/pool-oserror-fallback
Open

Fix: fall back to sequential extraction when the process pool cannot start#3497
curtismu7 wants to merge 1 commit into
Graphify-Labs:v8from
curtismu7:fix/pool-oserror-fallback

Conversation

@curtismu7

Copy link
Copy Markdown
Contributor

Problem

_extract_parallel falls back to sequential extraction on BrokenProcessPool, but an OSError raised while constructing the ProcessPoolExecutor escapes and kills the whole extraction — and with it graphify update.

Seen on macOS: ProcessPoolExecutor.__init__multiprocessing SemLocksem_open fails with OSError: [Errno 28] No space left on device once leaked POSIX named semaphores exhaust the system-wide kern.posix.sem.max (10000). The disk had 41 GiB free, and python -c 'import multiprocessing as m; m.Lock()' failed the same way in any interpreter. graphify update printed [graphify watch] Rebuild failed: [Errno 28] No space left on device and left the graph stale, although sequential extraction needs no semaphore at all.

graphify/extract.py, in _extract_parallel
    with concurrent.futures.ProcessPoolExecutor(max_workers=max_workers) as pool:
concurrent/futures/process.py, in __init__
    self._call_queue = _SafeQueue(...)
multiprocessing/queues.py, in __init__
    self._rlock = ctx.Lock()
multiprocessing/synchronize.py, in __init__
    sl = self._semlock = _multiprocessing.SemLock(...)
OSError: [Errno 28] No space left on device

(Traceback from 0.8.37; the same unguarded path is on v8.)

Fix

A second handler after the BrokenProcessPool one: except OSError prints a warning naming the error and returns False, so extract() runs _extract_sequential — the same contract the existing handler uses. It's a separate branch because the BrokenProcessPool warning's Windows __main__ hint would be wrong here.

Test

test_extract_parallel_returns_false_when_pool_cannot_start patches ProcessPoolExecutor to raise OSError(ENOSPC) and asserts _extract_parallel returns False and warns. It imports concurrent.futures.process explicitly: that submodule loads lazily on first ProcessPoolExecutor access, so with the class patched out, the BrokenProcessPool handler's attribute lookup would itself raise AttributeError and the test would fail for the wrong reason.

  • The new test fails before the fix with the OSError, and passes after.
  • tests/test_extract.py: 226 passed, 4 skipped (baseline 225 plus the new test).
  • tests/test_hooks.py: 107 passed.
  • ruff check (v0.15.14, repo config) on both files: clean.

No CHANGELOG entry, since entries cite the PR number — happy to add one if you'd like.

🤖 Generated with Claude Code

_extract_parallel falls back to sequential on BrokenProcessPool, but an
OSError raised while constructing the ProcessPoolExecutor escaped and
killed the whole extraction (and `graphify update`).

Seen on macOS: ProcessPoolExecutor -> multiprocessing SemLock -> sem_open
fails with OSError(ENOSPC), "No space left on device", once leaked POSIX
named semaphores exhaust kern.posix.sem.max — with the disk nowhere near
full. Sequential extraction needs no semaphore, so hand back False the
same way the BrokenProcessPool branch does, with a warning that names the
OS error rather than the Windows __main__ hint.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

Formal verification. No changes could be formally verified in this run.


Graphify review — findings

Makes _extract_parallel catch OSError at pool construction and fall back to sequential extraction with a warning, instead of aborting the whole run — this covers the macOS case where leaked POSIX semaphores exhaust kern.posix.sem.max and sem_open fails with ENOSPC. Adds a test that patches ProcessPoolExecutor to raise ENOSPC and asserts the function returns False and names the OS error in its warning.

No blocking issues surfaced.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2136 functions depend on the 653 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 585 callers, 44 callees
  • new: _rebuild_code() — 115 callers, 51 callees
  • new: extract_js() — 85 callers, 4 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: dispatch_command() — 2 callers, 124 callees
  • new: _get_extractor() — 26 callers, 6 callees
  • new: run_pipeline() — 8 callers, 13 callees
  • new: collect_files() — 17 callers, 6 callees
  • …and 29 more — each is listed as a finding

Verification — 2136 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 1965 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

106 of 268 test file(s) selected (40%) via static blast radius.

  • tests/test_astro_extraction.py — impact
  • tests/test_astro_import_ids.py — impact
  • tests/test_build.py — impact
  • tests/test_builtin_global_type_refs.py — impact
  • tests/test_case_sensitive_resolution.py — impact
  • tests/test_cjs_module_extension.py — impact
  • tests/test_cpp_nested_and_cli.py — impact
  • tests/test_cpp_objc_cross_file_calls.py — impact
  • tests/test_cross_extension_reexport_self_cycle.py — impact
  • tests/test_cross_language_call_resolution.py — impact
  • tests/test_cross_repo_member_calls.py — impact
  • tests/test_csharp_call_site_generic_args.py — impact
  • tests/test_csharp_enum_members.py — impact
  • tests/test_csharp_field_generic_args.py — impact
  • tests/test_csharp_generic_callsites.py — impact
  • tests/test_csharp_interface_dispatch.py — impact
  • tests/test_csharp_member_calls.py — impact
  • tests/test_csharp_member_nodes.py — impact
  • tests/test_csharp_object_creation.py — impact
  • tests/test_csharp_partial_classes.py — impact
  • tests/test_csharp_type_resolution.py — impact
  • tests/test_definition_file_portability.py — impact
  • tests/test_detect.py — impact
  • tests/test_dotnet.py — impact
  • tests/test_duplicate_annotation_edges.py — impact
  • tests/test_extract.py — impact, changed-test
  • tests/test_extract_cache_location.py — impact
  • tests/test_file_label_disambiguation.py — impact
  • tests/test_file_node_id_spec.py — impact
  • tests/test_forwarding_review_findings.py — impact
  • tests/test_go_builtin_call_targets.py — impact
  • tests/test_go_qualified_resolution.py — impact
  • tests/test_import_extension_resolution.py — impact
  • tests/test_import_self_loops.py — impact
  • tests/test_imported_export_forwarding.py — impact
  • tests/test_incremental.py — impact
  • tests/test_indirect_call_arrow_single_param_shadow.py — impact
  • tests/test_indirect_call_catch_binding_shadow.py — impact
  • tests/test_indirect_call_external_import_shadow.py — impact
  • tests/test_indirect_call_for_of_binding_shadow.py — impact
  • tests/test_indirect_call_function_expression_shadow.py — impact
  • tests/test_indirect_call_nested_closure_shadow.py — impact
  • tests/test_indirect_dispatch.py — impact
  • tests/test_indirect_dispatch_assign_return.py — impact
  • tests/test_indirect_dispatch_getattr.py — impact
  • tests/test_inferred_confidence_rubric.py — impact
  • tests/test_inherited_field_receivers.py — impact
  • tests/test_issue_3405_python_resolution.py — impact
  • tests/test_java_member_calls.py — impact
  • tests/test_java_type_resolution.py — impact
  • … and 56 more

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

Formal verification

Could not verify: Could not verify \_extract\_parallel.

The verifier did not have enough to check \_extract\_parallel, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set

· 37 more finding(s) on lines outside this diff (see the check run).

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