2019-01-16 01:10:54 +01:00
|
|
|
<?php
|
|
|
|
|
2019-01-16 16:22:02 +01:00
|
|
|
namespace Joop\Asynchronous;
|
2019-01-16 01:10:54 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Class Asynchronous
|
2019-01-16 16:22:02 +01:00
|
|
|
* Responsible for management of child processes and shared memory.
|
2019-01-16 01:10:54 +01:00
|
|
|
*/
|
|
|
|
class Asynchronous
|
|
|
|
{
|
2019-01-16 16:22:02 +01:00
|
|
|
/** @var Asynchronous|null */
|
2019-01-16 01:10:54 +01:00
|
|
|
private static $instance;
|
|
|
|
|
2019-01-16 16:22:02 +01:00
|
|
|
/** @var int */
|
|
|
|
private static $key = 0;
|
|
|
|
|
|
|
|
|
2019-01-16 01:10:54 +01:00
|
|
|
/** @var bool */
|
|
|
|
private $isChild = false;
|
|
|
|
|
|
|
|
/** @var int[] */
|
|
|
|
private $children = [];
|
|
|
|
|
|
|
|
/** @var resource */
|
|
|
|
private $shm;
|
|
|
|
|
|
|
|
/** @var int */
|
|
|
|
private $shmKey;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param callable $function
|
|
|
|
* @param mixed ...$parameters
|
|
|
|
* @return Promise|null;
|
|
|
|
*/
|
|
|
|
public static function run(callable $function, ...$parameters)
|
|
|
|
{
|
|
|
|
$instance = self::getInstance();
|
2019-01-16 16:22:02 +01:00
|
|
|
$key = self::generatePromiseKey();
|
2019-01-16 01:10:54 +01:00
|
|
|
$pid = pcntl_fork();
|
|
|
|
|
2019-01-16 16:22:02 +01:00
|
|
|
/*
|
|
|
|
* The fork failed. Instead of returning a promise, we return null.
|
|
|
|
*/
|
|
|
|
if ($pid == -1)
|
2019-01-16 01:10:54 +01:00
|
|
|
return null;
|
|
|
|
|
2019-01-16 16:22:02 +01:00
|
|
|
/*
|
|
|
|
* Parent process. We keep track of the PID of the child process
|
|
|
|
* in order for us to read out it's status later on.
|
|
|
|
* A Promise instance is returned that corresponds to the key in
|
|
|
|
* memory to which the child process will write sometime.
|
|
|
|
*/
|
2019-01-16 01:10:54 +01:00
|
|
|
if ($pid > 0) {
|
|
|
|
$instance->children[] = $pid;
|
|
|
|
|
|
|
|
return new Promise($key);
|
|
|
|
}
|
|
|
|
|
2019-01-16 16:22:02 +01:00
|
|
|
/*
|
|
|
|
* Child process. Mark the (copied) instance of this class as a child
|
|
|
|
* to prevent unneeded shutdown handler execution.
|
|
|
|
* Reattach to the shared memory block (the $shm member variable is a
|
|
|
|
* resource since PHP > 5.3 and is thus not shared with the child)
|
|
|
|
* and execute the function.
|
|
|
|
* On a successful execution, write the result to the shared memory
|
|
|
|
* block to which the parent is attached.
|
|
|
|
* On failure, write a default response to the block in order for
|
|
|
|
* the Promise to be able to resolve.
|
|
|
|
*/
|
2019-01-16 01:10:54 +01:00
|
|
|
$instance->isChild = true;
|
2019-01-16 16:22:02 +01:00
|
|
|
$instance->attachShm();
|
2019-01-16 01:10:54 +01:00
|
|
|
try {
|
2019-01-16 16:22:02 +01:00
|
|
|
$response = call_user_func_array($function, $parameters);
|
2019-01-16 01:10:54 +01:00
|
|
|
shm_put_var($instance->shm, $key, $response ?? Promise::RESPONSE_NONE);
|
|
|
|
exit(0);
|
2019-01-16 16:22:02 +01:00
|
|
|
} catch (\Throwable $throwable) {
|
|
|
|
shm_put_var($instance->shm, $key, Promise::RESPONSE_ERROR);
|
2019-01-16 01:10:54 +01:00
|
|
|
exit(1);
|
|
|
|
}
|
2019-01-16 16:22:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
public static function cleanup()
|
|
|
|
{
|
|
|
|
/*
|
|
|
|
* Iterate over all child process PIDs and check
|
|
|
|
* if one or more of them has stopped.
|
|
|
|
*/
|
|
|
|
$instance = self::getInstance();
|
|
|
|
foreach ($instance->children as $index => $pid) {
|
|
|
|
$response = pcntl_waitpid($pid, $status, WNOHANG);
|
|
|
|
if ($response === $pid)
|
|
|
|
unset($instance->children[$index]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-16 16:53:44 +01:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
public static function awaitChildren()
|
|
|
|
{
|
|
|
|
$instance = self::getInstance();
|
|
|
|
while (count($instance->children) > 0) {
|
|
|
|
pcntl_wait($status);
|
|
|
|
array_shift($instance->children);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-16 16:22:02 +01:00
|
|
|
/**
|
|
|
|
* @return int
|
|
|
|
*/
|
|
|
|
public static function childCount()
|
|
|
|
{
|
|
|
|
return count(self::getInstance()->children);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-01-16 01:10:54 +01:00
|
|
|
|
2019-01-16 16:22:02 +01:00
|
|
|
/*
|
|
|
|
* Private methods below
|
|
|
|
*/
|
2019-01-16 01:10:54 +01:00
|
|
|
|
2019-01-16 16:22:02 +01:00
|
|
|
/**
|
|
|
|
* Asynchronous constructor.
|
|
|
|
*/
|
|
|
|
private function __construct()
|
|
|
|
{
|
|
|
|
/*
|
|
|
|
* Use the filename as an identifier to create the
|
|
|
|
* System V IPC key.
|
|
|
|
*/
|
|
|
|
$this->shmKey = ftok(__FILE__, 't');
|
|
|
|
Promise::__setShmKey($this->shmKey);
|
|
|
|
$this->attachShm();
|
2019-01-16 01:10:54 +01:00
|
|
|
}
|
|
|
|
|
2019-01-16 16:22:02 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
private function attachShm()
|
|
|
|
{
|
|
|
|
$this->shm = shm_attach($this->shmKey);
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return Asynchronous
|
|
|
|
*/
|
|
|
|
private static function getInstance()
|
|
|
|
{
|
|
|
|
if (is_null(self::$instance)) {
|
|
|
|
/*
|
|
|
|
* This is executed once during runtime;
|
|
|
|
* when a functionality from this class
|
|
|
|
* is used for the first time.
|
|
|
|
*/
|
|
|
|
self::$instance = new static();
|
|
|
|
self::registerHandlers();
|
|
|
|
}
|
|
|
|
|
|
|
|
return self::$instance;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-01-16 01:10:54 +01:00
|
|
|
/**
|
|
|
|
* @return int
|
|
|
|
*/
|
|
|
|
private static function generatePromiseKey()
|
|
|
|
{
|
2019-01-16 16:22:02 +01:00
|
|
|
/*
|
|
|
|
* Get the current key.
|
|
|
|
*/
|
2019-01-16 01:10:54 +01:00
|
|
|
$promiseKey = self::$key;
|
2019-01-16 16:22:02 +01:00
|
|
|
|
|
|
|
/*
|
|
|
|
* Reset the key to 0 if the upper bound of
|
|
|
|
* 9.999.999 is reached (Windows limit for
|
|
|
|
* shm keys).
|
|
|
|
*/
|
|
|
|
self::$key = (++self::$key > 9999999) ? 0 : self::$key;
|
2019-01-16 01:10:54 +01:00
|
|
|
|
|
|
|
return $promiseKey;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
*/
|
2019-01-16 16:22:02 +01:00
|
|
|
private static function registerHandlers()
|
2019-01-16 01:10:54 +01:00
|
|
|
{
|
2019-01-16 16:22:02 +01:00
|
|
|
$instance = self::getInstance();
|
2019-01-16 01:10:54 +01:00
|
|
|
|
2019-01-16 16:22:02 +01:00
|
|
|
/*
|
|
|
|
* The shutdown handler
|
|
|
|
*/
|
|
|
|
$handler = function () use (&$instance) {
|
|
|
|
/*
|
|
|
|
* A child process has no business here.
|
|
|
|
*/
|
|
|
|
if ($instance->isChild)
|
|
|
|
return;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Wait for all children to finish to
|
|
|
|
* ensure that all writing to the shared
|
|
|
|
* memory block is finished.
|
|
|
|
*/
|
2019-01-16 16:53:44 +01:00
|
|
|
self::awaitChildren();
|
2019-01-16 16:22:02 +01:00
|
|
|
|
|
|
|
/*
|
|
|
|
* Ask the kernel to mark the shared memory
|
|
|
|
* block for removal and detach from it to
|
|
|
|
* actually allow for removal.
|
|
|
|
*/
|
|
|
|
if (is_resource($instance->shm)) {
|
|
|
|
shm_remove($instance->shm);
|
|
|
|
shm_detach($instance->shm);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Actually register the handler as shutdown
|
|
|
|
* handler and signal handler for SIGINT, SIGTERM
|
|
|
|
*/
|
|
|
|
register_shutdown_function($handler);
|
|
|
|
foreach ([SIGINT, SIGTERM] as $SIGNAL)
|
|
|
|
pcntl_signal($SIGNAL, $handler);
|
2019-01-16 01:10:54 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|