Fix: fall back to sequential extraction when the process pool cannot start - #3497
Fix: fall back to sequential extraction when the process pool cannot start#3497curtismu7 wants to merge 1 commit into
Conversation
_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>
There was a problem hiding this comment.
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— impacttests/test_astro_import_ids.py— impacttests/test_build.py— impacttests/test_builtin_global_type_refs.py— impacttests/test_case_sensitive_resolution.py— impacttests/test_cjs_module_extension.py— impacttests/test_cpp_nested_and_cli.py— impacttests/test_cpp_objc_cross_file_calls.py— impacttests/test_cross_extension_reexport_self_cycle.py— impacttests/test_cross_language_call_resolution.py— impacttests/test_cross_repo_member_calls.py— impacttests/test_csharp_call_site_generic_args.py— impacttests/test_csharp_enum_members.py— impacttests/test_csharp_field_generic_args.py— impacttests/test_csharp_generic_callsites.py— impacttests/test_csharp_interface_dispatch.py— impacttests/test_csharp_member_calls.py— impacttests/test_csharp_member_nodes.py— impacttests/test_csharp_object_creation.py— impacttests/test_csharp_partial_classes.py— impacttests/test_csharp_type_resolution.py— impacttests/test_definition_file_portability.py— impacttests/test_detect.py— impacttests/test_dotnet.py— impacttests/test_duplicate_annotation_edges.py— impacttests/test_extract.py— impact, changed-testtests/test_extract_cache_location.py— impacttests/test_file_label_disambiguation.py— impacttests/test_file_node_id_spec.py— impacttests/test_forwarding_review_findings.py— impacttests/test_go_builtin_call_targets.py— impacttests/test_go_qualified_resolution.py— impacttests/test_import_extension_resolution.py— impacttests/test_import_self_loops.py— impacttests/test_imported_export_forwarding.py— impacttests/test_incremental.py— impacttests/test_indirect_call_arrow_single_param_shadow.py— impacttests/test_indirect_call_catch_binding_shadow.py— impacttests/test_indirect_call_external_import_shadow.py— impacttests/test_indirect_call_for_of_binding_shadow.py— impacttests/test_indirect_call_function_expression_shadow.py— impacttests/test_indirect_call_nested_closure_shadow.py— impacttests/test_indirect_dispatch.py— impacttests/test_indirect_dispatch_assign_return.py— impacttests/test_indirect_dispatch_getattr.py— impacttests/test_inferred_confidence_rubric.py— impacttests/test_inherited_field_receivers.py— impacttests/test_issue_3405_python_resolution.py— impacttests/test_java_member_calls.py— impacttests/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).
Problem
_extract_parallelfalls back to sequential extraction onBrokenProcessPool, but anOSErrorraised while constructing theProcessPoolExecutorescapes and kills the whole extraction — and with itgraphify update.Seen on macOS:
ProcessPoolExecutor.__init__→multiprocessingSemLock→sem_openfails withOSError: [Errno 28] No space left on deviceonce leaked POSIX named semaphores exhaust the system-widekern.posix.sem.max(10000). The disk had 41 GiB free, andpython -c 'import multiprocessing as m; m.Lock()'failed the same way in any interpreter.graphify updateprinted[graphify watch] Rebuild failed: [Errno 28] No space left on deviceand left the graph stale, although sequential extraction needs no semaphore at all.(Traceback from 0.8.37; the same unguarded path is on
v8.)Fix
A second handler after the
BrokenProcessPoolone:except OSErrorprints a warning naming the error and returnsFalse, soextract()runs_extract_sequential— the same contract the existing handler uses. It's a separate branch because theBrokenProcessPoolwarning's Windows__main__hint would be wrong here.Test
test_extract_parallel_returns_false_when_pool_cannot_startpatchesProcessPoolExecutorto raiseOSError(ENOSPC)and asserts_extract_parallelreturnsFalseand warns. It importsconcurrent.futures.processexplicitly: that submodule loads lazily on firstProcessPoolExecutoraccess, so with the class patched out, theBrokenProcessPoolhandler's attribute lookup would itself raiseAttributeErrorand the test would fail for the wrong reason.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