Skip to content

Fix Linux pytest shutdown with explicit Julia signal handling - #818

Open
MilesCranmerBot wants to merge 4 commits into
JuliaPy:mainfrom
MilesCranmerBot:fix-juliacall-shutdown
Open

Fix Linux pytest shutdown with explicit Julia signal handling#818
MilesCranmerBot wants to merge 4 commits into
JuliaPy:mainfrom
MilesCranmerBot:fix-juliacall-shutdown

Conversation

@MilesCranmerBot

Copy link
Copy Markdown
Contributor

Problem

On Linux, pytest's faulthandler plugin can replace Julia's signal handlers during teardown. In a PySR notebook reproduction, all seven cells passed, then the pytest subprocess exited with SIGSEGV inside jl_atexit_hook.

A native debugger trace showed the ownership sequence: CPython installed its SIGSEGV handler, Julia installed its safepoint handler, then faulthandler.disable() restored CPython's saved SIG_DFL. A Julia worker subsequently faulted on a GC safepoint page during shutdown. The shutdown path is unchanged between v0.9.35 and the upstream base of this PR.

Change

Add a small pytest entry-point plugin that blocks pytest's faulthandler plugin before configuration when both conditions hold:

  • The platform is Linux.
  • Julia signal handling is explicitly set to yes, using -X juliacall-handle-signals before PYTHON_JULIACALL_HANDLE_SIGNALS.

The entry point lives outside the juliacall package so pytest startup does not initialize Julia. Merely installing JuliaCall does not activate the policy. A warning explains the diagnostic tradeoff, and -p no:juliacall opts out.

Pytest-managed Python fatal-error and timeout tracebacks are unavailable under this policy. A positive faulthandler_timeout combined with faulthandler_exit_on_timeout raises a configuration error instead of silently removing timeout enforcement. Malformed boolean configuration still fails. Pytest versions without the exit-on-timeout option remain supported.

This only coordinates pytest's diagnostic plugin. Arbitrary faulthandler.disable() calls or native handler replacement while Julia is running can still disrupt Julia's signals. Julia fatal diagnostics remain available.

Related to #764 and #763. This PR does not bypass jl_atexit_hook, Julia finalizers or user atexit hooks, and does not claim to fix every possible cause of #764.

Verification

Tested a built and installed wheel on Linux x86-64 with Python 3.13.11, Julia 1.12.7 and SymbolicRegression 2.4.1:

  • Existing Python test suite plus regression tests: 21 passed.
  • Pytest 8.3.0 configuration checks: 8 passed, including the missing-option path. Pytest 9.1.1 checks cover option precedence, opt-out, unconfigured behavior and no eager Julia initialization.
  • Fixed six-thread fresh-process shutdown campaign: 20/20 exited zero, with Python atexit, Julia atexit and Julia finalizer markers present in every process.
  • Unchanged PySR notebook test: 3/3 passed, seven cells each, without PYTEST_ADDOPTS or a -p no:faulthandler workaround.
  • Complete PySR main,cli,startup,interrupt suite: 204 passed. Four independently executed shards each passed 51 tests. The unchanged preservation report confirmed all original test IDs and skips were preserved and the shards partitioned the full suite exactly.
  • Fresh JAX/Torch auxiliary suite: 15 passed.
  • An intentional native SIGSEGV under pytest exited -11 and printed Julia's fatal diagnostic/backtrace.
  • Timeout enforcement conflict exited 4; malformed boolean configuration exited 1. Zero timeout and diagnostic-only timeout configurations passed.

The configuration regression failed on the published wheel before the integration. Final campaigns ran sequentially against a separately provisioned project using documented executable/project overrides. Unchanged PySR tests added their optional dependencies; the developed PythonCall path/version and SymbolicRegression version stayed unchanged.

To exercise the installed-wheel regression from this checkout:

uv build --wheel
uv venv --python 3.13.11 .venv
uv pip install --python .venv/bin/python dist/juliacall-0.9.35-py3-none-any.whl pytest numpy
PYTHON_JULIACALL_HANDLE_SIGNALS=yes PYTHON_JULIACALL_THREADS=6 \
  uv run --no-project --python .venv/bin/python python -m pytest \
  pytest/test_all.py pytest/test_signals.py

With PySR and its development dependencies installed in the verification environment, the unchanged consumer commands were:

uv run --no-project --python .venv/bin/python python -X faulthandler -m pysr test startup -k test_notebook
uv run --no-project --python .venv/bin/python python -m pysr test main,cli,startup,interrupt
uv run --no-project --python .venv/bin/python python -m pysr test jax,torch

The final campaign used six Julia threads and Julia signal handling enabled. No PySR source, assertion, skip, search budget or test timeout was changed by this PR.

MilesCranmerBot and others added 3 commits September 12, 2026 02:12
Co-authored-by: Miles Cranmer <miles.cranmer@gmail.com>
Co-authored-by: Miles Cranmer <miles.cranmer@gmail.com>
Co-authored-by: Miles Cranmer <miles.cranmer@gmail.com>
@MilesCranmer

MilesCranmer commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Can confirm I believe this is a valid fix to some of the issues I've been seeing in PySR CI jobs. Seems like pytest and juliacall are fighting at shutdown for some reason. Not sure if this is also fixes #764 but perhaps it might.

Co-authored-by: Miles Cranmer <miles.cranmer@gmail.com>
@cjdoris

cjdoris commented Sep 14, 2026

Copy link
Copy Markdown
Member

This feels like quite a magical/intrusive PR. I'm not totally opposed to it but is there any reason the user cannot just do pytest -p no:faulthandler themselves? We can document this in the FAQs.

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.

3 participants