Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions frankenphp.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
17 changes: 17 additions & 0 deletions frankenphp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"})
Expand Down
11 changes: 11 additions & 0 deletions testdata/signal-mask.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

require_once __DIR__.'/_executor.php';

// Tells whether the calling thread blocks SIGURG, the signal the Go runtime
// preempts goroutines with: bit 22 of the mask the kernel reports.
return function () {
preg_match('/^SigBlk:\s*([0-9a-f]+)/m', file_get_contents('/proc/thread-self/status'), $mask);

echo hexdec($mask[1]) >> 22 & 1 ? 'blocked' : 'not blocked';
};
Loading