Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/nightly-chaos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,10 @@ jobs:
" || true
- name: Run Python worker benchmarks (sweep, jitter, rescue)
timeout-minutes: 15
env:
# A native crash here prints no Python frame on its own; #434 was
# diagnosable only from the shell's "Segmentation fault" line.
PYTHONFAULTHANDLER: "1"
run: |
set -o pipefail
PYTHONPATH=scripts .venv/bin/python scripts/benchmark_runtime.py --scenario workers \
Expand Down
28 changes: 28 additions & 0 deletions awa-python/scripts/benchmark_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import argparse
import asyncio
import os
import sys
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from statistics import quantiles
Expand Down Expand Up @@ -1074,9 +1075,36 @@ def parse_args() -> argparse.Namespace:
return parser.parse_args()


def _flush_std_streams() -> None:
"""Flush stdout and stderr, tolerating an already-closed stream.

`os._exit` skips stdio, this script uses buffered output rather than
`flush=True` per print, and the nightly tees stdout into the artifact the
regression checker reads — so without this the artifact comes out empty.

A raising flush would propagate out of `main` and hand control back to
normal interpreter finalization, which is the crash path the caller is
avoiding. Mirrors `_flush_std_streams` in `tests/_subprocess_exit.py`;
duplicated because that module lives outside this script's import path.
"""
for stream in (sys.stdout, sys.stderr):
try:
stream.flush()
except (OSError, ValueError):
pass


def main() -> None:
asyncio.run(async_main(parse_args()))

# Exit before the interpreter finalizer phase: the pyo3-async-runtimes
# tokio runtime is process-global and its outstanding tasks hold Python
# references, so finalization can SIGSEGV after correct output (#434,
# #228, PyO3/pyo3#1415). `tests/_subprocess_exit.py` does the same for
# the chaos helpers.
_flush_std_streams()
os._exit(0)

Comment thread
coderabbitai[bot] marked this conversation as resolved.

if __name__ == "__main__":
main()
18 changes: 18 additions & 0 deletions awa-python/tests/_subprocess_exit.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
import asyncio
import os
import signal
import sys
from collections.abc import Awaitable, Callable
from types import FrameType
from typing import NoReturn


def _flush_std_streams() -> None:
"""Flush stdout and stderr before a forced exit, tolerating a closed stream.

`os._exit` skips stdio, so callers may rely on this for their final
flush rather than passing `flush=True` on every print. A raising flush
is swallowed: letting it propagate would hand control back to normal
interpreter finalization, which is what the caller is avoiding.
"""
for stream in (sys.stdout, sys.stderr):
try:
stream.flush()
except Exception:
pass


def _exit_from_signal(signum: int, _frame: FrameType | None) -> NoReturn:
_flush_std_streams()
os._exit(128 + signum)


Expand All @@ -18,4 +35,5 @@ def install_exit_without_finalizers_on_signals() -> None:
def run_async_main_without_finalizers(main: Callable[[], Awaitable[None]]) -> NoReturn:
install_exit_without_finalizers_on_signals()
asyncio.run(main())
_flush_std_streams()
os._exit(0)