Skip to content
Merged
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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.7.0] -

### Fixed
- **`Async\signal()` no longer dies in worker threads** (#109). Each call to
`php_request_startup()` in a worker thread ran `zend_signal_activate()`,
which unconditionally re-installed `zend_signal_handler_defer` via
`sigaction()` — clobbering the libuv handler the reactor had installed in
the main thread. The next process-directed signal then hit Zend's defer
path in a worker whose `SIGG(handlers)` was empty, fell through to
`SIG_DFL`, and killed the process. Fixed in `Zend/zend_signal.c`:
`zend_signal_activate()` and `zend_signal_deactivate()` now early-return
when `zend_async_reactor_is_enabled()` (reactor module registered at
MINIT) — the reactor owns the OS-level sigaction process-wide, and the
per-thread libuv signal callback already dispatches via TLS `SIGG(handlers)`.
`zend_sigaction` is unchanged so pcntl-only flows keep working. Tests
`tests/signal/008-009` cover both `ThreadPool` and `spawn_thread` variants.
- **PDO MySQL `010-pdo_resource_cleanup` no longer false-fails under
parallel test workers** (#114). The test counted leaks against
`SHOW STATUS LIKE 'Threads_connected'` — a *server-global* counter that
Expand Down
59 changes: 59 additions & 0 deletions tests/signal/008-signal_thread_pool_multi.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
--TEST--
Async\signal() #109: multiple workers in ThreadPool each receive process-directed signal
--SKIPIF--
<?php
if (PHP_OS_FAMILY === 'Windows') echo "skip Unix-only test";
if (!function_exists('posix_kill')) echo "skip posix extension required";
if (!PHP_ZTS) echo "skip ZTS required";
?>
--FILE--
<?php

use Async\Signal;
use Async\ThreadPool;
use function Async\signal;
use function Async\spawn;
use function Async\await;
use function Async\timeout;
use function Async\delay;

echo "start\n";

$pool = new ThreadPool(workers: 2);

$f1 = $pool->submit(function () {
try {
$r = await(signal(Signal::SIGUSR1), timeout(2000));
return "w1:" . $r->name;
} catch (\Throwable $e) {
return "w1:ex:" . $e->getMessage();
}
});

$f2 = $pool->submit(function () {
try {
$r = await(signal(Signal::SIGUSR1), timeout(2000));
return "w2:" . $r->name;
} catch (\Throwable $e) {
return "w2:ex:" . $e->getMessage();
}
});

spawn(function () {
delay(300);
posix_kill(getmypid(), SIGUSR1);
});

$results = [await($f1), await($f2)];
sort($results);
foreach ($results as $r) echo $r . "\n";

$pool->close();
echo "end\n";

?>
--EXPECT--
start
w1:SIGUSR1
w2:SIGUSR1
end
56 changes: 56 additions & 0 deletions tests/signal/009-signal_spawn_thread_multi.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
--TEST--
Async\signal() #109: multiple spawn_thread workers each receive process-directed signal
--SKIPIF--
<?php
if (PHP_OS_FAMILY === 'Windows') echo "skip Unix-only test";
if (!function_exists('posix_kill')) echo "skip posix extension required";
if (!PHP_ZTS) echo "skip ZTS required";
?>
--FILE--
<?php

use Async\Signal;
use function Async\signal;
use function Async\spawn;
use function Async\spawn_thread;
use function Async\await;
use function Async\timeout;
use function Async\delay;

echo "start\n";

$t1 = spawn_thread(function () {
try {
$r = await(signal(Signal::SIGUSR1), timeout(2000));
return "t1:" . $r->name;
} catch (\Throwable $e) {
return "t1:ex:" . $e->getMessage();
}
});

$t2 = spawn_thread(function () {
try {
$r = await(signal(Signal::SIGUSR1), timeout(2000));
return "t2:" . $r->name;
} catch (\Throwable $e) {
return "t2:ex:" . $e->getMessage();
}
});

spawn(function () {
delay(300);
posix_kill(getmypid(), SIGUSR1);
});

$results = [await($t1), await($t2)];
sort($results);
foreach ($results as $r) echo $r . "\n";

echo "end\n";

?>
--EXPECT--
start
t1:SIGUSR1
t2:SIGUSR1
end
44 changes: 44 additions & 0 deletions tests/signal/010-signal_unregistered_kills_process.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
--TEST--
Async\signal() #109: unregistered SIGINT/SIGTERM still kill the process at OS level
--SKIPIF--
<?php
if (PHP_OS_FAMILY === 'Windows') echo "skip Unix-only test";
if (!extension_loaded('pcntl')) echo "skip pcntl required";
if (!extension_loaded('posix')) echo "skip posix required";
?>
--EXTENSIONS--
pcntl
posix
--FILE--
<?php
// Verify: a PHP process with TrueAsync loaded but no registered handler
// for SIGINT/SIGTERM is killed by the signal exactly as the OS default
// action says. The patch in Zend/zend_signal.c must not change this —
// Ctrl-C semantics for unhandled signals are preserved.
//
// pcntl_fork + pcntl_waitpid gives us reliable WIFSIGNALED/WTERMSIG info.

function check(int $signum, string $name): void {
$pid = pcntl_fork();
if ($pid === 0) {
// child: just sleep — no signal handlers registered
usleep(3_000_000);
exit(0);
}
usleep(200_000);
posix_kill($pid, $signum);
pcntl_waitpid($pid, $status);
$sig = pcntl_wifsignaled($status) ? pcntl_wtermsig($status) : 0;
echo "$name: ", ($sig === $signum ? "killed by $name" : "FAIL exit_sig=$sig"), "\n";
}

check(SIGINT, 'SIGINT');
check(SIGTERM, 'SIGTERM');
check(SIGUSR1, 'SIGUSR1');
echo "done\n";
?>
--EXPECT--
SIGINT: killed by SIGINT
SIGTERM: killed by SIGTERM
SIGUSR1: killed by SIGUSR1
done
31 changes: 31 additions & 0 deletions tests/signal/011-signal_pcntl_still_works.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
--TEST--
Async\signal() #109: pcntl_signal still intercepts when TrueAsync is loaded
--SKIPIF--
<?php
if (PHP_OS_FAMILY === 'Windows') echo "skip Unix-only test";
if (!extension_loaded('pcntl')) echo "skip pcntl required";
if (!extension_loaded('posix')) echo "skip posix required";
?>
--EXTENSIONS--
pcntl
posix
--FILE--
<?php
// Verify: pcntl_signal() goes through zend_sigaction (which the patch
// does NOT touch) and still receives the signal correctly even though
// zend_signal_activate is a no-op under TrueAsync.

$got = null;
pcntl_signal(SIGUSR1, function (int $signo) use (&$got) {
$got = $signo;
});

posix_kill(posix_getpid(), SIGUSR1);
pcntl_signal_dispatch();

echo "got=", var_export($got, true), "\n";
echo $got === SIGUSR1 ? "OK\n" : "FAIL\n";
?>
--EXPECT--
got=10
OK
64 changes: 64 additions & 0 deletions tests/signal/012-signal_thread_pool_different_signals.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
--TEST--
Async\signal() #109: ThreadPool workers can register different signals concurrently
--SKIPIF--
<?php
if (PHP_OS_FAMILY === 'Windows') echo "skip Unix-only test";
if (!function_exists('posix_kill')) echo "skip posix extension required";
if (!PHP_ZTS) echo "skip ZTS required";
?>
--FILE--
<?php
// Each worker waits for a *different* signal. The main thread fires both
// signals; each worker must receive its own and only its own. This
// validates that per-thread libuv signal handles are independent and
// that signal dispatch routes by signo, not by "first registered".

use Async\Signal;
use Async\ThreadPool;
use function Async\signal;
use function Async\spawn;
use function Async\await;
use function Async\timeout;
use function Async\delay;

echo "start\n";

$pool = new ThreadPool(workers: 2);

$f1 = $pool->submit(function () {
try {
$r = await(signal(Signal::SIGUSR1), timeout(2000));
return "w1:" . $r->name;
} catch (\Throwable $e) {
return "w1:ex:" . $e->getMessage();
}
});

$f2 = $pool->submit(function () {
try {
$r = await(signal(Signal::SIGUSR2), timeout(2000));
return "w2:" . $r->name;
} catch (\Throwable $e) {
return "w2:ex:" . $e->getMessage();
}
});

spawn(function () {
delay(300);
posix_kill(getmypid(), SIGUSR1);
posix_kill(getmypid(), SIGUSR2);
});

$results = [await($f1), await($f2)];
sort($results);
foreach ($results as $r) echo $r . "\n";

$pool->close();
echo "end\n";

?>
--EXPECT--
start
w1:SIGUSR1
w2:SIGUSR2
end
Loading