Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
YouTube Security VideosAndroid Police: Samsung is smashing records! #shorts #tech #phones(21.09.2026 um 13:55 Uhr)
YouTube Security Videosheise & c't: Bundesnetzagentur wollte diesen Futterautomaten verbieten(21.09.2026 um 13:53 Uhr)
YouTube Security VideosNeil Patel: Your Google Traffic Isn't An Asset It's A Loan #shorts(21.09.2026 um 14:05 Uhr)
Windows Tipps & SecurityF-14 A Tomcat Top Gun endlich als Revell Klemmbausteinmodell erhältlich(21.09.2026 um 14:27 Uhr)
Sichere ProgrammierungShow the Hand-Back Sample Before Approving an Agent Score(21.09.2026 um 14:15 Uhr)
Sichere ProgrammierungHybrid retrieval in one Postgres query: RRF over tsvector + pgvector(21.09.2026 um 14:15 Uhr)
YouTube Security VideosAndroid Police: Samsung is smashing records! #shorts #tech #phones(21.09.2026 um 13:55 Uhr)
YouTube Security Videosheise & c't: Bundesnetzagentur wollte diesen Futterautomaten verbieten(21.09.2026 um 13:53 Uhr)
YouTube Security VideosNeil Patel: Your Google Traffic Isn't An Asset It's A Loan #shorts(21.09.2026 um 14:05 Uhr)
Windows Tipps & SecurityF-14 A Tomcat Top Gun endlich als Revell Klemmbausteinmodell erhältlich(21.09.2026 um 14:27 Uhr)
Sichere ProgrammierungShow the Hand-Back Sample Before Approving an Agent Score(21.09.2026 um 14:15 Uhr)
Sichere ProgrammierungHybrid retrieval in one Postgres query: RRF over tsvector + pgvector(21.09.2026 um 14:15 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

I Replaced pytest-xdist With 60 Lines of subprocess.Popen. Here's Why.

Our test suite had a flake we couldn't pin down. A test would pass alone, pass in its own file, then fail when the full suite ran. Re-run it and it passed again. Classic state leak. The culprit turned out to be the thing we thought was…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

Our test suite had a flake we couldn't pin down. A test would pass alone, pass in its own file, then fail when the full suite ran. Re-run it and it passed again. Classic state leak.



The culprit turned out to be the thing we thought was protecting us: pytest-xdist.






The Problem



xdist spreads tests across persistent worker processes. Those workers stay alive and pick up file after file. That is great for spawn overhead and terrible for isolation. Any module-level dict, any ContextVar, any singleton that one test file mutates rides along into the next file that lands on the same worker.



We had ~17k tests across ~850 files. The state that leaked was almost always module-level: a registry populated at import time, a cache that never got reset, an env-var read once and memoized. None of it was the test author's bug. It was the worker being reused.



You can chase this with fixtures forever. We tried. autouse reset fixtures, monkeypatched globals, teardown hooks. Every one of them is a patch over the real issue: the interpreter is shared when it shouldn't be.






The Fix



Give every test file its own fresh Python interpreter. Run one python -m pytest <file> per file, with bounded parallelism. No persistent workers, no shared process, nothing to leak through.



The whole pool is a ThreadPoolExecutor handing out subprocess launches:




from concurrent.futures import ThreadPoolExecutor
import os, subprocess, sys, time

def run_one_file(file, pytest_args, repo_root, file_timeout):
cmd = [sys.executable, "-m", "pytest", str(file), *pytest_args]
proc = subprocess.Popen(
cmd,
cwd=repo_root,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
env=os.environ,
# POSIX: head of its own process group so we can kill the
# whole tree atomically. Windows: maps to CREATE_NEW_PROCESS_GROUP.
start_new_session=True,
)
try:
output, _ = proc.communicate(timeout=file_timeout)
return file, proc.returncode, output
except subprocess.TimeoutExpired:
kill_tree(proc)
return file, 124, "(file timeout; process tree killed)"

with ThreadPoolExecutor(max_workers=os.cpu_count()) as pool:
futures = [pool.submit(run_one_file, f, args, root, 300) for f in files]
# ThreadPoolExecutor.__exit__ blocks until all are done






That's the core. A semaphore-gated Popen pool in about 60 lines, versus xdist's loadfile/loadscope modes, --max-worker-restart, and an internal control plane we didn't need.






Why Per-File and Not Per-Test



The obvious question: if isolation is the goal, why not one process per test?



Because the math kills you. Process spawn is roughly 250ms. Per test that is 250ms x 17,000 = ~70 minutes of pure spawn overhead before a single assertion runs. Per file it is 250ms x 850 = ~3.5 minutes, which fits the CI budget.



And per-file buys you the isolation boundary that actually matters. Cross-file module state was the entire flake source. Intra-file state is the test author's responsibility, and a fresh interpreter per file draws the line exactly where the bug lived.






Gotchas



The part that bit us hardest was killing timed-out processes. A test can spawn a uvicorn server or an async runtime as a grandchild. proc.kill() only takes out the immediate child. The grandchildren reparent to PID 1 on Linux, or get adopted by services.exe on Windows, and they leak, holding ports and eventually starving the runner.



You have to kill the whole tree. And you have to capture the process group id before the leader exits:




pgid = None
if sys.platform != "win32":
try:
pgid = os.getpgid(proc.pid) # capture NOW
except (ProcessLookupError, PermissionError):
pgid = None






Here is the trap. Once the leader process is reaped, os.getpgid(proc.pid) raises ProcessLookupError even though grandchildren in that group are still alive. If you wait until cleanup time to look up the pgid, it is already gone and you kill nothing. Capture it right after Popen, then SIGKILL the group:




def kill_tree(proc, pgid=None):
if proc.pid is None:
return
if sys.platform == "win32":
# taskkill walks the recorded ppid chain, works after the root exits
subprocess.run(["taskkill", "/F", "/T", "/PID", str(proc.pid)],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=10)
else:
os.killpg(pgid, signal.SIGKILL) # atomic, whole group






We reached for psutil first. It does not help here: in the happy path the root is already reaped, so psutil.Process(pid) can't find it, and grandchildren reparented to PID 1 aren't reachable by a tree walk either. The platform-native primitives (process groups on POSIX, taskkill /F /T on Windows) handle both the alive-root and dead-root cases without the extra dependency.



The other cost is honest: per-file spawn is slower than a warm xdist worker on a single file. We ate that on purpose. A test suite you can trust in 3.5 minutes beats a flaky one in 2.






One More Thing: Retry the File, Not the Test



Fresh interpreters cut the flakes way down, but a truly timing-sensitive test can still misfire once. Instead of a blanket rerun plugin, the runner retries a failed file exactly once in a brand new subprocess. If the retry passes, the file counts as green, but it gets printed in a FLAKY summary with both attempts' output.



That summary matters. A pass-on-retry is not "handled," it's a bug you now have a reproduction window for. Silently swallowing it is how a suite rots back into the flaky state you just escaped. Surfacing it keeps the pressure on to fix the underlying timing assumption.




def run_with_retry(file, args, root, timeout, retries=1):
for attempt in range(retries + 1):
f, rc, out = run_one_file(file, args, root, timeout)
if rc == 0:
if attempt > 0:
mark_flaky(f) # green, but reported loudly
return f, rc, out
return f, rc, out









What about you?



If you're fighting flakes that only show up in the full suite, check whether your runner reuses workers before you write another reset fixture. How are you drawing your isolation boundary?

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-94097 | A vulnerability was determined in Netcore NBR200V2 1.3.241127.071246. Th…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick