pytest -q finishes the run — every test executes, the summary is written — and then the process never exits. It sits in interpreter shutdown indefinitely.
Measured on one such run: 5h27m wall clock, 53 seconds of CPU. The suite itself took a few minutes; the rest was the hang.
Evidence
sample <pid> during the hang:
1712 Py_RunMain (in Python) + 272
1712 Py_FinalizeEx (in Python) + 64
1712 wait_for_thread_shutdown (in Python) + 92
wait_for_thread_shutdown joins non-daemon threads. Something started one and never joined it.
Likely culprit
jvspatial/api/integrations/scheduler/scheduler.py:69 creates a ThreadPoolExecutor. Since Python 3.9 its worker threads are non-daemon, and concurrent.futures.thread._python_exit joins them at interpreter exit — which is precisely the frame above.
Note the SchedulerThread itself (scheduler.py:124-126) is daemon=True, so it looks handled at a glance; the executor it owns is the part that isn't. If the executor is never .shutdown(), or a worker is parked on a blocking call, exit blocks forever.
Stated as the strongest hypothesis, not a confirmed diagnosis — I have not instrumented which thread is actually outstanding.
Why it is worth fixing
- CI hides it. The runner tears the container down, so this never shows up in a workflow — only locally, where it costs a maintainer real time.
- It hides results. Anyone piping the suite (
pytest ... | tail) gets no output at all, because the pipe never closes. That is how it burned five hours here: the run had completed and looked like it was still going.
- It reproduces on unmodified
main.
Repro
pytest -q # completes, then hangs; ^C or kill required
pytest -q | tail # worse: no output ever appears
Suggested direction
Shut the executor down on scheduler stop (and register an atexit fallback), or construct it so the workers cannot outlive the process. A pytest session-finish assertion that no non-daemon threads remain would keep it from coming back.
Found while running the full suite during #37 / #38.
pytest -qfinishes the run — every test executes, the summary is written — and then the process never exits. It sits in interpreter shutdown indefinitely.Measured on one such run: 5h27m wall clock, 53 seconds of CPU. The suite itself took a few minutes; the rest was the hang.
Evidence
sample <pid>during the hang:wait_for_thread_shutdownjoins non-daemon threads. Something started one and never joined it.Likely culprit
jvspatial/api/integrations/scheduler/scheduler.py:69creates aThreadPoolExecutor. Since Python 3.9 its worker threads are non-daemon, andconcurrent.futures.thread._python_exitjoins them at interpreter exit — which is precisely the frame above.Note the
SchedulerThreaditself (scheduler.py:124-126) isdaemon=True, so it looks handled at a glance; the executor it owns is the part that isn't. If the executor is never.shutdown(), or a worker is parked on a blocking call, exit blocks forever.Stated as the strongest hypothesis, not a confirmed diagnosis — I have not instrumented which thread is actually outstanding.
Why it is worth fixing
pytest ... | tail) gets no output at all, because the pipe never closes. That is how it burned five hours here: the run had completed and looked like it was still going.main.Repro
Suggested direction
Shut the executor down on scheduler stop (and register an
atexitfallback), or construct it so the workers cannot outlive the process. Apytestsession-finish assertion that no non-daemon threads remain would keep it from coming back.Found while running the full suite during #37 / #38.