Search code examples
phpsignalspcntl

Is there a way to signal a SIGALRM with a finer-grained delay than pcntl_alarm(int $seconds)?


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.


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);
    }