frankenphp
2ac6c419 - feat: cross-platform force-kill primitive for stuck PHP threads (#2365)

Commit
190 days ago
feat: cross-platform force-kill primitive for stuck PHP threads (#2365) First step of the split suggested in #2287: land the force-kill infrastructure as a standalone, reviewable primitive independent of background workers. ## Design Each PHP thread, at boot from its own TSRM context, hands a `force_kill_slot` (pointers to its `EG(vm_interrupt)` and `EG(timed_out)` atomic bools, plus `pthread_t` / Windows `HANDLE`) back to Go via `go_frankenphp_store_force_kill_slot`. The slot lives on `phpThread` and is protected by a per-thread `RWMutex` so the zero-and-release path at thread exit cannot race an in-flight kill. From any goroutine, Go passes the slot back to `frankenphp_force_kill_thread`, which stores `true` into both atomic bools (waking the VM at the next opcode boundary, routing through `zend_timeout` -> "Maximum execution time exceeded") and delivers a platform-specific wake-up: - **Linux/FreeBSD**: `pthread_kill(SIGRTMIN+3)` with a no-op handler installed once via `pthread_once`, `SA_ONSTACK`, no `SA_RESTART`. Signal delivery returns any in-flight blocking syscall with `EINTR`. - **Windows**: `CancelSynchronousIo` + `QueueUserAPC` covers alertable I/O and `SleepEx`. Non-alertable `Sleep` (including PHP's `usleep`) stays uninterruptible. - **macOS**: atomic-bool path only; threads stuck in blocking syscalls wait for the syscall to complete naturally. **Reserved signal**: `SIGRTMIN+3`. A PHP script that calls `pcntl_signal(SIGRTMIN+3, ...)` clobbers this. Embedders whose own Go code uses `SIGRTMIN+3` must patch it here. glibc NPTL reserves `SIGRTMIN..SIGRTMIN+2`, so the offset cannot go lower. ## Drain integration `drainWorkerThreads` waits `drainGracePeriod` (30s) for each thread to reach `Yielding`, then arms force-kill on stragglers and **keeps waiting** until they yield. `phpThread.shutdown` does the same. There is no abandon path: if a thread is stuck in a syscall force-kill cannot interrupt (macOS, Windows non-alertable Sleep), the drain blocks until the syscall returns naturally — matching pre-patch behaviour exactly, just typically much faster because force-kill cuts a `sleep(60)` down to milliseconds. Operators that want a harder bound rely on their orchestrator (systemd, k8s, supervisord) to SIGKILL the process. `go_frankenphp_on_thread_shutdown` runs on both the healthy path and the unhealthy-during-Shutdown path so `state.Done` is set even when force-kill bails the thread. Without it, `phpThread.shutdown`'s `WaitFor(state.Done)` would never unblock. ## Testing `TestRestartWorkersForceKillsStuckThread` drives the full path via a marker file so `RestartWorkers` only arms once the worker is proven parked in `sleep()`, then asserts bounded elapsed time and that the post-sleep echo never runs.
Parents
Loading