fix: pcntl signal reaching Go runtime causing long running cli scripts to get stuck#2445
Conversation
|
Clanker got this one right: https://github.com/laravel/framework/blob/v13.11.2/src/Illuminate/Queue/Worker.php#L853-L880 Workers set up signal receivers and the orchestrator issues them: https://github.com/laravel/horizon/blob/v5.47.0/src/Supervisor.php#L187-L220 |
7341d26 to
86c62b5
Compare
There was a problem hiding this comment.
Pull request overview
This PR addresses long-running frankenphp php-cli scripts getting stuck or crashing due to pcntl-managed Unix signals being delivered on Go runtime threads that lack the required PHP TSRM state. It does this by blocking a set of signals early (before Go runtime init) so Go threads inherit the blocked mask, then explicitly unblocking those signals on the dedicated PHP CLI pthread.
Changes:
- Block common
pcntl/process-control signals in a C constructor so Go runtime threads inherit a safe signal mask. - Unblock those signals on the
execute_script_clipthread so PHP CLI scripts can still receive and handle them. - Add a regression test (Go + PHP script) to validate reliable
pcntl_signaldelivery in long-running CLI scripts.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
frankenphp.c |
Blocks a set of Unix signals before Go runtime init; unblocks them in the PHP CLI execution thread to avoid pcntl/TSRM crashes. |
testdata/command-pcntl.php |
Adds a long-running PHP CLI signal exercise script to validate signal delivery behavior. |
cli_test.go |
Adds a regression test invoking the new PHP script via the internal CLI test harness. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| frankenphp_fill_cli_signal_set(&set); | ||
| /* Single-threaded at this point (constructors run before Go's runtime), | ||
| * so sigprocmask is sufficient and portable. */ | ||
| sigprocmask(SIG_BLOCK, &set, NULL); | ||
| } |
There was a problem hiding this comment.
Blocking the CLI-related signals process-wide in the constructor (via sigprocmask) also blocks termination signals like SIGTERM/SIGINT for all Go threads by default.
Yes, that's the point.
Please consider adding an explicit mitigation for library embedders (e.g. documented requirement, or a Go-side bootstrap that ensures SIGTERM/SIGINT remain deliverable even when no signal.Notify is used).
I'm not sure how realistic it is to actually hit this issue. The paths that Sloppilot mentioned should get stuck don't actually get stuck.
There was a problem hiding this comment.
Looks like Go unblocks sigkillable signals anyway, so this was effectively a no-op for the signals sloppilot flagged: https://github.com/golang/go/blob/1952e618b834bda60fb9efff0fc0af46b38e110d/src/runtime/signal_unix.go#L1356
|
Thanks! |
fixes #1902
fixes #2326
probably fixes #871
closes #614
probably fixes #2479
edit: draft until I can manually verify with a real laravel/symfony reproducer