Skip to content

#3913: Full Suite Gate out of its timeout: small scans skip the worker pool, and Windows/macOS scans now match Linux - #3916

Merged
squid-protocol merged 4 commits into
mainfrom
claude/ci-spawn-overhead
Sep 27, 2026
Merged

squid-protocol merged 4 commits into
mainfrom
claude/ci-spawn-overhead

Conversation

@squid-protocol

@squid-protocol squid-protocol commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Fixes #3913. Follow-up for the per-process import cost: #3914.

What was wrong

The Full Suite Gate's Windows jobs had reached their 30-minute limit. On #3901's own run, Windows 3.9 finished in 29 min 58 s, and on main's run 36335845364 both Windows jobs were cancelled at 98%.

Almost none of that time was testing:

  • About 150 test fixtures scan a 3–6 file estate. Every scan started cpu_count - 1 worker processes, whatever its file count.
  • Under the spawn start method, which Windows and macOS use, each worker re-imports GitGalaxy. That takes about 3 s, mostly compiling the language registry.
  • Meanwhile xdist was running 4 such tests at once on a 4-core runner.

A one-file scan measured on Linux:

Start method Wall time
fork 3.5 s
spawn 9.4 s

The extraction itself took 0.06 s.

Changes

  • galaxyscope: no pool for a small scan.
    • It never starts more workers than files.
    • A scan of 16 files or fewer, or one run with GALAXYSCOPE_MAX_WORKERS=1, runs the same worker code in-process through _InlineExecutor.
    • The in-process path keeps what the process boundary gave:
      • the worker's arguments are copies;
      • the root log level and the SIGALRM ReDoS-fuse handler are restored;
      • exceptions arrive through a Future;
      • it runs on the main thread only.
    • A one-file scan under spawn drops from 9.4 s to 3.4 s.
  • tests/conftest.py: under xdist, GALAXYSCOPE_MAX_WORKERS=1, because xdist already puts a test on every core.
  • full-suite-gate.yml: timeout-minutes 30 → 45, as headroom only.

A real engine bug the new equivalence test found

