Skip to content

Commit 6d92aec

Browse files
committed
diag: probe Windows wheel shutdown crash (cp313, subprocess tally)
1 parent 43e15e5 commit 6d92aec

2 files changed

Lines changed: 104 additions & 6 deletions

File tree

.github/workflows/python-wheels.yml

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,10 @@ jobs:
7575
VCPKG_ROOT: ${{ github.workspace }}/vcpkg
7676
VCPKG_DEFAULT_TRIPLET: x64-windows-dynamic-release
7777
CMAKE_TOOLCHAIN_FILE: ${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake
78-
CIBW_BUILD: "*-win_amd64"
78+
CIBW_BUILD: "cp313-win_amd64"
7979
CIBW_TEST_REQUIRES: pytest adbc_driver_manager geoarrow-pyarrow geopandas
80-
# Test command exits 0 even on failure so CIBW continues building all wheels;
81-
# failures are recorded in .test_failed and checked after upload.
82-
# We use Python here to be absolutely sure there are no shell escaping issues.
83-
CIBW_TEST_COMMAND: >-
84-
python -c "import subprocess,sys,pathlib; proj=pathlib.Path(r'{project}'); r=subprocess.run([sys.executable,'-m','pytest',str(pathlib.Path(r'{package}')/'tests'),'-vv','-o','faulthandler_timeout=600']); r.returncode and (print(f'Tests failed (exit {r.returncode}), creating .test_failed marker at {proj.absolute()}') or (proj/'.test_failed').touch() or True); sys.exit(0)"
80+
# DIAGNOSTIC: run the shutdown-crash probe instead of pytest.
81+
CIBW_TEST_COMMAND: "python {project}/ci/scripts/win_teardown_probe.py"
8582

8683
- uses: actions/upload-artifact@v7
8784
with:

ci/scripts/win_teardown_probe.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
"""Diagnostic probe for the Windows wheel shutdown crash.
19+
20+
The Windows wheel job passes every pytest test, then a random subset of
21+
interpreters aborts at *process exit* with 0xC0000409 (Windows __fastfail --
22+
how a Rust abort / corrupted-teardown surfaces). This harness pins down which
23+
teardown path triggers it by running many short-lived subprocesses per
24+
scenario and tallying their exit codes. Each subprocess exercises one teardown
25+
shape (bare import, one connect, connect+query, drop-before-exit, os._exit,
26+
many connects); comparing crash rates across scenarios isolates the cause
27+
without needing a symbolized dump.
28+
29+
Always exits 0 -- read the printed tally from the CI log.
30+
"""
31+
32+
import subprocess
33+
import sys
34+
35+
FASTFAIL = 3221226505 # 0xC0000409, STATUS_STACK_BUFFER_OVERRUN / __fastfail
36+
37+
# Each scenario is a snippet run in its own fresh interpreter. The crash is at
38+
# interpreter finalization, so every repetition must be a separate process.
39+
SCENARIOS = {
40+
# Static/global teardown only: import auto-configures PROJ + GDAL.
41+
"import_only": "import sedonadb",
42+
# One per-session Tokio runtime built, then normal interpreter shutdown.
43+
"connect": "import sedonadb; sedonadb.connect()",
44+
# Runtime actually drives a query before shutdown.
45+
"connect_query": (
46+
"import sedonadb; sd = sedonadb.connect();"
47+
" sd.sql('SELECT 1').to_arrow_table()"
48+
),
49+
# Drop the context (runs the RuntimeHandle janitor) *before* finalization.
50+
"connect_del_gc": (
51+
"import sedonadb, gc; sd = sedonadb.connect(); del sd; gc.collect()"
52+
),
53+
# Skip CPython finalization + static teardown entirely.
54+
"connect_osexit": (
55+
"import sedonadb, os; sedonadb.connect(); os._exit(0)"
56+
),
57+
# Many runtimes alive at once, mirroring a suite that opens many sessions.
58+
"many_connects": (
59+
"import sedonadb; ctxs = [sedonadb.connect() for _ in range(8)]"
60+
),
61+
}
62+
63+
REPS = 40
64+
65+
66+
def run_scenario(name, snippet):
67+
codes = {}
68+
first_crash_stderr = None
69+
for _ in range(REPS):
70+
proc = subprocess.run(
71+
[sys.executable, "-c", snippet],
72+
capture_output=True,
73+
text=True,
74+
)
75+
rc = proc.returncode
76+
codes[rc] = codes.get(rc, 0) + 1
77+
if rc != 0 and first_crash_stderr is None:
78+
first_crash_stderr = proc.stderr[-2000:]
79+
crashes = sum(n for rc, n in codes.items() if rc != 0)
80+
print(f"[{name}] {crashes}/{REPS} nonzero exits codes={codes}")
81+
if first_crash_stderr:
82+
print(f" first-crash stderr tail:\n{first_crash_stderr}")
83+
return name, crashes
84+
85+
86+
def main():
87+
print(f"python: {sys.version}")
88+
print(f"executable: {sys.executable}")
89+
print(f"reps per scenario: {REPS}\n")
90+
results = []
91+
for name, snippet in SCENARIOS.items():
92+
results.append(run_scenario(name, snippet))
93+
print("\n=== SUMMARY (fastfail=0xC0000409) ===")
94+
for name, crashes in results:
95+
print(f" {name:16s} {crashes}/{REPS}")
96+
# Never fail the job: this is a diagnostic, the signal is the tally above.
97+
sys.exit(0)
98+
99+
100+
if __name__ == "__main__":
101+
main()

0 commit comments

Comments
 (0)