pcntl_alarm(int $seconds)
only has a resolution of seconds. Is there a way in PHP to signal a SIGALRM with a delay of, say, milliseconds? Maybe a posix_kill()
with a delay argument?
PS.: I'm aware of Swoole\Process::alarm()
from the PECL extension Swoole, but I'm looking for a more bare-bones PHP solution.
I found one way to do it, but it is a bit convoluted:
<?php
// alarm uses proc_open() to signal this process from a child process
function alarm(int $msec): void {
$desc = [
['pipe', 'r']
];
$pid = posix_getpid();
$process = proc_open('php', $desc, $pipes);
fwrite(
$pipes[0],
"<?php
usleep($msec * 1000);
posix_kill($pid, SIGALRM);
"
);
fclose($pipes[0]);
}
function handleSignal(int $signal): void {
switch($signal) {
case SIGALRM:
echo "interrupted by ALARM\n";
break;
}
}
pcntl_async_signals(true);
pcntl_signal(SIGALRM, 'handleSignal');
// set alarm 200ms from now
alarm(200);
while(true) {
echo "going to sleep for 10 seconds...\n";
// first sleep(10) will be interrupted after 200ms
sleep(10);
}
...and it's way too resource intensive. And because it needs to spawn a new process each time probably not very time-accurate either.
Addendum:
I've managed to make it more efficient by creating only one long-running interrupter process, instead of creating short-running processes for each interruption request.
It's still far from ideal, but it does the job for now:
<?php
// long-running interrupter process for the Interrupter class
// that accepts interruption requests with a delay
class InterrupterProcess
{
private $process;
private $writePipe;
private const PROCESS_CODE = <<<'CODE'
<?php
$readPipe = fopen('php://fd/3', 'r');
$interrupts = [];
while(true) {
$r = [$readPipe];
$w = null;
$e = null;
$time = microtime(true);
$minExpiry = min($interrupts + [($time + 1)]);
$timeout = $minExpiry - $time;
if(stream_select($r, $w, $e, (int) $timeout, (int) (fmod($timeout, 1) * 1e6)) > 0) {
$interrupt = json_decode(fread($readPipe, 1024), true);
$interrupts[$interrupt['pid']] = $interrupt['microtime'];
}
$time = microtime(true);
foreach($interrupts as $pid => $interrupt) {
if($interrupt <= $time) {
posix_kill($pid, SIGALRM);
unset($interrupts[$pid]);
}
}
}
CODE;
public function __construct() {
$desc = [
['pipe', 'r'],
STDOUT,
STDOUT,
['pipe', 'r']
];
$this->process = proc_open(['php'], $desc, $pipes);
$this->writePipe = $pipes[3];
fwrite($pipes[0], self::PROCESS_CODE);
fclose($pipes[0]);
}
public function __destruct() {
$this->destroy();
}
public function setInterrupt(int $pid, float $delay): bool {
if(!is_null($this->writePipe)) {
fwrite($this->writePipe, json_encode(['pid' => $pid, 'microtime' => microtime(true) + $delay]));
return true;
}
return false;
}
private function destroy(): void {
if(!is_null($this->writePipe)) {
fclose($this->writePipe);
$this->writePipe = null;
}
if(!is_null($this->process)) {
proc_terminate($this->process);
proc_close($this->process);
$this->process = null;
}
}
}
// main Interrupter class
class Interrupter
{
private $process;
public function __destruct() {
$this->destroy();
}
public function interrupt(float $delay): void {
if(is_null($this->process)) {
pcntl_async_signals(true);
pcntl_signal(SIGALRM, function(int $signal): void {
$this->handleSignal($signal);
});
$this->process = $this->createInterrupterProcess();
}
$this->process->setInterrupt(posix_getpid(), $delay);
}
private function createInterrupterProcess(): InterrupterProcess {
return new InterrupterProcess();
}
private function handleSignal(int $signal): void {
switch($signal) {
case SIGALRM:
$time = time();
echo "interrupted by ALARM @ $time\n";
break;
}
}
private function destroy(): void {
$this->process = null;
}
}
$interrupter = new Interrupter();
while(true) {
$time = time();
echo "going to sleep for 10 seconds @ $time...\n";
$interrupter->interrupt(2);
sleep(10);
}