detector.py decided whether to record call qualifiers (#3329) with invocation_pattern is p for p in QUALIFIED_CALLS_OUT_PATTERNS.

  • A spawned worker receives its rules pickled, so each pattern arrives as a new object and the is check never matched.
  • Result: every Windows and macOS scan silently dropped calls_out_qualifiers. On the test estate, 20 of 21 function rows differed from Linux.
  • The golden masters run only on Linux, so nothing caught it.
  • The family is now matched by the pattern's text.
  • A forced-spawn scan now equals fork, which equals in-process, and a Linux test forces a spawned pool so this stays caught.

Two timing-sensitive tests, root-caused

  • test_production_regex_redos_immunity called any 0.25 s gap in its status queue a hang.
    • A busy runner makes that gap by itself. It blamed a different innocent regex each run: dart::func_start on main's macOS, then cpp::func_start here.
    • The same rule also passed the test having checked nothing, whenever no worker had started within 0.25 s.
    • Now a regex is flagged after running for 10 s, and a stalled or incomplete run fails.
    • A new test proves the check still catches ^(a+)+$.
  • test_a_redirect_is_refused_so_the_key_is_not_forwarded answered without reading the POST body.
    • On Windows, closing a socket with unread data sends a reset, which can arrive before the 302 (WinError 10053).
    • The handler now reads the body first.

Measured (Full Suite Gate, job wall time)

Job Before (run 36340546823, #3901) After (run 36346298642)
windows-latest 3.9 29 m 58 s (pytest 1648 s) 16 m 31 s
windows-latest 3.12 17 m 26 s 11 m 54 s (pytest 712 s)
macos-latest 3.9 27 m 44 s 9 m 20 s
macos-latest 3.12 19 m 38 s 8 m 47 s
ubuntu-latest (4 jobs) 5–10 m 5–10 m (unchanged)

The slowest job went from about 30 minutes to 16.5, so the 45-minute limit leaves room. Before this PR, the Windows 3.9 --durations list was led by the gauntlet oracle test at 318 s and about 20 mainframe fixtures at 38–47 s each. Now no mainframe fixture exceeds 23 s on Windows. Locally (4-core Linux, -n auto) the suite runs in 256 s, where the pre-push gates took 919 s earlier today.

Verification

  • Golden masters are byte-identical in both modes. I checked twice:

    • with the whole crucible corpus forced through the in-process path (GALAXYSCOPE_MAX_WORKERS=1);
    • after the detector fix.
  • audit_check.py: all clear.

  • Full suite locally: 12,666 passed.

  • New tests (tests/core_engine/test_inline_extraction.py):

    • in-process rows equal the pool's on a 21-file estate;
    • a spawned pool equals in-process on any OS;
    • a pickled rule is still recognised as the qualified-call family;
    • the executor's isolation guarantees hold.

    The spawn and pickling tests fail without the detector fix.

🤖 Generated with Claude Code

https://claude.ai/code/session_01X9PBcL8evG7CEzff1zE8v5

A one-file scan took 3.5 s under fork but 9.4 s under spawn (Windows, macOS):
every ProcessPoolExecutor worker re-imports GitGalaxy (~3 s) to extract files
that take milliseconds, and galaxyscope always started cpu_count-1 of them.
The Full Suite Gate's Windows jobs reached their 30-minute limit on ~150 such
test scans (mainframe fixtures at 38-47 s each, the gauntlet oracle at 318 s).

- galaxyscope: never more workers than files; a scan of <= 16 files, or with
  GALAXYSCOPE_MAX_WORKERS=1, runs the same worker code in-process through
  _InlineExecutor (copied arguments, root log level and SIGALRM handler
  restored, exceptions via Future, main thread only). 1-file spawn scan:
  9.4 s -> 3.4 s.
- tests/conftest.py: under xdist, GALAXYSCOPE_MAX_WORKERS=1 (xdist already
  fills the cores).
- full-suite-gate.yml: timeout 30 -> 45 minutes, as headroom.
- test_inline_extraction.py: in-process rows equal the pool's on a 21-file
  estate; the executor's isolation guarantees.

Follow-up for the per-process import cost: #3914.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X9PBcL8evG7CEzff1zE8v5
@squid-protocol squid-protocol added the core-engine Modifications to the central physics and parsing engine label Sep 27, 2026 — with Claude
@github-actions

Copy link
Copy Markdown
Contributor

🐦‍⬛ Muninn Security Scan

✅ No security issues found.

🐦‍⬛ Powered by Muninn · Skald Lab

detector.py decided whether to record call qualifiers (#3329) with
`invocation_pattern is p for p in QUALIFIED_CALLS_OUT_PATTERNS`. Under the
`spawn` start method (Windows, macOS) a scan worker's rules arrive pickled,
so the pattern is a new object, `is` never matched, and every spawned scan
silently dropped calls_out_qualifiers -- Windows/macOS output differed from
Linux's (and the golden masters, which only run on Linux). The Windows and
macOS Full Suite jobs on this branch caught it through the pool-vs-inline
equivalence test: 20 of 21 function rows differed.

Now matched by the pattern's text. Forced spawn == fork == in-process on the
test estate; new tests force a spawned pool on any OS and check a pickled
rule is still recognised (both fail without the fix).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X9PBcL8evG7CEzff1zE8v5
…0.25 s of queue silence

test_production_regex_redos_immunity called any 0.25 s gap in its status queue
a hang. On a busy 4-core runner (8 spawned fuzzers importing the registry
beside 3 other xdist workers) that gap happens by itself: it blamed a
different innocent regex each run (dart::func_start on main's macOS, then
cpp::func_start on this PR's ubuntu 3.10). The same rule passed having
checked nothing when no worker had started within 0.25 s.

- a regex is flagged once it has run HANG_SECONDS (10 s) over the payloads,
  checked every loop pass, not only on silence;
- a compile error still reports DONE (it is not a hang);
- the test fails when the fuzzer stalls or checks fewer regexes than it
  collected;
- new test: the kill-switch still catches ^(a+)+$.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X9PBcL8evG7CEzff1zE8v5
…ying

test_a_redirect_is_refused_so_the_key_is_not_forwarded's handler answered
302 without reading the POST body, then the server closed the socket. On
Windows, closing a socket with unread data sends a reset, which can reach
the client before the response: ConnectionAbortedError (WinError 10053) on
this PR's windows-latest 3.12 job. Reading Content-Length bytes first makes
the close orderly on every OS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X9PBcL8evG7CEzff1zE8v5
@squid-protocol squid-protocol changed the title #3913: extract small scans in-process instead of spawning a worker pool #3913: Full Suite Gate out of its timeout: small scans skip the worker pool, and Windows/macOS scans now match Linux Sep 27, 2026
@squid-protocol
squid-protocol merged commit c69bea3 into main Sep 27, 2026
44 checks passed
@squid-protocol
squid-protocol deleted the claude/ci-spawn-overhead branch September 27, 2026 20:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core-engine Modifications to the central physics and parsing engine

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Full Suite Gate: Windows at its 30-minute limit, because every tiny test scan spawns worker processes that re-import GitGalaxy

2 participants