Skip to content

Fix fork-child hangs on copied condition variables (musl); skip re-testing identical trees on develop - #69

Merged
davidnich merged 2 commits into
developfrom
bugfix/musl-fork-gil-switch-cond
Nov 13, 2026
Merged

davidnich merged 2 commits into
developfrom
bugfix/musl-fork-gil-switch-cond

Conversation

@davidnich

Copy link
Copy Markdown
Member

Summary

Fork fix: On Alpine (musl), test/standalone-signals.py could hang forever in the child of os.fork(). The child kept the parent's copy of the GIL switch helper's condition variable, which still listed the parent's helper thread as a waiter. musl's pthread_cond_signal() waits for the waiters it wakes, so the child's notify_one() never returned while it held the helper's lock (job test-alpine 228940, pipeline 64826).

  • gil_switch_fork_child(): the child constructs GilSwitchState::cond again in place.
  • Two more condition variables had the same problem, and both are fixed the same way:
    • QorePythonProgram::pgm_thr_cond, in forkChildThreadStates().
    • The condition of the declaration handles.
  • Related bug, also fixed: in a fork child, the declaration handles still counted uses by the parent's other threads, which never end in the child, so deleting a Program waited forever. A new child handler sets each handle's use count to the forking thread's uses and resets the waiter count. It reaches every handle through an intrusive list, so it allocates no memory in the child.
  • None of the old objects are destroyed, because pthread_cond_destroy() can also wait for waiters. This matches QoreCondition::reinitAfterFork(), which modules can't call.

CI: develop push pipelines now skip the test jobs when the identical tree already passed every test job, for example on the merged PR branch. This is ported from module-ncurses 73d328c and module-pgsql 251bac2. QORE_CI_FORCE_FULL=1 forces a full run, and missing or incomplete evidence also runs every job in full.

Tests

  • New test/standalone-fork.py: forks 20 times with the GIL switch helper idle and 20 times while it is busy. The children wait for the GIL in Qore calls.
  • New python-fork.qtest case: the child deletes a Program whose class another thread was constructing. Without the fix it hangs; verified locally, killed by timeout.
  • standalone-signals.py and the new test wait for children with a deadline, using a pidfd and select, instead of waiting forever. A hung child now fails the test instead of holding the job until the CI timeout.
  • test/test_tree_tested.py: 24 unit tests for the CI scripts.
  • Local debug build with Python 3.14 on glibc: all qtests and standalone tests pass. Valgrind on the fork tests shows the same results as develop.
  • The musl hang itself can only be reproduced on Alpine; the test-alpine job runs the new tests.

Known gap: there is no test for a thread that waits in invalidate() or waitForThreadsIntern() at the moment of the fork. A test can't tell that a thread has reached that wait without polling, so those two fixes are covered by code review only.

🤖 Generated with Claude Code

…the parent (musl)

A child of fork() kept the parent's copies of the module's condition
variables, which still listed the parent's waiting threads as waiters.
Those threads do not exist in the child, and musl's
pthread_cond_signal()/broadcast() wait for the waiters that they wake, so
the child hung (seen in test-alpine: standalone-signals.py hung in the
child's first Qore call that released the GIL, in notify_one() of the GIL
switch helper's condition variable while holding its lock).

- gil_switch_fork_child(): construct GilSwitchState::cond again in place
  (the helper thread always waits on it while the prepare handler holds
  the lock)
- forkChildThreadStates(): construct pgm_thr_cond of every interpreter
  again
- QorePythonDeclHandle: a new child handler clears the threads waiting in
  invalidate(), constructs the condition again, and sets the use count of
  every handle to the forking thread's uses; previously the uses of the
  parent's other threads never ended in the child, and deleting a Program
  waited for them forever.  All handles are kept in an intrusive list so
  the handler reaches them without allocating memory.

The old objects are not destroyed, as pthread_cond_destroy() can also
wait for the waiters (as QoreCondition::reinitAfterFork() in libqore,
which modules cannot call).

Tests: new test/standalone-fork.py (forks with the GIL switch helper idle
and busy; the child waits for the GIL in Qore calls), a new
python-fork.qtest case (the child deletes a Program whose class another
thread was constructing; it hangs without the fix), and
standalone-signals.py waits for its child with a deadline (pidfd) instead
of waiting forever.
… tests

As in module-ncurses (73d328c) and module-pgsql (251bac2): the new
tree-already-tested job (.pre, develop pushes only) finds a successful
pipeline for the commit or for a parent with the identical tree (the
merged pull request branch) in which every test job passed, and each test
job then reports success to GitHub under its own context with a link to
that pipeline and ends before building.  Missing or incomplete evidence
runs every job in full; QORE_CI_FORCE_FULL=1 forces a full run.

test/test_tree_tested.py tests the scripts and checks that the required
jobs of tree-already-tested.py match the test jobs of .gitlab-ci.yml; it
runs in every test job.
Copilot AI balanced review requested due to automatic review settings November 13, 2026 17:46

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@davidnich
davidnich merged commit e32281d into develop Nov 13, 2026
8 checks passed
@davidnich
davidnich deleted the bugfix/musl-fork-gil-switch-cond branch November 13, 2026 17:56
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.

2 participants