diff --git a/frankenphp.c b/frankenphp.c index ca7ec27b14..08241e2122 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -1874,6 +1874,20 @@ static void *php_thread(void *arg) { * grace period can wake it from a busy PHP loop or blocking syscall. */ frankenphp_register_thread_for_kill(thread_index); +#if !defined(PHP_WIN32) && defined(SIGURG) + /* That was the first call of this thread into Go, which unblocks, for + * good, the signal its runtime preempts goroutines with. The signal is + * sent to a thread while it runs Go code, and may only land once the + * thread is back in PHP and entering a system call, which then fails with + * EINTR: stream_select() returns false, a poll context throws. Blocked + * again from here on, the Go code this thread runs is preempted at its + * safe points only, which the callbacks of a PHP thread reach quickly. */ + sigset_t preemption; + sigemptyset(&preemption); + sigaddset(&preemption, SIGURG); + pthread_sigmask(SIG_BLOCK, &preemption, NULL); +#endif + bool thread_is_healthy = true; bool has_attempted_shutdown = false; diff --git a/frankenphp_test.go b/frankenphp_test.go index c792c3642f..e57738cfa9 100644 --- a/frankenphp_test.go +++ b/frankenphp_test.go @@ -191,6 +191,23 @@ func TestEnvVarsInPhpIni(t *testing.T) { }) } +// The Go runtime preempts goroutines with SIGURG, which would interrupt the +// system call a PHP thread sleeps in when it lands there +func TestPreemptionSignalIsBlocked_module(t *testing.T) { testPreemptionSignalIsBlocked(t, nil) } +func TestPreemptionSignalIsBlocked_worker(t *testing.T) { + testPreemptionSignalIsBlocked(t, &testOptions{workerScript: "signal-mask.php"}) +} +func testPreemptionSignalIsBlocked(t *testing.T, opts *testOptions) { + if runtime.GOOS != "linux" { + t.Skip("the signal mask of a thread is read from /proc") + } + + runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) { + body, _ := testGet("http://example.com/signal-mask.php", handler, t) + assert.Equal(t, "blocked", body) + }, opts) +} + func TestFinishRequest_module(t *testing.T) { testFinishRequest(t, &testOptions{}) } func TestFinishRequest_worker(t *testing.T) { testFinishRequest(t, &testOptions{workerScript: "finish-request.php"}) diff --git a/testdata/signal-mask.php b/testdata/signal-mask.php new file mode 100644 index 0000000000..82526bdf60 --- /dev/null +++ b/testdata/signal-mask.php @@ -0,0 +1,11 @@ +> 22 & 1 ? 'blocked' : 'not blocked'; +};