From 1a746608e0bd60ca74a3d718407b1ce0838daf53 Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Sat, 16 Dec 2017 14:32:48 +0100 Subject: [PATCH 01/50] WUP --- composer.json | 6 +- config/.gitkeep | 0 src/CallableProcess.php | 18 ---- src/Output/ErrorProcessOutput.php | 39 -------- src/Output/ProcessOutput.php | 47 --------- src/ParallelProcess.php | 81 +++++++++++++++ src/Pool.php | 147 +++++++++------------------- src/Process.php | 119 ---------------------- src/Runtime.php | 100 ------------------- src/Runtime/ChildProcess.php | 43 ++++++++ src/Runtime/ParentRuntime.php | 55 +++++++++++ src/Task.php | 8 -- src/helpers.php | 11 +-- tests/ChildProcessBootstrapTest.php | 41 ++++++++ tests/MyTask.php | 20 ---- tests/PoolTest.php | 5 +- 16 files changed, 276 insertions(+), 464 deletions(-) delete mode 100644 config/.gitkeep delete mode 100644 src/CallableProcess.php delete mode 100644 src/Output/ErrorProcessOutput.php delete mode 100644 src/Output/ProcessOutput.php create mode 100644 src/ParallelProcess.php delete mode 100644 src/Process.php delete mode 100644 src/Runtime.php create mode 100644 src/Runtime/ChildProcess.php create mode 100644 src/Runtime/ParentRuntime.php delete mode 100644 src/Task.php create mode 100644 tests/ChildProcessBootstrapTest.php delete mode 100644 tests/MyTask.php diff --git a/composer.json b/composer.json index 06e4ea19..134ee8cb 100644 --- a/composer.json +++ b/composer.json @@ -16,9 +16,13 @@ } ], "require": { - "php": "^7.1" + "php": "^7.1", + "guzzlehttp/promises": "^1.3", + "jeremeamia/superclosure": "^2.3", + "symfony/process": "^4.0" }, "require-dev": { + "larapack/dd": "^1.1", "phpunit/phpunit": "^6.0" }, "autoload": { diff --git a/config/.gitkeep b/config/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/CallableProcess.php b/src/CallableProcess.php deleted file mode 100644 index c31577e6..00000000 --- a/src/CallableProcess.php +++ /dev/null @@ -1,18 +0,0 @@ -callable = $callable; - } - - public function execute() - { - return call_user_func_array($this->callable, []); - } -} diff --git a/src/Output/ErrorProcessOutput.php b/src/Output/ErrorProcessOutput.php deleted file mode 100644 index 23353185..00000000 --- a/src/Output/ErrorProcessOutput.php +++ /dev/null @@ -1,39 +0,0 @@ - get_class($payload), - 'message' => $payload->getMessage(), - ]; - - return new static($payload); - } - - public function payload(): Throwable - { - $class = $this->payload['class'] ?? null; - $message = $this->payload['message'] ?? null; - - $payload = new $class($message); - - return $payload; - } -} diff --git a/src/Output/ProcessOutput.php b/src/Output/ProcessOutput.php deleted file mode 100644 index 5e86d8e5..00000000 --- a/src/Output/ProcessOutput.php +++ /dev/null @@ -1,47 +0,0 @@ -payload = $payload; - } - - /** - * @param $payload - * - * @return static - */ - public static function create($payload) - { - return new static($payload); - } - - public function setSuccess(bool $success = true): self - { - $this->success = $success; - - return $this; - } - - public function isSuccess(): bool - { - return $this->success; - } - - public function payload() - { - return $this->payload; - } - - public function serialize() - { - return serialize($this); - } -} diff --git a/src/ParallelProcess.php b/src/ParallelProcess.php new file mode 100644 index 00000000..b9e9c974 --- /dev/null +++ b/src/ParallelProcess.php @@ -0,0 +1,81 @@ +process = $process; + $this->inputStream = $inputStream; + $this->promise = new Promise(); + $this->internalId = uniqid(getmypid()); + } + + public static function create(Process $process, InputStream $inputStream): self + { + return new self($process, $inputStream); + } + + public function start(): self + { + $this->process->start(); + $this->inputStream->close(); + + return $this; + } + + public function isRunning(): bool + { + return $this->process->isRunning(); + } + + public function isSuccessful(): bool + { + return $this->process->isSuccessful(); + } + + public function isTerminated(): bool + { + return $this->process->isTerminated(); + } + + public function output() + { + return unserialize($this->process->getOutput()); + } + + public function errorOutput() + { + return $this->process->getErrorOutput(); + } + + public function process(): Process + { + return $this->process; + } + + public function promise(): Promise + { + return $this->promise; + } + + public function internalId(): string + { + return $this->internalId; + } + + public function pid(): ?string + { + return $this->process->getPid(); + } +} diff --git a/src/Pool.php b/src/Pool.php index fd91a704..49aafecb 100644 --- a/src/Pool.php +++ b/src/Pool.php @@ -2,31 +2,25 @@ namespace Spatie\Async; -use Exception; +use ArrayAccess; +use GuzzleHttp\Promise\Promise; +use Spatie\Async\Runtime\ParentRuntime; -class Pool implements \ArrayAccess +class Pool implements ArrayAccess { - protected $runtime; protected $concurrency = 20; protected $tasksPerProcess = 1; + protected $maximumExecutionTime = 300; - /** @var \Spatie\Async\Task[] */ - protected $tasks = []; - - /** @var \Spatie\Async\Process[] */ + /** @var \Spatie\Async\ParallelProcess[] */ protected $queue = []; - /** @var \Spatie\Async\Process[] */ + /** @var \Spatie\Async\ParallelProcess[] */ protected $inProgress = []; - /** @var \Spatie\Async\Process[] */ + /** @var \Spatie\Async\ParallelProcess[] */ protected $finished = []; - /** @var \Spatie\Async\Process[] */ + /** @var \Spatie\Async\ParallelProcess[] */ protected $failed = []; - public function __construct() - { - $this->runtime = new Runtime(); - } - /** * @return static */ @@ -51,17 +45,13 @@ public function tasksPerProcess(int $tasksPerProcess): self public function maximumExecutionTime(int $maximumExecutionTime): self { - $this->runtime->maximumExecutionTime($maximumExecutionTime); + $this->maximumExecutionTime = $maximumExecutionTime; return $this; } public function notify(): void { - if (count($this->tasks) >= $this->tasksPerProcess) { - $this->scheduleTasks($this->tasksPerProcess); - } - if (count($this->inProgress) >= $this->concurrency) { return; } @@ -72,105 +62,78 @@ public function notify(): void return; } - $process = $this->run($process); - - $this->inProgress($process); + $this->putInProgress($process); } - public function add($process): ?Process + public function add(callable $callable): Promise { - if ($process instanceof Task) { - $this->queueTask($process); + $process = ParentRuntime::createChildProcess($callable); - return null; - } - - if (! $process instanceof Process) { - $process = new CallableProcess($process); - } + $this->putInQueue($process); - $process->setInternalId(uniqid(getmypid())); - - $this->queue($process); - - return $process; - } - - public function run(Process $process): Process - { - return $this->runtime->start($process); + return $process->promise(); } public function wait(): void { - $this->scheduleTasks(); - while (count($this->inProgress)) { foreach ($this->inProgress as $process) { - $processStatus = pcntl_waitpid($process->pid(), $status, WNOHANG | WUNTRACED); - - if ($processStatus == $process->pid()) { - $isSuccess = $this->runtime->handleFinishedProcess($process); - - if ($isSuccess) { - $this->finished($process); - } else { - $this->failed($process); - } - } elseif ($processStatus == 0) { - $isRunning = $this->runtime->handleRunningProcess($process, $status); - - if (!$isRunning) { - $this->failed($process); - } - } else { - throw new Exception("Could not reliably manage process {$process->pid()}"); + if ($process->isRunning()) { + dump($process->output()); + continue; } - } - if (! count($this->inProgress)) { - break; + if (!$process->isSuccessful()) { + $this->markAsFailed($process); + + continue; + } + + $this->markAsFinished($process); } - usleep(100000); + if (count($this->inProgress)) { + usleep(100000); + } } } - public function queueTask(Task $task): void + public function putInQueue(ParallelProcess $process): void { - $this->tasks[] = $task; + $this->queue[$process->internalId()] = $process; $this->notify(); } - public function queue(Process $process): void + public function putInProgress(ParallelProcess $process): void { - $this->queue[$process->internalId()] = $process; + $process->start(); - $this->notify(); - } + $process->process()->wait(); - public function inProgress(Process $process): void - { unset($this->queue[$process->internalId()]); - $this->inProgress[$process->pid()] = $process; + $this->inProgress[$process->internalId()] = $process; } - public function finished(Process $process): void + public function markAsFinished(ParallelProcess $process): void { - unset($this->inProgress[$process->pid()]); + $process->promise()->resolve($process->output()); - $this->finished[$process->pid()] = $process; + unset($this->inProgress[$process->internalId()]); + + $this->finished[$process->internalId()] = $process; $this->notify(); } - public function failed(Process $process): void + public function markAsFailed(ParallelProcess $process): void { - unset($this->inProgress[$process->pid()]); + $process->promise()->reject($process->errorOutput()); + + unset($this->inProgress[$process->internalId()]); - $this->failed[$process->pid()] = $process; + $this->failed[$process->internalId()] = $process; $this->notify(); } @@ -197,26 +160,8 @@ public function offsetUnset($offset) // TODO } - protected function scheduleTasks(?int $amount = null): void - { - $amount = $amount ?? count($this->tasks); - - $tasksToRun = array_splice($this->tasks, 0, $amount); - - if (! count($tasksToRun)) { - return; - } - - $this->add(new CallableProcess(function () use ($tasksToRun) { - /** @var \Spatie\Async\Task $task */ - foreach ($tasksToRun as $task) { - $task->execute(); - } - })); - } - /** - * @return \Spatie\Async\Process[] + * @return \Spatie\Async\ParallelProcess[] */ public function getFinished(): array { @@ -224,7 +169,7 @@ public function getFinished(): array } /** - * @return \Spatie\Async\Process[] + * @return \Spatie\Async\ParallelProcess[] */ public function getFailed(): array { diff --git a/src/Process.php b/src/Process.php deleted file mode 100644 index d4919e97..00000000 --- a/src/Process.php +++ /dev/null @@ -1,119 +0,0 @@ -internalId; - } - - public function pid(): string - { - return $this->pid; - } - - public function socket() - { - return $this->socket; - } - - public function startTime(): int - { - return $this->startTime; - } - - public function hasTasks(): bool - { - return count($this->tasks) > 0; - } - - public function then(callable $callback): self - { - $this->successCallback = $callback; - - return $this; - } - - public function catch(callable $callback): self - { - $this->errorCallback = $callback; - - return $this; - } - - public function timeout(callable $callback): self - { - $this->timeoutCallback = $callback; - - return $this; - } - - public function triggerSuccess($output = null) - { - if (! $this->successCallback) { - return; - } - - return call_user_func_array($this->successCallback, [$output]); - } - - public function triggerError($output = null) - { - if (! $this->errorCallback) { - return; - } - - return call_user_func_array($this->errorCallback, [$output]); - } - - public function triggerTimeout($output = null) - { - if (! $this->timeoutCallback) { - return; - } - - return call_user_func_array($this->timeoutCallback, [$output]); - } - - public function setInternalId($internalId): self - { - $this->internalId = $internalId; - - return $this; - } - - public function setPid($pid): self - { - $this->pid = $pid; - - return $this; - } - - public function setSocket($socket): self - { - $this->socket = $socket; - - return $this; - } - - public function setStartTime($startTime): self - { - $this->startTime = $startTime; - - return $this; - } -} diff --git a/src/Runtime.php b/src/Runtime.php deleted file mode 100644 index bdf8380c..00000000 --- a/src/Runtime.php +++ /dev/null @@ -1,100 +0,0 @@ -maximumExecutionTime = $maximumExecutionTime; - - return $this; - } - - public function start(Process $process): Process - { - socket_create_pair(AF_UNIX, SOCK_STREAM, 0, $sockets); - - [$parentSocket, $childSocket] = $sockets; - - if (($pid = pcntl_fork()) == 0) { - socket_close($childSocket); - - $this->executeChildProcess($parentSocket, $process); - - exit; - } - - socket_close($parentSocket); - - return $process - ->setPid($pid) - ->setSocket($childSocket) - ->setStartTime(time()); - } - - public function handleFinishedProcess(Process $process): bool - { - $output = $this->readProcessOutputFromSocket($process); - - if (!$output->isSuccess()) { - $process->triggerError($output->payload()); - - return false; - } - - $process->triggerSuccess($output->payload()); - - return true; - } - - public function handleRunningProcess(Process $process, $status): bool - { - if ($process->startTime() + $this->maximumExecutionTime < time() || pcntl_wifstopped($status)) { - if (! posix_kill($process->pid(), SIGKILL)) { - throw new Exception("Failed to kill {$process->pid()}: ".posix_strerror(posix_get_last_error())); - } - - $process->triggerTimeout(); - - return false; - } - - return true; - } - - protected function executeChildProcess($parentSocket, Process $process): void - { - try { - $output = ProcessOutput::create($process->execute())->setSuccess(); - } catch (Throwable $e) { - $output = ErrorProcessOutput::create($e); - } - - socket_write($parentSocket, $output->serialize()); - - socket_close($parentSocket); - } - - protected function readProcessOutputFromSocket(Process $process): ProcessOutput - { - $rawOutput = ''; - - while ($buffer = socket_read($process->socket(), 1024)) { - $rawOutput .= $buffer; - } - - $output = unserialize($rawOutput); - - socket_close($process->socket()); - - return $output; - } -} diff --git a/src/Runtime/ChildProcess.php b/src/Runtime/ChildProcess.php new file mode 100644 index 00000000..35009297 --- /dev/null +++ b/src/Runtime/ChildProcess.php @@ -0,0 +1,43 @@ +unserialize($serializedClosure); + +try { + $output = serialize($closure()); + + echo $output; + + exit(0); +} catch (Throwable $e) { + exit(1); +} diff --git a/src/Runtime/ParentRuntime.php b/src/Runtime/ParentRuntime.php new file mode 100644 index 00000000..7111a4aa --- /dev/null +++ b/src/Runtime/ParentRuntime.php @@ -0,0 +1,55 @@ +serialize($closure); + + $input = new InputStream(); + + $input->write(self::$autoloader); + $input->write("\r\n"); + $input->write($serializedClosure); + + $process = new Process('php ' . self::$childProcessScript); + $process->setInput($input); + + $input->close(); + + return ParallelProcess::create($process, $input); + } +} diff --git a/src/Task.php b/src/Task.php deleted file mode 100644 index 03c1d0c0..00000000 --- a/src/Task.php +++ /dev/null @@ -1,8 +0,0 @@ -promise(); } } diff --git a/tests/ChildProcessBootstrapTest.php b/tests/ChildProcessBootstrapTest.php new file mode 100644 index 00000000..24a82eaf --- /dev/null +++ b/tests/ChildProcessBootstrapTest.php @@ -0,0 +1,41 @@ +serialize(function () { + echo 'child'; + }); + + $input = new InputStream(); + $input->write($autoloader); + $input->write("\r\n"); + $input->write($serializedClosure); + + $process = new Process("php {$bootstrap}"); + $process->setInput($input); + + $process->start(); + + $input->close(); + + $process->wait(); + + $this->assertEquals('child', $process->getOutput()); + } +} diff --git a/tests/MyTask.php b/tests/MyTask.php deleted file mode 100644 index 00aa60c8..00000000 --- a/tests/MyTask.php +++ /dev/null @@ -1,20 +0,0 @@ -callable = $callable; - } - - public function execute() - { - call_user_func($this->callable); - } -} diff --git a/tests/PoolTest.php b/tests/PoolTest.php index 91f3ee46..e9d9331c 100644 --- a/tests/PoolTest.php +++ b/tests/PoolTest.php @@ -4,7 +4,6 @@ use Exception; use PHPUnit\Framework\TestCase; -use Spatie\Async\Tests\MyTask; class PoolTest extends TestCase { @@ -67,7 +66,7 @@ public function it_can_handle_timeout() for ($i = 0; $i < 5; $i++) { $pool->add(function () { sleep(1); - })->timeout(function () { + })->then(function() {}, function () { $this->counter += 1; }); } @@ -85,7 +84,7 @@ public function it_can_handle_exceptions() for ($i = 0; $i < 5; $i++) { $pool->add(function () { throw new Exception('test'); - })->catch(function (Exception $e) { + })->then(function () {}, function (Exception $e) { $this->assertEquals('test', $e->getMessage()); $this->counter += 1; From 6499529831856e37f7f461d3423b7b0624d3f99b Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Sat, 16 Dec 2017 15:07:52 +0100 Subject: [PATCH 02/50] WIP --- src/ParallelProcess.php | 1 + src/Pool.php | 3 +-- src/Runtime/ChildProcess.php | 22 ++++++++++------------ tests/PoolTest.php | 12 ++++++++---- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/ParallelProcess.php b/src/ParallelProcess.php index b9e9c974..3158df18 100644 --- a/src/ParallelProcess.php +++ b/src/ParallelProcess.php @@ -29,6 +29,7 @@ public static function create(Process $process, InputStream $inputStream): self public function start(): self { $this->process->start(); + $this->inputStream->close(); return $this; diff --git a/src/Pool.php b/src/Pool.php index 49aafecb..c607c5e1 100644 --- a/src/Pool.php +++ b/src/Pool.php @@ -79,7 +79,6 @@ public function wait(): void while (count($this->inProgress)) { foreach ($this->inProgress as $process) { if ($process->isRunning()) { - dump($process->output()); continue; } @@ -93,7 +92,7 @@ public function wait(): void } if (count($this->inProgress)) { - usleep(100000); + usleep(10000); } } } diff --git a/src/Runtime/ChildProcess.php b/src/Runtime/ChildProcess.php index 35009297..676ed661 100644 --- a/src/Runtime/ChildProcess.php +++ b/src/Runtime/ChildProcess.php @@ -28,16 +28,14 @@ require_once $autoloader; -$serializer = new SuperClosure\Serializer(); - -$closure = $serializer->unserialize($serializedClosure); - -try { - $output = serialize($closure()); - - echo $output; - - exit(0); -} catch (Throwable $e) { - exit(1); +if ($serializedClosure) { + $serializer = new SuperClosure\Serializer(); + + $closure = $serializer->unserialize($serializedClosure); +} else { + $closure = function () { + return 'hi'; + }; } + +fputs(STDOUT, serialize($closure())); diff --git a/tests/PoolTest.php b/tests/PoolTest.php index e9d9331c..9456a213 100644 --- a/tests/PoolTest.php +++ b/tests/PoolTest.php @@ -35,8 +35,8 @@ public function it_can_run_processes_in_parallel() $executionTime = $endTime - $startTime; - $this->assertTrue($executionTime >= 0.1); - $this->assertTrue($executionTime < 0.2); + $this->assertTrue($executionTime >= 0.1, "Execution time was {$executionTime}, expected more than 0.1."); + $this->assertTrue($executionTime < 0.2, "Execution time was {$executionTime}, expected less than 0.2."); } /** @test */ @@ -48,12 +48,12 @@ public function it_can_handle_success() $pool->add(function () { return 2; })->then(function (int $output) { - $this->counter += $output; + OutputResult::$success += $output; }); } $pool->wait(); - +dd(OutputResult::$success); $this->assertEquals(10, $this->counter); } @@ -169,3 +169,7 @@ public function it_can_run_tasks_bundled() $this->assertCount(4, $pool->getFinished()); } } + +class OutputResult { + public static $success = 0; +} From 8a4d5c70f847d9cf60ed77583c2d2cddbf00cb0e Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Sat, 16 Dec 2017 17:38:13 +0100 Subject: [PATCH 03/50] Add basic Exception handling --- src/Output/SerializableException.php | 27 ++++++++ src/ParallelProcess.php | 66 +++++++++++++++---- src/Pool.php | 45 ++++++++----- src/Runtime/ChildProcess.php | 25 +++++--- tests/ChildProcessBootstrapTest.php | 2 +- tests/PoolTest.php | 95 +++++++++++++--------------- 6 files changed, 169 insertions(+), 91 deletions(-) create mode 100644 src/Output/SerializableException.php diff --git a/src/Output/SerializableException.php b/src/Output/SerializableException.php new file mode 100644 index 00000000..e6fb8d39 --- /dev/null +++ b/src/Output/SerializableException.php @@ -0,0 +1,27 @@ +class = get_class($e); + $this->message = $e->getMessage(); + $this->trace = $e->getTraceAsString(); + } + + public function asThrowable(): Throwable + { + /** @var Throwable $throwable */ + $throwable = new $this->class($this->message); + + return $throwable; + } +} diff --git a/src/ParallelProcess.php b/src/ParallelProcess.php index 3158df18..4a54e458 100644 --- a/src/ParallelProcess.php +++ b/src/ParallelProcess.php @@ -2,7 +2,7 @@ namespace Spatie\Async; -use GuzzleHttp\Promise\Promise; +use Spatie\Async\Output\SerializableException; use Symfony\Component\Process\InputStream; use Symfony\Component\Process\Process; @@ -10,15 +10,17 @@ class ParallelProcess { protected $process; protected $inputStream; - protected $promise; - protected $internalId; + protected $id; + + protected $successCallbacks = []; + protected $errorCallbacks = []; + protected $timeoutCallbacks = []; public function __construct(Process $process, InputStream $inputStream) { $this->process = $process; $this->inputStream = $inputStream; - $this->promise = new Promise(); - $this->internalId = uniqid(getmypid()); + $this->id = uniqid(getmypid()); } public static function create(Process $process, InputStream $inputStream): self @@ -26,6 +28,27 @@ public static function create(Process $process, InputStream $inputStream): self return new self($process, $inputStream); } + public function then(callable $callback): self + { + $this->successCallbacks[] = $callback; + + return $this; + } + + public function catch(callable $callback): self + { + $this->errorCallbacks[] = $callback; + + return $this; + } + + public function timeout(callable $callback): self + { + $this->timeoutCallbacks[] = $callback; + + return $this; + } + public function start(): self { $this->process->start(); @@ -57,7 +80,7 @@ public function output() public function errorOutput() { - return $this->process->getErrorOutput(); + return unserialize($this->process->getErrorOutput()); } public function process(): Process @@ -65,18 +88,37 @@ public function process(): Process return $this->process; } - public function promise(): Promise + public function id(): string { - return $this->promise; + return $this->id; } - public function internalId(): string + public function triggerSuccess() { - return $this->internalId; + $output = $this->output(); + + foreach ($this->successCallbacks as $callback) { + call_user_func_array($callback, [$output]); + } + } + + public function triggerError() + { + $output = $this->errorOutput(); + + if ($output instanceof SerializableException) { + $output = $output->asThrowable(); + } + + foreach ($this->errorCallbacks as $callback) { + call_user_func_array($callback, [$output]); + } } - public function pid(): ?string + public function triggerTimeout() { - return $this->process->getPid(); + foreach ($this->timeoutCallbacks as $callback) { + call_user_func_array($callback, []); + } } } diff --git a/src/Pool.php b/src/Pool.php index c607c5e1..aedbbf86 100644 --- a/src/Pool.php +++ b/src/Pool.php @@ -3,14 +3,14 @@ namespace Spatie\Async; use ArrayAccess; -use GuzzleHttp\Promise\Promise; use Spatie\Async\Runtime\ParentRuntime; +use Symfony\Component\Process\Exception\ProcessTimedOutException; class Pool implements ArrayAccess { protected $concurrency = 20; protected $tasksPerProcess = 1; - protected $maximumExecutionTime = 300; + protected $timeout = 300; /** @var \Spatie\Async\ParallelProcess[] */ protected $queue = []; @@ -43,9 +43,9 @@ public function tasksPerProcess(int $tasksPerProcess): self return $this; } - public function maximumExecutionTime(int $maximumExecutionTime): self + public function timeout(int $timeout): self { - $this->maximumExecutionTime = $maximumExecutionTime; + $this->timeout = $timeout; return $this; } @@ -65,13 +65,13 @@ public function notify(): void $this->putInProgress($process); } - public function add(callable $callable): Promise + public function add(callable $callable): ParallelProcess { $process = ParentRuntime::createChildProcess($callable); $this->putInQueue($process); - return $process->promise(); + return $process; } public function wait(): void @@ -99,40 +99,51 @@ public function wait(): void public function putInQueue(ParallelProcess $process): void { - $this->queue[$process->internalId()] = $process; + $this->queue[$process->id()] = $process; $this->notify(); } public function putInProgress(ParallelProcess $process): void { - $process->start(); + $process->process()->setTimeout($this->timeout); - $process->process()->wait(); + $process->start(); - unset($this->queue[$process->internalId()]); + unset($this->queue[$process->id()]); - $this->inProgress[$process->internalId()] = $process; + $this->inProgress[$process->id()] = $process; } public function markAsFinished(ParallelProcess $process): void { - $process->promise()->resolve($process->output()); + $process->triggerSuccess(); + + unset($this->inProgress[$process->id()]); + + $this->finished[$process->id()] = $process; + + $this->notify(); + } + + public function markAsTimeout(ParallelProcess $process): void + { + $process->triggerTimeout(); - unset($this->inProgress[$process->internalId()]); + unset($this->inProgress[$process->id()]); - $this->finished[$process->internalId()] = $process; + $this->failed[$process->id()] = $process; $this->notify(); } public function markAsFailed(ParallelProcess $process): void { - $process->promise()->reject($process->errorOutput()); + $process->triggerError(); - unset($this->inProgress[$process->internalId()]); + unset($this->inProgress[$process->id()]); - $this->failed[$process->internalId()] = $process; + $this->failed[$process->id()] = $process; $this->notify(); } diff --git a/src/Runtime/ChildProcess.php b/src/Runtime/ChildProcess.php index 676ed661..e1509ac3 100644 --- a/src/Runtime/ChildProcess.php +++ b/src/Runtime/ChildProcess.php @@ -28,14 +28,21 @@ require_once $autoloader; -if ($serializedClosure) { - $serializer = new SuperClosure\Serializer(); - - $closure = $serializer->unserialize($serializedClosure); -} else { - $closure = function () { - return 'hi'; - }; +$serializer = new SuperClosure\Serializer(); + +$closure = $serializer->unserialize($serializedClosure); + +try { + $output = call_user_func($closure); + + fputs(STDOUT, serialize($output)); + + exit(0); +} catch (Throwable $e) { + $output = new \Spatie\Async\Output\SerializableException($e); + + fputs(STDERR, serialize($output)); + + exit(1); } -fputs(STDOUT, serialize($closure())); diff --git a/tests/ChildProcessBootstrapTest.php b/tests/ChildProcessBootstrapTest.php index 24a82eaf..8e510a56 100644 --- a/tests/ChildProcessBootstrapTest.php +++ b/tests/ChildProcessBootstrapTest.php @@ -36,6 +36,6 @@ public function it_can_run() $process->wait(); - $this->assertEquals('child', $process->getOutput()); + $this->assertContains('child', $process->getOutput()); } } diff --git a/tests/PoolTest.php b/tests/PoolTest.php index 9456a213..4a45887e 100644 --- a/tests/PoolTest.php +++ b/tests/PoolTest.php @@ -7,13 +7,9 @@ class PoolTest extends TestCase { - protected $counter = 0; - protected function setUp() { parent::setUp(); - - $this->counter = 0; } /** @test */ @@ -35,7 +31,6 @@ public function it_can_run_processes_in_parallel() $executionTime = $endTime - $startTime; - $this->assertTrue($executionTime >= 0.1, "Execution time was {$executionTime}, expected more than 0.1."); $this->assertTrue($executionTime < 0.2, "Execution time was {$executionTime}, expected less than 0.2."); } @@ -44,36 +39,40 @@ public function it_can_handle_success() { $pool = Pool::create(); + $counter = 0; + for ($i = 0; $i < 5; $i++) { $pool->add(function () { return 2; - })->then(function (int $output) { - OutputResult::$success += $output; + })->then(function (int $output) use (&$counter) { + $counter += $output; }); } $pool->wait(); -dd(OutputResult::$success); - $this->assertEquals(10, $this->counter); + + $this->assertEquals(10, $counter); } /** @test */ public function it_can_handle_timeout() { $pool = Pool::create() - ->maximumExecutionTime(0); + ->timeout(1); + + $counter = 0; for ($i = 0; $i < 5; $i++) { $pool->add(function () { - sleep(1); - })->then(function() {}, function () { - $this->counter += 1; + sleep(2); + })->timeout(function () use (&$counter) { + $counter += 1; }); } $pool->wait(); - $this->assertEquals(5, $this->counter); + $this->assertEquals(5, $counter); } /** @test */ @@ -84,16 +83,13 @@ public function it_can_handle_exceptions() for ($i = 0; $i < 5; $i++) { $pool->add(function () { throw new Exception('test'); - })->then(function () {}, function (Exception $e) { + })->catch(function (Exception $e) { $this->assertEquals('test', $e->getMessage()); - - $this->counter += 1; }); } $pool->wait(); - $this->assertEquals(5, $this->counter); $this->assertCount(5, $pool->getFailed()); } @@ -108,8 +104,6 @@ public function it_can_handle_a_maximum_of_concurrent_processes() for ($i = 0; $i < 5; $i++) { $pool->add(function () { usleep(1000); - })->then(function () { - $this->counter += 1; }); } @@ -120,7 +114,6 @@ public function it_can_handle_a_maximum_of_concurrent_processes() $executionTime = $endTime - $startTime; $this->assertTrue($executionTime >= 0.5); - $this->assertEquals(5, $this->counter); $this->assertCount(5, $pool->getFinished()); } @@ -129,47 +122,45 @@ public function it_works_with_helper_functions() { $pool = Pool::create(); + $counter = 0; + for ($i = 0; $i < 5; $i++) { $pool[] = async(function () { usleep(random_int(10, 1000)); return 2; - })->then(function (int $output) { - $this->counter += $output; - }); - } - - await($pool); - - $this->assertEquals(10, $this->counter); - } - - /** @test */ - public function it_can_run_tasks_bundled() - { - $pool = Pool::create() - ->tasksPerProcess(2); - - $timeStart = microtime(true); - - for ($i = 0; $i < 7; $i++) { - $pool[] = new MyTask(function () { - sleep(1); + })->then(function (int $output) use (&$counter) { + $counter += $output; }); } await($pool); - $timeEnd = microtime(true); - - $executionTime = $timeEnd - $timeStart; - - $this->assertTrue($executionTime >= 2); - $this->assertTrue($executionTime < 3); - $this->assertCount(4, $pool->getFinished()); + $this->assertEquals(10, $counter); } -} -class OutputResult { - public static $success = 0; +// /** @test */ +// public function it_can_run_tasks_bundled() +// { +// $pool = Pool::create() +// ->tasksPerProcess(2); +// +// $timeStart = microtime(true); +// +// for ($i = 0; $i < 7; $i++) { +// $pool[] = new MyTask(function () { +// sleep(1); +// }); +// } +// +// await($pool); +// +// $timeEnd = microtime(true); +// +// $executionTime = $timeEnd - $timeStart; +// +// $this->assertTrue($executionTime >= 2); +// $this->assertTrue($executionTime < 3); +// $this->assertCount(4, $pool->getFinished()); +// } } From 7a6fc9ba96b07715d785811afdc3d208ddd6f226 Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Mon, 18 Dec 2017 09:01:38 +0100 Subject: [PATCH 04/50] WIP --- src/Pool.php | 6 ++++-- src/helpers.php | 4 ++-- tests/PoolTest.php | 50 +++++++++++++++++++++++----------------------- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/Pool.php b/src/Pool.php index aedbbf86..5b810611 100644 --- a/src/Pool.php +++ b/src/Pool.php @@ -65,9 +65,11 @@ public function notify(): void $this->putInProgress($process); } - public function add(callable $callable): ParallelProcess + public function add($process): ParallelProcess { - $process = ParentRuntime::createChildProcess($callable); + if (!$process instanceof ParallelProcess) { + $process = ParentRuntime::createChildProcess($process); + } $this->putInQueue($process); diff --git a/src/helpers.php b/src/helpers.php index 97ad9d7a..88f5282f 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -4,9 +4,9 @@ use Spatie\Async\Runtime\ParentRuntime; if (! function_exists('async')) { - function async(callable $callable): \GuzzleHttp\Promise\Promise + function async(callable $callable): \Spatie\Async\ParallelProcess { - return ParentRuntime::createChildProcess($callable)->promise(); + return ParentRuntime::createChildProcess($callable); } } diff --git a/tests/PoolTest.php b/tests/PoolTest.php index 4a45887e..2446c440 100644 --- a/tests/PoolTest.php +++ b/tests/PoolTest.php @@ -113,7 +113,7 @@ public function it_can_handle_a_maximum_of_concurrent_processes() $executionTime = $endTime - $startTime; - $this->assertTrue($executionTime >= 0.5); + $this->assertTrue($executionTime >= 0.2, "Execution time was {$executionTime}, expected more than 0.2."); $this->assertCount(5, $pool->getFinished()); } @@ -139,28 +139,28 @@ public function it_works_with_helper_functions() $this->assertEquals(10, $counter); } -// /** @test */ -// public function it_can_run_tasks_bundled() -// { -// $pool = Pool::create() -// ->tasksPerProcess(2); -// -// $timeStart = microtime(true); -// -// for ($i = 0; $i < 7; $i++) { -// $pool[] = new MyTask(function () { -// sleep(1); -// }); -// } -// -// await($pool); -// -// $timeEnd = microtime(true); -// -// $executionTime = $timeEnd - $timeStart; -// -// $this->assertTrue($executionTime >= 2); -// $this->assertTrue($executionTime < 3); -// $this->assertCount(4, $pool->getFinished()); -// } + /** @test */ + public function it_can_run_tasks_bundled() + { + $pool = Pool::create() + ->tasksPerProcess(2); + + $timeStart = microtime(true); + + for ($i = 0; $i < 7; $i++) { + $pool[] = new MyTask(function () { + sleep(1); + }); + } + + await($pool); + + $timeEnd = microtime(true); + + $executionTime = $timeEnd - $timeStart; + + $this->assertTrue($executionTime >= 2); + $this->assertTrue($executionTime < 3); + $this->assertCount(4, $pool->getFinished()); + } } From 1ee671c005800bd714b390b31e380a2dfa7a9f8a Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Mon, 18 Dec 2017 13:39:00 +0100 Subject: [PATCH 05/50] Exception handling in child runtime --- src/Runtime/ChildProcess.php | 47 ++++++++++++++++++------------------ tests/PoolTest.php | 25 ------------------- 2 files changed, 24 insertions(+), 48 deletions(-) diff --git a/src/Runtime/ChildProcess.php b/src/Runtime/ChildProcess.php index e1509ac3..1250e934 100644 --- a/src/Runtime/ChildProcess.php +++ b/src/Runtime/ChildProcess.php @@ -1,48 +1,49 @@ unserialize($serializedClosure); + $closure = $serializer->unserialize($serializedClosure); -try { $output = call_user_func($closure); fputs(STDOUT, serialize($output)); exit(0); } catch (Throwable $e) { + require_once __DIR__ . '/../Output/SerializableException.php'; + $output = new \Spatie\Async\Output\SerializableException($e); fputs(STDERR, serialize($output)); exit(1); } - diff --git a/tests/PoolTest.php b/tests/PoolTest.php index 2446c440..3a4353e5 100644 --- a/tests/PoolTest.php +++ b/tests/PoolTest.php @@ -138,29 +138,4 @@ public function it_works_with_helper_functions() $this->assertEquals(10, $counter); } - - /** @test */ - public function it_can_run_tasks_bundled() - { - $pool = Pool::create() - ->tasksPerProcess(2); - - $timeStart = microtime(true); - - for ($i = 0; $i < 7; $i++) { - $pool[] = new MyTask(function () { - sleep(1); - }); - } - - await($pool); - - $timeEnd = microtime(true); - - $executionTime = $timeEnd - $timeStart; - - $this->assertTrue($executionTime >= 2); - $this->assertTrue($executionTime < 3); - $this->assertCount(4, $pool->getFinished()); - } } From 02c935fe6c173245f815d2fa43e9a97aea0cb1c6 Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Thu, 21 Dec 2017 08:31:58 +0100 Subject: [PATCH 06/50] Move to signal handlers - WIP --- src/ParallelProcess.php | 17 ++++++++------- src/Pool.php | 39 +++++++++++++++++++---------------- src/Runtime/ChildProcess.php | 15 ++------------ src/Runtime/ParentRuntime.php | 22 +++++++------------- 4 files changed, 41 insertions(+), 52 deletions(-) diff --git a/src/ParallelProcess.php b/src/ParallelProcess.php index 4a54e458..a45e259e 100644 --- a/src/ParallelProcess.php +++ b/src/ParallelProcess.php @@ -3,29 +3,27 @@ namespace Spatie\Async; use Spatie\Async\Output\SerializableException; -use Symfony\Component\Process\InputStream; use Symfony\Component\Process\Process; class ParallelProcess { protected $process; - protected $inputStream; protected $id; + protected $pid; protected $successCallbacks = []; protected $errorCallbacks = []; protected $timeoutCallbacks = []; - public function __construct(Process $process, InputStream $inputStream) + public function __construct(Process $process) { $this->process = $process; - $this->inputStream = $inputStream; $this->id = uniqid(getmypid()); } - public static function create(Process $process, InputStream $inputStream): self + public static function create(Process $process): self { - return new self($process, $inputStream); + return new self($process); } public function then(callable $callback): self @@ -53,7 +51,7 @@ public function start(): self { $this->process->start(); - $this->inputStream->close(); + $this->pid = $this->process->getPid(); return $this; } @@ -93,6 +91,11 @@ public function id(): string return $this->id; } + public function pid(): ?string + { + return $this->pid; + } + public function triggerSuccess() { $output = $this->output(); diff --git a/src/Pool.php b/src/Pool.php index 5b810611..29472308 100644 --- a/src/Pool.php +++ b/src/Pool.php @@ -5,6 +5,7 @@ use ArrayAccess; use Spatie\Async\Runtime\ParentRuntime; use Symfony\Component\Process\Exception\ProcessTimedOutException; +use Symfony\Component\Process\Process; class Pool implements ArrayAccess { @@ -78,24 +79,26 @@ public function add($process): ParallelProcess public function wait(): void { - while (count($this->inProgress)) { - foreach ($this->inProgress as $process) { - if ($process->isRunning()) { - continue; - } + pcntl_async_signals(true); - if (!$process->isSuccessful()) { - $this->markAsFailed($process); + pcntl_signal(SIGCHLD, function ($signo, $status) { + while (true) { + $pid = pcntl_waitpid(-1, $status, WNOHANG | WUNTRACED); - continue; + if ($pid <= 0) { + break; } - $this->markAsFinished($process); + $this->markAsFinished($this->inProgress[$pid]); } + }); - if (count($this->inProgress)) { - usleep(10000); + while ($this->inProgress) { + if (! $this->inProgress) { + break; } + + usleep(50000); } } @@ -114,16 +117,16 @@ public function putInProgress(ParallelProcess $process): void unset($this->queue[$process->id()]); - $this->inProgress[$process->id()] = $process; + $this->inProgress[$process->pid()] = $process; } public function markAsFinished(ParallelProcess $process): void { $process->triggerSuccess(); - unset($this->inProgress[$process->id()]); + unset($this->inProgress[$process->pid()]); - $this->finished[$process->id()] = $process; + $this->finished[$process->pid()] = $process; $this->notify(); } @@ -132,9 +135,9 @@ public function markAsTimeout(ParallelProcess $process): void { $process->triggerTimeout(); - unset($this->inProgress[$process->id()]); + unset($this->inProgress[$process->pid()]); - $this->failed[$process->id()] = $process; + $this->failed[$process->pid()] = $process; $this->notify(); } @@ -143,9 +146,9 @@ public function markAsFailed(ParallelProcess $process): void { $process->triggerError(); - unset($this->inProgress[$process->id()]); + unset($this->inProgress[$process->pid()]); - $this->failed[$process->id()] = $process; + $this->failed[$process->pid()] = $process; $this->notify(); } diff --git a/src/Runtime/ChildProcess.php b/src/Runtime/ChildProcess.php index 1250e934..e3086d03 100644 --- a/src/Runtime/ChildProcess.php +++ b/src/Runtime/ChildProcess.php @@ -1,19 +1,8 @@ serialize($closure); + $process = new Process(implode(' ', [ + 'exec php', + self::$childProcessScript, + self::$autoloader, + base64_encode(self::$serializer->serialize($callable)) + ])); - $input = new InputStream(); - - $input->write(self::$autoloader); - $input->write("\r\n"); - $input->write($serializedClosure); - - $process = new Process('php ' . self::$childProcessScript); - $process->setInput($input); - - $input->close(); - - return ParallelProcess::create($process, $input); + return ParallelProcess::create($process); } } From cbec9582fa0b4cc6b852f05f40914ddfbca7adfb Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Thu, 21 Dec 2017 08:54:12 +0100 Subject: [PATCH 07/50] Correctly handle exit codes --- src/ParallelProcess.php | 15 +++++++++++++-- src/Pool.php | 16 ++++++++++++++-- tests/ChildProcessBootstrapTest.php | 15 +++------------ 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/ParallelProcess.php b/src/ParallelProcess.php index a45e259e..f70ce175 100644 --- a/src/ParallelProcess.php +++ b/src/ParallelProcess.php @@ -15,6 +15,9 @@ class ParallelProcess protected $errorCallbacks = []; protected $timeoutCallbacks = []; + protected $output; + protected $errorOutput; + public function __construct(Process $process) { $this->process = $process; @@ -73,12 +76,20 @@ public function isTerminated(): bool public function output() { - return unserialize($this->process->getOutput()); + if (!$this->output) { + $this->output = unserialize($this->process->getOutput()); + } + + return $this->output; } public function errorOutput() { - return unserialize($this->process->getErrorOutput()); + if (!$this->errorOutput) { + $this->errorOutput = unserialize($this->process->getErrorOutput()); + } + + return $this->errorOutput; } public function process(): Process diff --git a/src/Pool.php b/src/Pool.php index 29472308..3a38690a 100644 --- a/src/Pool.php +++ b/src/Pool.php @@ -83,13 +83,25 @@ public function wait(): void pcntl_signal(SIGCHLD, function ($signo, $status) { while (true) { - $pid = pcntl_waitpid(-1, $status, WNOHANG | WUNTRACED); + $pid = pcntl_waitpid(-1, $processState, WNOHANG | WUNTRACED); if ($pid <= 0) { break; } - $this->markAsFinished($this->inProgress[$pid]); + $process = $this->inProgress[$pid] ?? null; + + if (!$process) { + continue; + } + + if ($status['status'] === 0) { + $this->markAsFinished($process); + + continue; + } + + $this->markAsFailed($process); } }); diff --git a/tests/ChildProcessBootstrapTest.php b/tests/ChildProcessBootstrapTest.php index 8e510a56..b3ae7253 100644 --- a/tests/ChildProcessBootstrapTest.php +++ b/tests/ChildProcessBootstrapTest.php @@ -4,7 +4,6 @@ use PHPUnit\Framework\TestCase; use SuperClosure\Serializer; -use Symfony\Component\Process\InputStream; use Symfony\Component\Process\Process; class ChildProcessBootstrapTest extends TestCase @@ -18,22 +17,14 @@ public function it_can_run() $serializer = new Serializer(); - $serializedClosure = $serializer->serialize(function () { + $serializedClosure = base64_encode($serializer->serialize(function () { echo 'child'; - }); + })); - $input = new InputStream(); - $input->write($autoloader); - $input->write("\r\n"); - $input->write($serializedClosure); - - $process = new Process("php {$bootstrap}"); - $process->setInput($input); + $process = new Process("php {$bootstrap} {$autoloader} {$serializedClosure}"); $process->start(); - $input->close(); - $process->wait(); $this->assertContains('child', $process->getOutput()); From d9de98186d0bc25e9d48cffbc10751bd1cdb711c Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Thu, 21 Dec 2017 08:54:40 +0100 Subject: [PATCH 08/50] Code cleanup --- src/Pool.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Pool.php b/src/Pool.php index 3a38690a..51901662 100644 --- a/src/Pool.php +++ b/src/Pool.php @@ -4,8 +4,6 @@ use ArrayAccess; use Spatie\Async\Runtime\ParentRuntime; -use Symfony\Component\Process\Exception\ProcessTimedOutException; -use Symfony\Component\Process\Process; class Pool implements ArrayAccess { From a9edf742930ef4741fa1bc140255dee4fbc2df95 Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Thu, 21 Dec 2017 19:25:18 +0100 Subject: [PATCH 09/50] Add timeout support --- src/ParallelProcess.php | 3 +++ src/Pool.php | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/src/ParallelProcess.php b/src/ParallelProcess.php index f70ce175..a7deeaf5 100644 --- a/src/ParallelProcess.php +++ b/src/ParallelProcess.php @@ -10,6 +10,7 @@ class ParallelProcess protected $process; protected $id; protected $pid; + protected $startTime; protected $successCallbacks = []; protected $errorCallbacks = []; @@ -52,6 +53,8 @@ public function timeout(callable $callback): self public function start(): self { + $this->startTime = microtime(true); + $this->process->start(); $this->pid = $this->process->getPid(); diff --git a/src/Pool.php b/src/Pool.php index 51901662..edf1d290 100644 --- a/src/Pool.php +++ b/src/Pool.php @@ -4,6 +4,7 @@ use ArrayAccess; use Spatie\Async\Runtime\ParentRuntime; +use Symfony\Component\Process\Exception\ProcessTimedOutException; class Pool implements ArrayAccess { @@ -104,6 +105,14 @@ public function wait(): void }); while ($this->inProgress) { + foreach ($this->inProgress as $process) { + try { + $process->process()->checkTimeout(); + } catch (ProcessTimedOutException $e) { + $this->markAsTimeout($process); + } + } + if (! $this->inProgress) { break; } From fe089acdad652070d9b085d45f1f48432969ff46 Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Thu, 21 Dec 2017 19:25:45 +0100 Subject: [PATCH 10/50] Code cleanup --- src/ParallelProcess.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/ParallelProcess.php b/src/ParallelProcess.php index a7deeaf5..f70ce175 100644 --- a/src/ParallelProcess.php +++ b/src/ParallelProcess.php @@ -10,7 +10,6 @@ class ParallelProcess protected $process; protected $id; protected $pid; - protected $startTime; protected $successCallbacks = []; protected $errorCallbacks = []; @@ -53,8 +52,6 @@ public function timeout(callable $callback): self public function start(): self { - $this->startTime = microtime(true); - $this->process->start(); $this->pid = $this->process->getPid(); From 8ed5c5bd516037282973afc14cc9209767bfd8e4 Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Thu, 21 Dec 2017 19:29:00 +0100 Subject: [PATCH 11/50] Code cleanup --- src/Runtime/{ChildProcess.php => ChildRuntime.php} | 0 src/Runtime/ParentRuntime.php | 2 +- src/Task.php | 8 ++++++++ ...ChildProcessBootstrapTest.php => ChildRuntimeTest.php} | 4 ++-- 4 files changed, 11 insertions(+), 3 deletions(-) rename src/Runtime/{ChildProcess.php => ChildRuntime.php} (100%) create mode 100644 src/Task.php rename tests/{ChildProcessBootstrapTest.php => ChildRuntimeTest.php} (84%) diff --git a/src/Runtime/ChildProcess.php b/src/Runtime/ChildRuntime.php similarity index 100% rename from src/Runtime/ChildProcess.php rename to src/Runtime/ChildRuntime.php diff --git a/src/Runtime/ParentRuntime.php b/src/Runtime/ParentRuntime.php index 82d053e7..d6648173 100644 --- a/src/Runtime/ParentRuntime.php +++ b/src/Runtime/ParentRuntime.php @@ -25,7 +25,7 @@ public static function init() }); self::$autoloader = reset($existingAutoloaderFiles); - self::$childProcessScript = __DIR__ . '/ChildProcess.php'; + self::$childProcessScript = __DIR__ . '/ChildRuntime.php'; self::$serializer = new Serializer(); self::$isInitialised = true; diff --git a/src/Task.php b/src/Task.php new file mode 100644 index 00000000..03c1d0c0 --- /dev/null +++ b/src/Task.php @@ -0,0 +1,8 @@ + Date: Thu, 21 Dec 2017 19:36:53 +0100 Subject: [PATCH 12/50] Remove Guzzle dependency --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index 134ee8cb..ed4cc14a 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,6 @@ ], "require": { "php": "^7.1", - "guzzlehttp/promises": "^1.3", "jeremeamia/superclosure": "^2.3", "symfony/process": "^4.0" }, From efb85513a2e28fac68a49d126f2402fd3bd2de66 Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Fri, 22 Dec 2017 15:54:21 +0100 Subject: [PATCH 13/50] WIP --- composer.json | 2 +- src/ParallelProcess.php | 41 ++++++++++++++++++++++++++++------- src/Runtime/ChildRuntime.php | 8 +++---- src/Runtime/ParentRuntime.php | 11 +++++----- tests/ChildRuntimeTest.php | 9 ++++---- 5 files changed, 46 insertions(+), 25 deletions(-) diff --git a/composer.json b/composer.json index ed4cc14a..9a9cfc22 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ ], "require": { "php": "^7.1", - "jeremeamia/superclosure": "^2.3", + "opis/closure": "^3.0", "symfony/process": "^4.0" }, "require-dev": { diff --git a/src/ParallelProcess.php b/src/ParallelProcess.php index f70ce175..a70c1946 100644 --- a/src/ParallelProcess.php +++ b/src/ParallelProcess.php @@ -2,8 +2,11 @@ namespace Spatie\Async; +use Error; +use PHPUnit\Runner\Exception; use Spatie\Async\Output\SerializableException; use Symfony\Component\Process\Process; +use Throwable; class ParallelProcess { @@ -76,8 +79,14 @@ public function isTerminated(): bool public function output() { - if (!$this->output) { - $this->output = unserialize($this->process->getOutput()); + if (! $this->output) { + $processOutput = $this->process->getErrorOutput(); + + $this->output = @unserialize(base64_decode($processOutput)); + + if (! $this->output) { + $this->errorOutput = $processOutput; + } } return $this->output; @@ -85,8 +94,16 @@ public function output() public function errorOutput() { - if (!$this->errorOutput) { - $this->errorOutput = unserialize($this->process->getErrorOutput()); + if (! $this->errorOutput) { + $processOutput = $this->process->getErrorOutput(); + + $errorOutput = @unserialize(base64_decode($processOutput)); + + if (! $errorOutput) { + $errorOutput = $processOutput; + } + + $this->errorOutput = $errorOutput; } return $this->errorOutput; @@ -118,14 +135,22 @@ public function triggerSuccess() public function triggerError() { - $output = $this->errorOutput(); + $exception = $this->errorOutput(); - if ($output instanceof SerializableException) { - $output = $output->asThrowable(); + if ($exception instanceof SerializableException) { + $exception = $exception->asThrowable(); } foreach ($this->errorCallbacks as $callback) { - call_user_func_array($callback, [$output]); + call_user_func_array($callback, [$exception]); + } + + if (! $this->errorCallbacks) { + if ($exception instanceof Throwable) { + throw $exception; + } + + throw new Error($exception); } } diff --git a/src/Runtime/ChildRuntime.php b/src/Runtime/ChildRuntime.php index e3086d03..ee591200 100644 --- a/src/Runtime/ChildRuntime.php +++ b/src/Runtime/ChildRuntime.php @@ -18,13 +18,11 @@ require_once $autoloader; - $serializer = new SuperClosure\Serializer(); - - $closure = $serializer->unserialize($serializedClosure); + $closure = Opis\Closure\unserialize($serializedClosure); $output = call_user_func($closure); - fputs(STDOUT, serialize($output)); + fputs(STDOUT, base64_encode(serialize($output))); exit(0); } catch (Throwable $e) { @@ -32,7 +30,7 @@ $output = new \Spatie\Async\Output\SerializableException($e); - fputs(STDERR, serialize($output)); + fputs(STDERR, base64_encode(serialize($output))); exit(1); } diff --git a/src/Runtime/ParentRuntime.php b/src/Runtime/ParentRuntime.php index d6648173..d140a214 100644 --- a/src/Runtime/ParentRuntime.php +++ b/src/Runtime/ParentRuntime.php @@ -2,17 +2,15 @@ namespace Spatie\Async\Runtime; +use Opis\Closure\SerializableClosure; use Spatie\Async\ParallelProcess; -use SuperClosure\Serializer; -use Symfony\Component\Process\InputStream; use Symfony\Component\Process\Process; +use function Opis\Closure\serialize; class ParentRuntime { protected static $isInitialised = false; protected static $autoloader; - /** @var Serializer */ - protected static $serializer; protected static $childProcessScript; public static function init() @@ -26,7 +24,6 @@ public static function init() self::$autoloader = reset($existingAutoloaderFiles); self::$childProcessScript = __DIR__ . '/ChildRuntime.php'; - self::$serializer = new Serializer(); self::$isInitialised = true; } @@ -37,11 +34,13 @@ public static function createChildProcess(callable $callable): ParallelProcess self::init(); } + $closure = new SerializableClosure($callable); + $process = new Process(implode(' ', [ 'exec php', self::$childProcessScript, self::$autoloader, - base64_encode(self::$serializer->serialize($callable)) + base64_encode(serialize($closure)) ])); return ParallelProcess::create($process); diff --git a/tests/ChildRuntimeTest.php b/tests/ChildRuntimeTest.php index 8d40d29b..af3a4c14 100644 --- a/tests/ChildRuntimeTest.php +++ b/tests/ChildRuntimeTest.php @@ -2,9 +2,10 @@ namespace Spatie\Async\Tests; +use Opis\Closure\SerializableClosure; use PHPUnit\Framework\TestCase; -use SuperClosure\Serializer; use Symfony\Component\Process\Process; +use function Opis\Closure\serialize; class ChildRuntimeTest extends TestCase { @@ -15,11 +16,9 @@ public function it_can_run() $autoloader = __DIR__ . '/../vendor/autoload.php'; - $serializer = new Serializer(); - - $serializedClosure = base64_encode($serializer->serialize(function () { + $serializedClosure = base64_encode(serialize(new SerializableClosure(function () { echo 'child'; - })); + }))); $process = new Process("php {$bootstrap} {$autoloader} {$serializedClosure}"); From ccc6b600bbd06473da07ebd72ea287de5cc6fd76 Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Fri, 22 Dec 2017 16:02:24 +0100 Subject: [PATCH 14/50] Fix output --- src/ParallelProcess.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ParallelProcess.php b/src/ParallelProcess.php index a70c1946..9f3a6fcb 100644 --- a/src/ParallelProcess.php +++ b/src/ParallelProcess.php @@ -80,7 +80,7 @@ public function isTerminated(): bool public function output() { if (! $this->output) { - $processOutput = $this->process->getErrorOutput(); + $processOutput = $this->process->getOutput(); $this->output = @unserialize(base64_decode($processOutput)); From 569a33c3c1ccb1f1f9ed10ea414caed775c249ca Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Fri, 22 Dec 2017 16:03:38 +0100 Subject: [PATCH 15/50] code cleanup --- src/ParallelProcess.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/ParallelProcess.php b/src/ParallelProcess.php index 9f3a6fcb..dbece4d1 100644 --- a/src/ParallelProcess.php +++ b/src/ParallelProcess.php @@ -97,13 +97,11 @@ public function errorOutput() if (! $this->errorOutput) { $processOutput = $this->process->getErrorOutput(); - $errorOutput = @unserialize(base64_decode($processOutput)); + $this->errorOutput = @unserialize(base64_decode($processOutput)); - if (! $errorOutput) { - $errorOutput = $processOutput; + if (! $this->errorOutput) { + $this->errorOutput = $processOutput; } - - $this->errorOutput = $errorOutput; } return $this->errorOutput; From b281bae606000fcee4bf59377ee5cb48551b6207 Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Fri, 22 Dec 2017 16:07:44 +0100 Subject: [PATCH 16/50] Add class usage test --- tests/MyClass.php | 8 ++++++++ tests/PoolTest.php | 23 ++++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 tests/MyClass.php diff --git a/tests/MyClass.php b/tests/MyClass.php new file mode 100644 index 00000000..1b8533a8 --- /dev/null +++ b/tests/MyClass.php @@ -0,0 +1,8 @@ +assertEquals(10, $counter); } -} + + /** @test */ + public function it_can_use_a_class_from_the_parent_process() + { + $pool = Pool::create(); + + $result = null; + + $pool[] = async(function () { + $class = new MyClass(); + + return $class; + })->then(function (MyClass $class) use (&$result) { + $result = $class; + }); + + await($pool); + + $this->assertInstanceOf(MyClass::class, $result); + } +}; From de87987c61945e27cca9e8b45e2d9760ffbe95dc Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Fri, 22 Dec 2017 16:09:03 +0100 Subject: [PATCH 17/50] Improce Class test --- tests/MyClass.php | 2 +- tests/PoolTest.php | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/MyClass.php b/tests/MyClass.php index 1b8533a8..ad937915 100644 --- a/tests/MyClass.php +++ b/tests/MyClass.php @@ -4,5 +4,5 @@ class MyClass { - + public $property = null; } diff --git a/tests/PoolTest.php b/tests/PoolTest.php index 9eefdbdd..f856e1ec 100644 --- a/tests/PoolTest.php +++ b/tests/PoolTest.php @@ -145,11 +145,14 @@ public function it_can_use_a_class_from_the_parent_process() { $pool = Pool::create(); + /** @var MyClass $result */ $result = null; $pool[] = async(function () { $class = new MyClass(); + $class->property = true; + return $class; })->then(function (MyClass $class) use (&$result) { $result = $class; @@ -158,5 +161,6 @@ public function it_can_use_a_class_from_the_parent_process() await($pool); $this->assertInstanceOf(MyClass::class, $result); + $this->assertTrue($result->property); } }; From a56763619833be24debf15b492424a86cb3ef6c5 Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Fri, 22 Dec 2017 16:12:36 +0100 Subject: [PATCH 18/50] Code cleanup --- src/Pool.php | 60 +++++++++++++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/src/Pool.php b/src/Pool.php index edf1d290..1834e083 100644 --- a/src/Pool.php +++ b/src/Pool.php @@ -21,6 +21,11 @@ class Pool implements ArrayAccess /** @var \Spatie\Async\ParallelProcess[] */ protected $failed = []; + public function __construct() + { + $this->registerListener(); + } + /** * @return static */ @@ -78,32 +83,6 @@ public function add($process): ParallelProcess public function wait(): void { - pcntl_async_signals(true); - - pcntl_signal(SIGCHLD, function ($signo, $status) { - while (true) { - $pid = pcntl_waitpid(-1, $processState, WNOHANG | WUNTRACED); - - if ($pid <= 0) { - break; - } - - $process = $this->inProgress[$pid] ?? null; - - if (!$process) { - continue; - } - - if ($status['status'] === 0) { - $this->markAsFinished($process); - - continue; - } - - $this->markAsFailed($process); - } - }); - while ($this->inProgress) { foreach ($this->inProgress as $process) { try { @@ -209,4 +188,33 @@ public function getFailed(): array { return $this->failed; } + + protected function registerListener(): void + { + pcntl_async_signals(true); + + pcntl_signal(SIGCHLD, function ($signo, $status) { + while (true) { + $pid = pcntl_waitpid(-1, $processState, WNOHANG | WUNTRACED); + + if ($pid <= 0) { + break; + } + + $process = $this->inProgress[$pid] ?? null; + + if (!$process) { + continue; + } + + if ($status['status'] === 0) { + $this->markAsFinished($process); + + continue; + } + + $this->markAsFailed($process); + } + }); + } } From d8891a41025ca03f233d5ed6205dee1b8e7a7e01 Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Fri, 22 Dec 2017 16:16:10 +0100 Subject: [PATCH 19/50] Add return of `await` --- src/ParallelProcess.php | 2 ++ src/Pool.php | 8 ++++++-- src/helpers.php | 4 ++-- tests/PoolTest.php | 20 ++++++++++++++++++++ 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/ParallelProcess.php b/src/ParallelProcess.php index dbece4d1..787a1857 100644 --- a/src/ParallelProcess.php +++ b/src/ParallelProcess.php @@ -129,6 +129,8 @@ public function triggerSuccess() foreach ($this->successCallbacks as $callback) { call_user_func_array($callback, [$output]); } + + return $output; } public function triggerError() diff --git a/src/Pool.php b/src/Pool.php index 1834e083..de54b3d1 100644 --- a/src/Pool.php +++ b/src/Pool.php @@ -21,6 +21,8 @@ class Pool implements ArrayAccess /** @var \Spatie\Async\ParallelProcess[] */ protected $failed = []; + protected $results = []; + public function __construct() { $this->registerListener(); @@ -81,7 +83,7 @@ public function add($process): ParallelProcess return $process; } - public function wait(): void + public function wait(): array { while ($this->inProgress) { foreach ($this->inProgress as $process) { @@ -98,6 +100,8 @@ public function wait(): void usleep(50000); } + + return $this->results; } public function putInQueue(ParallelProcess $process): void @@ -120,7 +124,7 @@ public function putInProgress(ParallelProcess $process): void public function markAsFinished(ParallelProcess $process): void { - $process->triggerSuccess(); + $this->results[] = $process->triggerSuccess(); unset($this->inProgress[$process->pid()]); diff --git a/src/helpers.php b/src/helpers.php index 88f5282f..d808380d 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -11,8 +11,8 @@ function async(callable $callable): \Spatie\Async\ParallelProcess } if (! function_exists('await')) { - function await(Pool $pool): void + function await(Pool $pool): array { - $pool->wait(); + return $pool->wait(); } } diff --git a/tests/PoolTest.php b/tests/PoolTest.php index f856e1ec..d9d712c2 100644 --- a/tests/PoolTest.php +++ b/tests/PoolTest.php @@ -163,4 +163,24 @@ public function it_can_use_a_class_from_the_parent_process() $this->assertInstanceOf(MyClass::class, $result); $this->assertTrue($result->property); } + + /** @test */ + public function it_returns_all_the_output_as_an_array() + { + $pool = Pool::create(); + + /** @var MyClass $result */ + $result = null; + + for ($i = 0; $i < 5; $i++) { + $pool[] = async(function () { + return 2; + }); + } + + $result = await($pool); + + $this->assertCount(5, $result); + $this->assertEquals(10, array_sum($result)); + } }; From 90175504f6faac9e5a3c88577fc8faf0c1618d3c Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Fri, 22 Dec 2017 16:19:13 +0100 Subject: [PATCH 20/50] Handle timeout in a -hopefully- better way --- src/ParallelProcess.php | 17 ++++++++++++++++- src/Pool.php | 4 +--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/ParallelProcess.php b/src/ParallelProcess.php index 787a1857..0ad2602f 100644 --- a/src/ParallelProcess.php +++ b/src/ParallelProcess.php @@ -3,7 +3,6 @@ namespace Spatie\Async; use Error; -use PHPUnit\Runner\Exception; use Spatie\Async\Output\SerializableException; use Symfony\Component\Process\Process; use Throwable; @@ -21,6 +20,8 @@ class ParallelProcess protected $output; protected $errorOutput; + protected $startTime; + public function __construct(Process $process) { $this->process = $process; @@ -55,6 +56,8 @@ public function timeout(callable $callback): self public function start(): self { + $this->startTime = microtime(true); + $this->process->start(); $this->pid = $this->process->getPid(); @@ -62,6 +65,13 @@ public function start(): self return $this; } + public function stop(): self + { + $this->process->stop(10, SIGKILL); + + return $this; + } + public function isRunning(): bool { return $this->process->isRunning(); @@ -122,6 +132,11 @@ public function pid(): ?string return $this->pid; } + public function executionTime(): float + { + return microtime(true) - $this->startTime; + } + public function triggerSuccess() { $output = $this->output(); diff --git a/src/Pool.php b/src/Pool.php index de54b3d1..cf6b1562 100644 --- a/src/Pool.php +++ b/src/Pool.php @@ -87,9 +87,7 @@ public function wait(): array { while ($this->inProgress) { foreach ($this->inProgress as $process) { - try { - $process->process()->checkTimeout(); - } catch (ProcessTimedOutException $e) { + if ($process->executionTime() > $this->timeout) { $this->markAsTimeout($process); } } From 4c8a7ef8af3db82cab5533ba853ef65ea198565c Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Fri, 22 Dec 2017 16:19:40 +0100 Subject: [PATCH 21/50] Code cleanup --- src/Pool.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Pool.php b/src/Pool.php index cf6b1562..48864a26 100644 --- a/src/Pool.php +++ b/src/Pool.php @@ -4,7 +4,6 @@ use ArrayAccess; use Spatie\Async\Runtime\ParentRuntime; -use Symfony\Component\Process\Exception\ProcessTimedOutException; class Pool implements ArrayAccess { From fad9208824e0a3065b773872c889ac29417e0692 Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Fri, 22 Dec 2017 15:23:58 +0000 Subject: [PATCH 22/50] Apply fixes from StyleCI --- src/ParallelProcess.php | 5 ++--- src/Pool.php | 4 ++-- src/Runtime/ChildRuntime.php | 14 +++++++------- src/Runtime/ParentRuntime.php | 14 +++++++------- tests/ChildRuntimeTest.php | 8 ++++---- tests/PoolTest.php | 2 +- 6 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/ParallelProcess.php b/src/ParallelProcess.php index 787a1857..b1e6d870 100644 --- a/src/ParallelProcess.php +++ b/src/ParallelProcess.php @@ -3,10 +3,9 @@ namespace Spatie\Async; use Error; -use PHPUnit\Runner\Exception; -use Spatie\Async\Output\SerializableException; -use Symfony\Component\Process\Process; use Throwable; +use Symfony\Component\Process\Process; +use Spatie\Async\Output\SerializableException; class ParallelProcess { diff --git a/src/Pool.php b/src/Pool.php index de54b3d1..f6299abb 100644 --- a/src/Pool.php +++ b/src/Pool.php @@ -74,7 +74,7 @@ public function notify(): void public function add($process): ParallelProcess { - if (!$process instanceof ParallelProcess) { + if (! $process instanceof ParallelProcess) { $process = ParentRuntime::createChildProcess($process); } @@ -207,7 +207,7 @@ protected function registerListener(): void $process = $this->inProgress[$pid] ?? null; - if (!$process) { + if (! $process) { continue; } diff --git a/src/Runtime/ChildRuntime.php b/src/Runtime/ChildRuntime.php index ee591200..0c365d19 100644 --- a/src/Runtime/ChildRuntime.php +++ b/src/Runtime/ChildRuntime.php @@ -4,16 +4,16 @@ $autoloader = $argv[1]; $serializedClosure = base64_decode($argv[2]); - if (!$autoloader) { + if (! $autoloader) { throw new InvalidArgumentException('No autoloader provided in child process.'); } - if (!file_exists($autoloader)) { + if (! file_exists($autoloader)) { throw new InvalidArgumentException("Could not find autoloader in child process: {$autoloader}"); } - if (!$serializedClosure) { - throw new InvalidArgumentException("No valid closure was passed to the child process."); + if (! $serializedClosure) { + throw new InvalidArgumentException('No valid closure was passed to the child process.'); } require_once $autoloader; @@ -22,15 +22,15 @@ $output = call_user_func($closure); - fputs(STDOUT, base64_encode(serialize($output))); + fwrite(STDOUT, base64_encode(serialize($output))); exit(0); } catch (Throwable $e) { - require_once __DIR__ . '/../Output/SerializableException.php'; + require_once __DIR__.'/../Output/SerializableException.php'; $output = new \Spatie\Async\Output\SerializableException($e); - fputs(STDERR, base64_encode(serialize($output))); + fwrite(STDERR, base64_encode(serialize($output))); exit(1); } diff --git a/src/Runtime/ParentRuntime.php b/src/Runtime/ParentRuntime.php index d140a214..67ff1733 100644 --- a/src/Runtime/ParentRuntime.php +++ b/src/Runtime/ParentRuntime.php @@ -2,10 +2,10 @@ namespace Spatie\Async\Runtime; -use Opis\Closure\SerializableClosure; use Spatie\Async\ParallelProcess; -use Symfony\Component\Process\Process; use function Opis\Closure\serialize; +use Opis\Closure\SerializableClosure; +use Symfony\Component\Process\Process; class ParentRuntime { @@ -16,21 +16,21 @@ class ParentRuntime public static function init() { $existingAutoloaderFiles = array_filter([ - __DIR__ . '/../../vendor/autoload.php', - __DIR__ . '/../../../vendor/autoload.php', + __DIR__.'/../../vendor/autoload.php', + __DIR__.'/../../../vendor/autoload.php', ], function (string $path) { return file_exists($path); }); self::$autoloader = reset($existingAutoloaderFiles); - self::$childProcessScript = __DIR__ . '/ChildRuntime.php'; + self::$childProcessScript = __DIR__.'/ChildRuntime.php'; self::$isInitialised = true; } public static function createChildProcess(callable $callable): ParallelProcess { - if (!self::$isInitialised) { + if (! self::$isInitialised) { self::init(); } @@ -40,7 +40,7 @@ public static function createChildProcess(callable $callable): ParallelProcess 'exec php', self::$childProcessScript, self::$autoloader, - base64_encode(serialize($closure)) + base64_encode(serialize($closure)), ])); return ParallelProcess::create($process); diff --git a/tests/ChildRuntimeTest.php b/tests/ChildRuntimeTest.php index af3a4c14..5a3736ad 100644 --- a/tests/ChildRuntimeTest.php +++ b/tests/ChildRuntimeTest.php @@ -2,19 +2,19 @@ namespace Spatie\Async\Tests; -use Opis\Closure\SerializableClosure; use PHPUnit\Framework\TestCase; -use Symfony\Component\Process\Process; use function Opis\Closure\serialize; +use Opis\Closure\SerializableClosure; +use Symfony\Component\Process\Process; class ChildRuntimeTest extends TestCase { /** @test */ public function it_can_run() { - $bootstrap = __DIR__ . '/../src/Runtime/ChildRuntime.php'; + $bootstrap = __DIR__.'/../src/Runtime/ChildRuntime.php'; - $autoloader = __DIR__ . '/../vendor/autoload.php'; + $autoloader = __DIR__.'/../vendor/autoload.php'; $serializedClosure = base64_encode(serialize(new SerializableClosure(function () { echo 'child'; diff --git a/tests/PoolTest.php b/tests/PoolTest.php index d9d712c2..ff4249a4 100644 --- a/tests/PoolTest.php +++ b/tests/PoolTest.php @@ -183,4 +183,4 @@ public function it_returns_all_the_output_as_an_array() $this->assertCount(5, $result); $this->assertEquals(10, array_sum($result)); } -}; +} From 158142b5ea839c9e82c609b2412df31d3b2b5f4f Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Fri, 22 Dec 2017 15:26:01 +0000 Subject: [PATCH 23/50] Apply fixes from StyleCI --- src/ParallelProcess.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ParallelProcess.php b/src/ParallelProcess.php index 0ad2602f..c0f74ef1 100644 --- a/src/ParallelProcess.php +++ b/src/ParallelProcess.php @@ -3,9 +3,9 @@ namespace Spatie\Async; use Error; -use Spatie\Async\Output\SerializableException; -use Symfony\Component\Process\Process; use Throwable; +use Symfony\Component\Process\Process; +use Spatie\Async\Output\SerializableException; class ParallelProcess { From 0aaab53697c144e649c6e86127be6c31e511cdaf Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Fri, 22 Dec 2017 16:31:02 +0100 Subject: [PATCH 24/50] Improve concurrency test --- tests/PoolTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/PoolTest.php b/tests/PoolTest.php index ff4249a4..9dca4d18 100644 --- a/tests/PoolTest.php +++ b/tests/PoolTest.php @@ -98,13 +98,13 @@ public function it_can_handle_exceptions() public function it_can_handle_a_maximum_of_concurrent_processes() { $pool = Pool::create() - ->concurrency(1); + ->concurrency(2); $startTime = microtime(true); - for ($i = 0; $i < 5; $i++) { + for ($i = 0; $i < 3; $i++) { $pool->add(function () { - usleep(1000); + sleep(1); }); } @@ -114,8 +114,8 @@ public function it_can_handle_a_maximum_of_concurrent_processes() $executionTime = $endTime - $startTime; - $this->assertTrue($executionTime >= 0.2, "Execution time was {$executionTime}, expected more than 0.2."); - $this->assertCount(5, $pool->getFinished()); + $this->assertTrue($executionTime >= 2, "Execution time was {$executionTime}, expected more than 0.2."); + $this->assertCount(3, $pool->getFinished()); } /** @test */ From 153bb5587564a156e0c8e1d1e55988d871c1c545 Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Sat, 23 Dec 2017 11:04:21 +0100 Subject: [PATCH 25/50] Add configurable autoloader --- src/Pool.php | 8 ++++---- src/Runtime/ParentRuntime.php | 23 ++++++++++++++--------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/Pool.php b/src/Pool.php index 7bfb0583..03a05d07 100644 --- a/src/Pool.php +++ b/src/Pool.php @@ -42,16 +42,16 @@ public function concurrency(int $concurrency): self return $this; } - public function tasksPerProcess(int $tasksPerProcess): self + public function timeout(int $timeout): self { - $this->tasksPerProcess = $tasksPerProcess; + $this->timeout = $timeout; return $this; } - public function timeout(int $timeout): self + public function autoload(string $autoloader): self { - $this->timeout = $timeout; + ParentRuntime::init($autoloader); return $this; } diff --git a/src/Runtime/ParentRuntime.php b/src/Runtime/ParentRuntime.php index 67ff1733..5b7a47dd 100644 --- a/src/Runtime/ParentRuntime.php +++ b/src/Runtime/ParentRuntime.php @@ -13,16 +13,21 @@ class ParentRuntime protected static $autoloader; protected static $childProcessScript; - public static function init() + public static function init(string $autoloader = null) { - $existingAutoloaderFiles = array_filter([ - __DIR__.'/../../vendor/autoload.php', - __DIR__.'/../../../vendor/autoload.php', - ], function (string $path) { - return file_exists($path); - }); - - self::$autoloader = reset($existingAutoloaderFiles); + if (!$autoloader) { + $existingAutoloaderFiles = array_filter([ + __DIR__.'/../../../autoload.php', + __DIR__.'/../../vendor/autoload.php', + __DIR__.'/../../../vendor/autoload.php', + ], function (string $path) { + return file_exists($path); + }); + + $autoloader = reset($existingAutoloaderFiles); + } + + self::$autoloader = $autoloader; self::$childProcessScript = __DIR__.'/ChildRuntime.php'; self::$isInitialised = true; From 7d14f26a78ff9b6ce70fc15a74956d11c8fcd87e Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Sat, 23 Dec 2017 11:05:03 +0100 Subject: [PATCH 26/50] Add extra null checks --- src/Runtime/ChildRuntime.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Runtime/ChildRuntime.php b/src/Runtime/ChildRuntime.php index 0c365d19..43847e4f 100644 --- a/src/Runtime/ChildRuntime.php +++ b/src/Runtime/ChildRuntime.php @@ -1,8 +1,8 @@ Date: Sat, 23 Dec 2017 15:29:25 +0100 Subject: [PATCH 27/50] Code cleanup --- src/ParallelProcess.php | 16 ++++++++-------- src/Pool.php | 22 +++++++++++----------- src/Runtime/ChildRuntime.php | 2 +- src/Runtime/ParentRuntime.php | 10 +++++----- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/ParallelProcess.php b/src/ParallelProcess.php index c0f74ef1..de818651 100644 --- a/src/ParallelProcess.php +++ b/src/ParallelProcess.php @@ -87,7 +87,7 @@ public function isTerminated(): bool return $this->process->isTerminated(); } - public function output() + public function getOutput() { if (! $this->output) { $processOutput = $this->process->getOutput(); @@ -102,7 +102,7 @@ public function output() return $this->output; } - public function errorOutput() + public function getErrorOutput() { if (! $this->errorOutput) { $processOutput = $this->process->getErrorOutput(); @@ -117,29 +117,29 @@ public function errorOutput() return $this->errorOutput; } - public function process(): Process + public function getProcess(): Process { return $this->process; } - public function id(): string + public function getId(): string { return $this->id; } - public function pid(): ?string + public function getPid(): ?string { return $this->pid; } - public function executionTime(): float + public function getCurrentExecutionTime(): float { return microtime(true) - $this->startTime; } public function triggerSuccess() { - $output = $this->output(); + $output = $this->getOutput(); foreach ($this->successCallbacks as $callback) { call_user_func_array($callback, [$output]); @@ -150,7 +150,7 @@ public function triggerSuccess() public function triggerError() { - $exception = $this->errorOutput(); + $exception = $this->getErrorOutput(); if ($exception instanceof SerializableException) { $exception = $exception->asThrowable(); diff --git a/src/Pool.php b/src/Pool.php index 03a05d07..6e943de1 100644 --- a/src/Pool.php +++ b/src/Pool.php @@ -86,7 +86,7 @@ public function wait(): array { while ($this->inProgress) { foreach ($this->inProgress as $process) { - if ($process->executionTime() > $this->timeout) { + if ($process->getCurrentExecutionTime() > $this->timeout) { $this->markAsTimeout($process); } } @@ -103,29 +103,29 @@ public function wait(): array public function putInQueue(ParallelProcess $process): void { - $this->queue[$process->id()] = $process; + $this->queue[$process->getId()] = $process; $this->notify(); } public function putInProgress(ParallelProcess $process): void { - $process->process()->setTimeout($this->timeout); + $process->getProcess()->setTimeout($this->timeout); $process->start(); - unset($this->queue[$process->id()]); + unset($this->queue[$process->getId()]); - $this->inProgress[$process->pid()] = $process; + $this->inProgress[$process->getPid()] = $process; } public function markAsFinished(ParallelProcess $process): void { $this->results[] = $process->triggerSuccess(); - unset($this->inProgress[$process->pid()]); + unset($this->inProgress[$process->getPid()]); - $this->finished[$process->pid()] = $process; + $this->finished[$process->getPid()] = $process; $this->notify(); } @@ -134,9 +134,9 @@ public function markAsTimeout(ParallelProcess $process): void { $process->triggerTimeout(); - unset($this->inProgress[$process->pid()]); + unset($this->inProgress[$process->getPid()]); - $this->failed[$process->pid()] = $process; + $this->failed[$process->getPid()] = $process; $this->notify(); } @@ -145,9 +145,9 @@ public function markAsFailed(ParallelProcess $process): void { $process->triggerError(); - unset($this->inProgress[$process->pid()]); + unset($this->inProgress[$process->getPid()]); - $this->failed[$process->pid()] = $process; + $this->failed[$process->getPid()] = $process; $this->notify(); } diff --git a/src/Runtime/ChildRuntime.php b/src/Runtime/ChildRuntime.php index 43847e4f..ceb37e9a 100644 --- a/src/Runtime/ChildRuntime.php +++ b/src/Runtime/ChildRuntime.php @@ -26,7 +26,7 @@ exit(0); } catch (Throwable $e) { - require_once __DIR__.'/../Output/SerializableException.php'; + require_once __DIR__ . '/../Output/SerializableException.php'; $output = new \Spatie\Async\Output\SerializableException($e); diff --git a/src/Runtime/ParentRuntime.php b/src/Runtime/ParentRuntime.php index 5b7a47dd..dc6ba5fa 100644 --- a/src/Runtime/ParentRuntime.php +++ b/src/Runtime/ParentRuntime.php @@ -15,11 +15,11 @@ class ParentRuntime public static function init(string $autoloader = null) { - if (!$autoloader) { + if (! $autoloader) { $existingAutoloaderFiles = array_filter([ - __DIR__.'/../../../autoload.php', - __DIR__.'/../../vendor/autoload.php', - __DIR__.'/../../../vendor/autoload.php', + __DIR__ . '/../../../autoload.php', + __DIR__ . '/../../vendor/autoload.php', + __DIR__ . '/../../../vendor/autoload.php', ], function (string $path) { return file_exists($path); }); @@ -28,7 +28,7 @@ public static function init(string $autoloader = null) } self::$autoloader = $autoloader; - self::$childProcessScript = __DIR__.'/ChildRuntime.php'; + self::$childProcessScript = __DIR__ . '/ChildRuntime.php'; self::$isInitialised = true; } From 835f5d83aab022d359db35bec9d9e53797e0eb13 Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Sat, 23 Dec 2017 18:55:45 +0000 Subject: [PATCH 28/50] Apply fixes from StyleCI --- src/Runtime/ChildRuntime.php | 2 +- src/Runtime/ParentRuntime.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Runtime/ChildRuntime.php b/src/Runtime/ChildRuntime.php index ceb37e9a..43847e4f 100644 --- a/src/Runtime/ChildRuntime.php +++ b/src/Runtime/ChildRuntime.php @@ -26,7 +26,7 @@ exit(0); } catch (Throwable $e) { - require_once __DIR__ . '/../Output/SerializableException.php'; + require_once __DIR__.'/../Output/SerializableException.php'; $output = new \Spatie\Async\Output\SerializableException($e); diff --git a/src/Runtime/ParentRuntime.php b/src/Runtime/ParentRuntime.php index dc6ba5fa..3ce5de44 100644 --- a/src/Runtime/ParentRuntime.php +++ b/src/Runtime/ParentRuntime.php @@ -17,9 +17,9 @@ public static function init(string $autoloader = null) { if (! $autoloader) { $existingAutoloaderFiles = array_filter([ - __DIR__ . '/../../../autoload.php', - __DIR__ . '/../../vendor/autoload.php', - __DIR__ . '/../../../vendor/autoload.php', + __DIR__.'/../../../autoload.php', + __DIR__.'/../../vendor/autoload.php', + __DIR__.'/../../../vendor/autoload.php', ], function (string $path) { return file_exists($path); }); @@ -28,7 +28,7 @@ public static function init(string $autoloader = null) } self::$autoloader = $autoloader; - self::$childProcessScript = __DIR__ . '/ChildRuntime.php'; + self::$childProcessScript = __DIR__.'/ChildRuntime.php'; self::$isInitialised = true; } From e9828a7f80c23995a949a12eb81ec232b3214f5c Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Sun, 24 Dec 2017 10:42:52 +0100 Subject: [PATCH 29/50] Update README --- README.md | 62 +++++++++++++++++++++---------------------------------- 1 file changed, 23 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 59be40ec..bd928cc9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Asynchronous and parallel PHP with PCNTL +# Asynchronous and parallel PHP [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/async.svg?style=flat-square)](https://packagist.org/packages/spatie/async) [![Build Status](https://img.shields.io/travis/spatie/async/master.svg?style=flat-square)](https://travis-ci.org/spatie/async) @@ -47,35 +47,21 @@ A pool is configurable by the developer: use Spatie\Async\Pool; $pool = Pool::create() - ->concurrency(20) // The maximum amount of processes which can run simultaneously. - ->maximumExecutionTime(200) // The maximum amount of time a process may take to finish in seconds. -; -``` -### Processes +// The maximum amount of processes which can run simultaneously. + ->concurrency(20) -You can just add closures to the pool, but in some cases you want a class to represent a process. - -```php -use Spatie\Async\Process; +// The maximum amount of time a process may take to finish in seconds. + ->timeout(15) -class MyProcess extends Process -{ - public function __construct() - { - // You can add your own dependencies. - } - - public function execute() - { - // You can do whatever you like in here. - } -} +// Configure which autoloader sub processes should use. + ->autoload(__DIR__ . '/../../vendor/autoload.php') +; ``` ### Event listeners -When adding a process or a callable to a pool, you'll get an instance of `Process` returned. +When creating asynchronous processes, you'll get an instance of `ParallelProcess` returned. You can add the following event hooks on a process. ```php @@ -117,13 +103,16 @@ for ($i = 0; $i < 5; $i++) { await($pool); ``` +### Error handling + +If an exception is thrown from within a child process, and not caught using the `->catch()` callback, +it will be thrown as a real exception when calling `await()` or `$pool->wait()`. + ## Behind the curtains When using this package, you're probably wondering what's happening underneath the surface. -PHP has an extension called [PCNTL](http://php.net/manual/en/book.pcntl.php) which can spawn forks of its current process. -PCNTL directly uses your system's `fork` call to create a copy of the process, as a child process. - +We're using the `symfony/process` component to create and manage child processes in PHP. By creating child processes on the fly, we're able to execute PHP scripts in parallel. This parallelism can improve performance significantly when dealing with multiple synchronous tasks, which don't really need to wait for each other. @@ -134,7 +123,7 @@ or the application might crash. The `Pool` class provided by this package takes care of handling as many processes as you want by scheduling and running them when it's possible. -That's the part that `async()` or `$pool->add()` do. Now let's look at what `await()` or `$pool->wait()` does. +That's the part that `async()` or `$pool->add()` does. Now let's look at what `await()` or `$pool->wait()` does. When multiple processes are spawned, each can have a separate time to completion. One process might eg. have to wait for a HTTP call, while the other has to process large amounts of data. @@ -143,23 +132,18 @@ Sometimes you also have points in your code which have to wait until the result This is why we have to wait at a certain point in time: for all processes on a pool to finish, so we can be sure it's safe to continue without accidentally killing the child processes which aren't done yet. -"Waiting" for all processes is done in a `while` loop, which will check the status of every process once in a while. +Waiting for all processes is done by using a `while` loop, which will wait until all processes are finished. +Determining when a process is finished is done by using a listener on the `SIGCHLD` signal. +This signal is emitted when a child process is finished by the OS kernel. +As of PHP 7.1, there's much better support for listening and handling signals, +making this approach more performant than eg. using process forks or sockets for communication. +You can read more about it [here](https://wiki.php.net/rfc/async_signals). + When a process is finished, its success event is triggered, which you can hook into with the `->then()` function. Likewise, when a process fails or times out, the loop will update that process' status and move on. - When all processes are finished, the while loop will see that there's nothing more to wait for, and stop. This is the moment your parent process can continue to execute. -Because we're working with separate processes, we need a way of communication between the parent and child processes. -You might for example want to use the result generated by your child processes, in the parent process. - -Our package uses UNIX sockets for this communication. -Once a process is executed, we'll serialize its output and send it via a socket to the parent process, -who can handle it further in the while loop we spoke about earlier. - -When a process throws an exception or fails, we can also catch that output and send it via the socket to the parent. -That's how you can also listen for unhandled exceptions thrown in a child process, and handle them yourself. - ## Testing ``` bash From 95cb2e7ec7b799de94aed6675b86325af4a17391 Mon Sep 17 00:00:00 2001 From: freek Date: Sun, 24 Dec 2017 11:10:52 +0100 Subject: [PATCH 30/50] import namespace --- src/helpers.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/helpers.php b/src/helpers.php index d808380d..1e0e94e7 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -2,9 +2,10 @@ use Spatie\Async\Pool; use Spatie\Async\Runtime\ParentRuntime; +use Spatie\Async\ParallelProcess; if (! function_exists('async')) { - function async(callable $callable): \Spatie\Async\ParallelProcess + function async(callable $callable): ParallelProcess { return ParentRuntime::createChildProcess($callable); } From 2ccfc373fc3c8b7a156e706b348ad4c516277ca6 Mon Sep 17 00:00:00 2001 From: freek Date: Sun, 24 Dec 2017 11:12:19 +0100 Subject: [PATCH 31/50] nitpicks --- src/Output/SerializableException.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Output/SerializableException.php b/src/Output/SerializableException.php index e6fb8d39..3c23fda3 100644 --- a/src/Output/SerializableException.php +++ b/src/Output/SerializableException.php @@ -6,15 +6,20 @@ class SerializableException { + /** @string */ protected $class; + + /** @string */ protected $message; + + /** @string */ protected $trace; - public function __construct(Throwable $e) + public function __construct(Throwable $exception) { - $this->class = get_class($e); - $this->message = $e->getMessage(); - $this->trace = $e->getTraceAsString(); + $this->class = get_class($exception); + $this->message = $exception->getMessage(); + $this->trace = $exception->getTraceAsString(); } public function asThrowable(): Throwable From 405d3a3b384cc8504ad1ebbae74336ad8191b9d9 Mon Sep 17 00:00:00 2001 From: freek Date: Sun, 24 Dec 2017 11:14:23 +0100 Subject: [PATCH 32/50] nitpick --- src/Runtime/ChildRuntime.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Runtime/ChildRuntime.php b/src/Runtime/ChildRuntime.php index 43847e4f..9ea970c0 100644 --- a/src/Runtime/ChildRuntime.php +++ b/src/Runtime/ChildRuntime.php @@ -25,10 +25,10 @@ fwrite(STDOUT, base64_encode(serialize($output))); exit(0); -} catch (Throwable $e) { +} catch (Throwable $exception) { require_once __DIR__.'/../Output/SerializableException.php'; - $output = new \Spatie\Async\Output\SerializableException($e); + $output = new \Spatie\Async\Output\SerializableException($exception); fwrite(STDERR, base64_encode(serialize($output))); From 6e19f8b61484c0913d5cb91c95e82539381e5f06 Mon Sep 17 00:00:00 2001 From: freek Date: Sun, 24 Dec 2017 11:17:28 +0100 Subject: [PATCH 33/50] nitpicks --- src/Output/SerializableException.php | 6 +++--- src/Runtime/ParentRuntime.php | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Output/SerializableException.php b/src/Output/SerializableException.php index 3c23fda3..20e39c01 100644 --- a/src/Output/SerializableException.php +++ b/src/Output/SerializableException.php @@ -6,13 +6,13 @@ class SerializableException { - /** @string */ + /** @var string */ protected $class; - /** @string */ + /** @var string */ protected $message; - /** @string */ + /** @var string */ protected $trace; public function __construct(Throwable $exception) diff --git a/src/Runtime/ParentRuntime.php b/src/Runtime/ParentRuntime.php index 3ce5de44..03dc1fcc 100644 --- a/src/Runtime/ParentRuntime.php +++ b/src/Runtime/ParentRuntime.php @@ -9,8 +9,13 @@ class ParentRuntime { + /** @var bool */ protected static $isInitialised = false; + + /** @var string */ protected static $autoloader; + + /** @var string */ protected static $childProcessScript; public static function init(string $autoloader = null) From c00aedc123b072ea5d8c039f6732640ab429c2a0 Mon Sep 17 00:00:00 2001 From: freek Date: Sun, 24 Dec 2017 11:18:12 +0100 Subject: [PATCH 34/50] nitpicks --- src/Pool.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Pool.php b/src/Pool.php index 6e943de1..27031af9 100644 --- a/src/Pool.php +++ b/src/Pool.php @@ -13,10 +13,13 @@ class Pool implements ArrayAccess /** @var \Spatie\Async\ParallelProcess[] */ protected $queue = []; + /** @var \Spatie\Async\ParallelProcess[] */ protected $inProgress = []; + /** @var \Spatie\Async\ParallelProcess[] */ protected $finished = []; + /** @var \Spatie\Async\ParallelProcess[] */ protected $failed = []; From 4ac6ebe6ab5a459fd8491d54ca137290ed627565 Mon Sep 17 00:00:00 2001 From: freek Date: Sun, 24 Dec 2017 11:21:49 +0100 Subject: [PATCH 35/50] nitpick --- src/Pool.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Pool.php b/src/Pool.php index 27031af9..4a263b96 100644 --- a/src/Pool.php +++ b/src/Pool.php @@ -90,7 +90,7 @@ public function wait(): array while ($this->inProgress) { foreach ($this->inProgress as $process) { if ($process->getCurrentExecutionTime() > $this->timeout) { - $this->markAsTimeout($process); + $this->markAsTimedOut($process); } } @@ -133,7 +133,7 @@ public function markAsFinished(ParallelProcess $process): void $this->notify(); } - public function markAsTimeout(ParallelProcess $process): void + public function markAsTimedOut(ParallelProcess $process): void { $process->triggerTimeout(); From b8f9657ec7b168c522d2067e139aa26ffee65ed9 Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Sun, 24 Dec 2017 11:23:58 +0100 Subject: [PATCH 36/50] Update README --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index bd928cc9..d84f74bc 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,35 @@ Likewise, when a process fails or times out, the loop will update that process' When all processes are finished, the while loop will see that there's nothing more to wait for, and stop. This is the moment your parent process can continue to execute. +## Comparison to other libraries + +There are two very well-known asynchronous libraries in PHP: + +- [ReactPHP](https://github.com/reactphp) +- [Amp](https://github.com/amphp) + +Both have their own take on parallel processing and offer a much wider range of features than this library. +Our implementation aims for better performance and ease of development, at the cost of a smaller feature set. + +I've personally ran some benchmarks against both libraries, for which the code can be found [here](https://github.com/brendt/async-benchmark). +The benchmark consists of 30 iterations of executing the same script. +The script itself will spawn 30 child processes which will sleep for either 1, 2 or 3 seconds, +depending on their position in the queue. +This way there's no random element in the sleep time, though there is variation. +These are the results, plotting the executing time of every iteration, in seconds. + +![Comaring spatie/async to Amp and ReactPHP](./docs/benchmarks.png) + +You can see that both Amp and our implementation are less performant than ReactPHP. +If you're looking for pure performance, ReactPHP might be a better choice. +Our package though has the benefit of a much simpler API, in our opinion. +We're also still improving this package, so chances are performance will be better in the future. + +So when should you use this library? +The benchmarks show that we're in the same league as Amp and ReactPHP for performing processes in parallel. +Both other libraries offer a lot more functionality, though often at the cost of simplicity or performance. +This package aims to solve only part of the bigger picture, but tries to solve it in a performant and easy-to-use way. + ## Testing ``` bash From 31b65c717f0b259a39d1c805c5d7a7d19fd16708 Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Sun, 24 Dec 2017 11:24:12 +0100 Subject: [PATCH 37/50] Add docs --- .gitignore | 1 - docs/benchmarks.png | Bin 0 -> 20647 bytes 2 files changed, 1 deletion(-) create mode 100644 docs/benchmarks.png diff --git a/.gitignore b/.gitignore index f02a2f86..073e37a0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ build composer.lock -docs vendor diff --git a/docs/benchmarks.png b/docs/benchmarks.png new file mode 100644 index 0000000000000000000000000000000000000000..31015baeb65e1c30a92b4421243b4435da204ff5 GIT binary patch literal 20647 zcmd42cUY5Ovo?waEQqKyr7O}!q&F#oNbkKXNC^;-7J3mynt=4)d*~1d5Q>E&El7X> z2~9eL&_gfZ!*B1i-*et;pYJ>EkMmEiwDqjDX3fmH=bo8xO?3sb+qAcdh=|CP6klr- z5nU4@BD!*R^D6L7i3Jseh)B~(>GdmJFARPv&~fA-bMr*m)5GIpg{jQV7h0hnGcqda zJ1Xs3sED7+%Cbr1z(%F%xux$5mg%Nflte(wQLt6E>o3?I-s#|zrkAK3M2`?-&=9Li61|w~>lrh+lbMy3m2na5;P5_3&s>8EE^~!XXYcVi5JR6u9BulgL zy$A-Z#iYv~1nL7_;#t!(%QymW|`W(s6rL07X3=oN3kf%&OHwY0hB=! zuLWZ)>e<2w8Vx)J+v@At!GWCbj!|e?TA!$M!sT(G^b8E*0d7ZHt_qm zqKb+NJtx@G(sC54w>p+Y{k(;T=QMr=NAukDX5eD82ab!23s%F$&F$vqR@^#&3(!H) z#VFuFI8yfx4wytgFtf9>??4SV+i)=!#4u3zdi)MCjb|tdLCJ0Nea!f>|aQm=V}pYI?efwsx)8 zwxz@4NY+C9OnYEw1Ql?;tKwZ0M6-!C+qV|$woXI?Pu%tXo-jB*V(xizT*yiWn9|kW zCzu&Y-~A~a5u|BzYwpS#JX@@+YULK)jZ3r9mg`adxSqQV-y;F{9@f zrFQ-OOTc=rs~c>F71mhkP2Hb?mbae4H4Ij@%!m8FTm`HusKB^wKH^E*dFDO>5mB{@ zh97-$R_rO+%Yo3bg?0P)ZmdQoV=q|1uavI^d9NMu z5bD&kroF(w-#pMoPpc2dObm-aMJ#lE&JsncxSZCuCmoXUJxQKe$M3IYXi2-f*rWA4 zq!CR>CpnWZ=gC4e1SuSrM9lJ)a=N^?T9vv7ggwZ8S-4<;R@0GWKYPJk_lsgaxmR0m z``f^>C&=>QZtVQ_I;jfW%p0-Z91Ma^R}0sk1TdQUna6B-mh2VD^yHFiWA;ckzGB68 zBMHfskm+~DR~0RkpYkE=`CdNYI-ZvFU^&cI+0@?@c$slFRcA}HFj_e^WqKIU{bT33 zq%JoU~I>NUl$KYgKjGQfNR?VJt1iI6c$BMY;KCi}ED!;w?!WtQ1D;qjI55D{ z>mnS&ulKI?`ExB-xZs95m(S88U(-<1WpDp+8?D(p)oE#X*W|Nu1EhP66YOLAeCj-# zcl2rTnGqEm-FiH1$&=`j>UQVGEsn^Ja~)``amvlva{A1vU(`=7HYVzvlfBU%jcwo$ zugohFcYEuz;AW+Ya9kMIsoUC}V*hssIRx(*-N}V-B%I*mI46T`A^6!5;S+wE+FTq& zkkETGPQ>l|a_48agQv(a_g$GuWp&T87e~bz8cWBY+6@CiXujN@;yl!b7T30yxiD91 zX$JgFec;)FI2@4@dSC6LOY7R$;!zaCY(-v+WEwZh0KM<&%wBJH^(_UZ9o&pzK8wB4`M>eUPp@O7D6 zB!1yMF;E`H`H*^HG|PEqa8{#rajJnsMRtHobO5x$@aySoK@qd2Pki$mWJ}=JZZYri z`kqf>_A&z>3N!^w9fTV%_hGJyn*icQc&k`Xa66kv@!P^IGm!gXe$*-r!=+A{58R_3 zm$0Tn?0%f1m(3C7Pfmb?`?{v6fN_A-!1`LAy8?%1^6RTw*tb%qNuNx1elh!dX~Vii z#@FJ`)ofiVuW-`P;vbojKn>J=*o*7ZilKF7`@z zp5kMqHub~AT`Id7=-)GVcsSuA!Dhe2wW=V~L%V>DO(>R$Vy3@Fy)@7~DF~1)*2t4;k7pE} z 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⪼Q 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 Date: Sun, 24 Dec 2017 11:27:42 +0100 Subject: [PATCH 38/50] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d84f74bc..7bada9b2 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ There are two very well-known asynchronous libraries in PHP: Both have their own take on parallel processing and offer a much wider range of features than this library. Our implementation aims for better performance and ease of development, at the cost of a smaller feature set. -I've personally ran some benchmarks against both libraries, for which the code can be found [here](https://github.com/brendt/async-benchmark). +I've personally ran some benchmarks against both libraries, for which the code can be found [here](https://github.com/spatie/async-benchmark). The benchmark consists of 30 iterations of executing the same script. The script itself will spawn 30 child processes which will sleep for either 1, 2 or 3 seconds, depending on their position in the queue. From f599592a864a196107c01fd7be638d2d1eb1f422 Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Sun, 24 Dec 2017 11:30:05 +0100 Subject: [PATCH 39/50] Code cleanup --- src/Pool.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Pool.php b/src/Pool.php index 6e943de1..b79ae5a2 100644 --- a/src/Pool.php +++ b/src/Pool.php @@ -56,7 +56,7 @@ public function autoload(string $autoloader): self return $this; } - public function notify(): void + public function notify() { if (count($this->inProgress) >= $this->concurrency) { return; @@ -101,14 +101,14 @@ public function wait(): array return $this->results; } - public function putInQueue(ParallelProcess $process): void + public function putInQueue(ParallelProcess $process) { $this->queue[$process->getId()] = $process; $this->notify(); } - public function putInProgress(ParallelProcess $process): void + public function putInProgress(ParallelProcess $process) { $process->getProcess()->setTimeout($this->timeout); @@ -119,7 +119,7 @@ public function putInProgress(ParallelProcess $process): void $this->inProgress[$process->getPid()] = $process; } - public function markAsFinished(ParallelProcess $process): void + public function markAsFinished(ParallelProcess $process) { $this->results[] = $process->triggerSuccess(); @@ -130,7 +130,7 @@ public function markAsFinished(ParallelProcess $process): void $this->notify(); } - public function markAsTimeout(ParallelProcess $process): void + public function markAsTimeout(ParallelProcess $process) { $process->triggerTimeout(); @@ -141,7 +141,7 @@ public function markAsTimeout(ParallelProcess $process): void $this->notify(); } - public function markAsFailed(ParallelProcess $process): void + public function markAsFailed(ParallelProcess $process) { $process->triggerError(); @@ -190,7 +190,7 @@ public function getFailed(): array return $this->failed; } - protected function registerListener(): void + protected function registerListener() { pcntl_async_signals(true); From 1a5234a36a5e08f5397ca6c058930be867e01913 Mon Sep 17 00:00:00 2001 From: freek Date: Sun, 24 Dec 2017 11:32:23 +0100 Subject: [PATCH 40/50] improve readability --- tests/PoolTest.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/PoolTest.php b/tests/PoolTest.php index 9dca4d18..3a8bb3bb 100644 --- a/tests/PoolTest.php +++ b/tests/PoolTest.php @@ -20,7 +20,7 @@ public function it_can_run_processes_in_parallel() $startTime = microtime(true); - for ($i = 0; $i < 5; $i++) { + foreach (range(1, 5) as $i) { $pool->add(function () { usleep(1000); }); @@ -42,7 +42,7 @@ public function it_can_handle_success() $counter = 0; - for ($i = 0; $i < 5; $i++) { + foreach (range(1, 5) as $i) { $pool->add(function () { return 2; })->then(function (int $output) use (&$counter) { @@ -63,7 +63,7 @@ public function it_can_handle_timeout() $counter = 0; - for ($i = 0; $i < 5; $i++) { + foreach (range(1, 5) as $i) { $pool->add(function () { sleep(2); })->timeout(function () use (&$counter) { @@ -81,7 +81,7 @@ public function it_can_handle_exceptions() { $pool = Pool::create(); - for ($i = 0; $i < 5; $i++) { + foreach (range(1, 5) as $i) { $pool->add(function () { throw new Exception('test'); })->catch(function (Exception $e) { @@ -102,7 +102,7 @@ public function it_can_handle_a_maximum_of_concurrent_processes() $startTime = microtime(true); - for ($i = 0; $i < 3; $i++) { + foreach (range(1, 3) as $i) { $pool->add(function () { sleep(1); }); @@ -125,7 +125,7 @@ public function it_works_with_helper_functions() $counter = 0; - for ($i = 0; $i < 5; $i++) { + foreach (range(1, 5) as $i) { $pool[] = async(function () { usleep(random_int(10, 1000)); @@ -172,7 +172,7 @@ public function it_returns_all_the_output_as_an_array() /** @var MyClass $result */ $result = null; - for ($i = 0; $i < 5; $i++) { + foreach (range(1, 5) as $i) { $pool[] = async(function () { return 2; }); From b7d7dc7ce46015a9d85b19b4080e5d369e0ab75d Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Sun, 24 Dec 2017 11:33:51 +0100 Subject: [PATCH 41/50] Add docblock --- src/Pool.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Pool.php b/src/Pool.php index b79ae5a2..9176ee32 100644 --- a/src/Pool.php +++ b/src/Pool.php @@ -168,7 +168,7 @@ public function offsetSet($offset, $value) { $this->add($value); } - +M public function offsetUnset($offset) { // TODO From f6fa1ce5206982eb22c0a066e70c437358eace59 Mon Sep 17 00:00:00 2001 From: freek Date: Sun, 24 Dec 2017 11:34:34 +0100 Subject: [PATCH 42/50] make loop more readable --- README.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 7bada9b2..f2a63ef6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![StyleCI](https://styleci.io/repos/114228700/shield?branch=master)](https://styleci.io/repos/114228700) [![Total Downloads](https://img.shields.io/packagist/dt/spatie/async.svg?style=flat-square)](https://packagist.org/packages/spatie/async) -This library provides a small and easy wrapper around PHP's PCNTL extension. +This library provides a small and easy wrapper around PHP's PCNTL extension. It allows for running difference processes in parallel, with an easy-to-use API. ## Installation @@ -49,13 +49,13 @@ use Spatie\Async\Pool; $pool = Pool::create() // The maximum amount of processes which can run simultaneously. - ->concurrency(20) + ->concurrency(20) // The maximum amount of time a process may take to finish in seconds. - ->timeout(15) + ->timeout(15) // Configure which autoloader sub processes should use. - ->autoload(__DIR__ . '/../../vendor/autoload.php') + ->autoload(__DIR__ . '/../../vendor/autoload.php') ; ``` @@ -90,7 +90,7 @@ use Spatie\Async\Process; $pool = Pool::create(); -for ($i = 0; $i < 5; $i++) { +foreach (range(1, 5) as $i) { $pool[] = async(function () { usleep(random_int(10, 1000)); @@ -106,7 +106,7 @@ await($pool); ### Error handling If an exception is thrown from within a child process, and not caught using the `->catch()` callback, -it will be thrown as a real exception when calling `await()` or `$pool->wait()`. +it will be thrown as a real exception when calling `await()` or `$pool->wait()`. ## Behind the curtains @@ -114,22 +114,22 @@ When using this package, you're probably wondering what's happening underneath t We're using the `symfony/process` component to create and manage child processes in PHP. By creating child processes on the fly, we're able to execute PHP scripts in parallel. -This parallelism can improve performance significantly when dealing with multiple synchronous tasks, +This parallelism can improve performance significantly when dealing with multiple synchronous tasks, which don't really need to wait for each other. By giving these tasks a separate process to run on, the underlying operating system can take care of running them in parallel. There's a caveat when dynamically spawning processes: you need to make sure that there won't be too many processes at once, or the application might crash. -The `Pool` class provided by this package takes care of handling as many processes as you want +The `Pool` class provided by this package takes care of handling as many processes as you want by scheduling and running them when it's possible. That's the part that `async()` or `$pool->add()` does. Now let's look at what `await()` or `$pool->wait()` does. -When multiple processes are spawned, each can have a separate time to completion. +When multiple processes are spawned, each can have a separate time to completion. One process might eg. have to wait for a HTTP call, while the other has to process large amounts of data. Sometimes you also have points in your code which have to wait until the result of a process is returned. -This is why we have to wait at a certain point in time: for all processes on a pool to finish, +This is why we have to wait at a certain point in time: for all processes on a pool to finish, so we can be sure it's safe to continue without accidentally killing the child processes which aren't done yet. Waiting for all processes is done by using a `while` loop, which will wait until all processes are finished. @@ -140,7 +140,7 @@ making this approach more performant than eg. using process forks or sockets for You can read more about it [here](https://wiki.php.net/rfc/async_signals). When a process is finished, its success event is triggered, which you can hook into with the `->then()` function. -Likewise, when a process fails or times out, the loop will update that process' status and move on. +Likewise, when a process fails or times out, the loop will update that process' status and move on. When all processes are finished, the while loop will see that there's nothing more to wait for, and stop. This is the moment your parent process can continue to execute. @@ -156,22 +156,22 @@ Our implementation aims for better performance and ease of development, at the c I've personally ran some benchmarks against both libraries, for which the code can be found [here](https://github.com/spatie/async-benchmark). The benchmark consists of 30 iterations of executing the same script. -The script itself will spawn 30 child processes which will sleep for either 1, 2 or 3 seconds, +The script itself will spawn 30 child processes which will sleep for either 1, 2 or 3 seconds, depending on their position in the queue. This way there's no random element in the sleep time, though there is variation. These are the results, plotting the executing time of every iteration, in seconds. -![Comaring spatie/async to Amp and ReactPHP](./docs/benchmarks.png) +![Comaring spatie/async to Amp and ReactPHP](./docs/benchmarks.png) You can see that both Amp and our implementation are less performant than ReactPHP. If you're looking for pure performance, ReactPHP might be a better choice. Our package though has the benefit of a much simpler API, in our opinion. We're also still improving this package, so chances are performance will be better in the future. -So when should you use this library? +So when should you use this library? The benchmarks show that we're in the same league as Amp and ReactPHP for performing processes in parallel. Both other libraries offer a lot more functionality, though often at the cost of simplicity or performance. -This package aims to solve only part of the bigger picture, but tries to solve it in a performant and easy-to-use way. +This package aims to solve only part of the bigger picture, but tries to solve it in a performant and easy-to-use way. ## Testing @@ -208,7 +208,7 @@ We publish all received postcards [on our company website](https://spatie.be/en/ Spatie is a webdesign agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource). -Does your business depend on our contributions? Reach out and support us on [Patreon](https://www.patreon.com/spatie). +Does your business depend on our contributions? Reach out and support us on [Patreon](https://www.patreon.com/spatie). All pledges will be dedicated to allocating workforce on maintenance and new awesome stuff. ## License From a99b048f5f67cc8734d3b3cac3a7d5f7b77c0f56 Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Sun, 24 Dec 2017 11:39:18 +0100 Subject: [PATCH 43/50] Add docblock --- src/Pool.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Pool.php b/src/Pool.php index f63a89fe..124dbe29 100644 --- a/src/Pool.php +++ b/src/Pool.php @@ -74,6 +74,11 @@ public function notify() $this->putInProgress($process); } + /** + * @param \Spatie\Async\ParallelProcess|callable $process + * + * @return \Spatie\Async\ParallelProcess + */ public function add($process): ParallelProcess { if (! $process instanceof ParallelProcess) { From 39525335b8d01969a33a3fff44c3a8b0d99e093a Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Sun, 24 Dec 2017 11:42:40 +0100 Subject: [PATCH 44/50] Add sleepTime --- README.md | 3 +++ src/Pool.php | 10 +++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f2a63ef6..203b2131 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,9 @@ $pool = Pool::create() // Configure which autoloader sub processes should use. ->autoload(__DIR__ . '/../../vendor/autoload.php') + +// Configure how long the loop should sleep before re-checking the processes statuses in milliseconds. + ->sleepTime(50000) ; ``` diff --git a/src/Pool.php b/src/Pool.php index 124dbe29..786c7d19 100644 --- a/src/Pool.php +++ b/src/Pool.php @@ -10,6 +10,7 @@ class Pool implements ArrayAccess protected $concurrency = 20; protected $tasksPerProcess = 1; protected $timeout = 300; + protected $sleepTime = 50000; /** @var \Spatie\Async\ParallelProcess[] */ protected $queue = []; @@ -59,6 +60,13 @@ public function autoload(string $autoloader): self return $this; } + public function sleepTime(int $sleepTime): self + { + $this->sleepTime = $sleepTime; + + return $this; + } + public function notify() { if (count($this->inProgress) >= $this->concurrency) { @@ -103,7 +111,7 @@ public function wait(): array break; } - usleep(50000); + usleep($this->sleepTime); } return $this->results; From 992333cd1f51610e2c7319642b6119e9655a07e9 Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Sun, 24 Dec 2017 11:43:06 +0100 Subject: [PATCH 45/50] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 203b2131..865072fd 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ $pool = Pool::create() // Configure which autoloader sub processes should use. ->autoload(__DIR__ . '/../../vendor/autoload.php') -// Configure how long the loop should sleep before re-checking the processes statuses in milliseconds. +// Configure how long the loop should sleep before re-checking the process statuses in milliseconds. ->sleepTime(50000) ; ``` From db0723302c927bd9a6fa5b88e01da754133a4c5b Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Sun, 24 Dec 2017 10:46:41 +0000 Subject: [PATCH 46/50] Apply fixes from StyleCI --- src/helpers.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers.php b/src/helpers.php index 1e0e94e7..39757ffd 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -1,8 +1,8 @@ Date: Sun, 24 Dec 2017 11:59:45 +0100 Subject: [PATCH 47/50] Add vendor autoload support --- src/Runtime/ParentRuntime.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Runtime/ParentRuntime.php b/src/Runtime/ParentRuntime.php index 03dc1fcc..d83a1f35 100644 --- a/src/Runtime/ParentRuntime.php +++ b/src/Runtime/ParentRuntime.php @@ -22,6 +22,7 @@ public static function init(string $autoloader = null) { if (! $autoloader) { $existingAutoloaderFiles = array_filter([ + __DIR__.'/../../../../autoload.php', __DIR__.'/../../../autoload.php', __DIR__.'/../../vendor/autoload.php', __DIR__.'/../../../vendor/autoload.php', From a8bc8165c67872747ff5978c10ccd8818bd5ba5c Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Sun, 24 Dec 2017 12:10:50 +0100 Subject: [PATCH 48/50] Add ParallelError --- src/ParallelError.php | 13 +++++++++++++ src/ParallelProcess.php | 8 +++++++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 src/ParallelError.php diff --git a/src/ParallelError.php b/src/ParallelError.php new file mode 100644 index 00000000..83c68ca4 --- /dev/null +++ b/src/ParallelError.php @@ -0,0 +1,13 @@ +getErrorOutput()) { + $this->triggerError(); + + return null; + } + $output = $this->getOutput(); foreach ($this->successCallbacks as $callback) { @@ -165,7 +171,7 @@ public function triggerError() throw $exception; } - throw new Error($exception); + throw ParallelError::fromException($exception); } } From 8d470c090c67c33c5110eed13fed56e23bee711d Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Sun, 24 Dec 2017 12:11:51 +0100 Subject: [PATCH 49/50] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 865072fd..3d4cc3a3 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ await($pool); ### Error handling If an exception is thrown from within a child process, and not caught using the `->catch()` callback, -it will be thrown as a real exception when calling `await()` or `$pool->wait()`. +it will be thrown as `Spatie\Async\ParallelError` when calling `await()` or `$pool->wait()`. ## Behind the curtains From ee377069c52a50eca071c27623d954630f131aae Mon Sep 17 00:00:00 2001 From: Brent Roose Date: Sun, 24 Dec 2017 11:23:16 +0000 Subject: [PATCH 50/50] Apply fixes from StyleCI --- src/ParallelError.php | 2 +- src/ParallelProcess.php | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ParallelError.php b/src/ParallelError.php index 83c68ca4..b0c62b2c 100644 --- a/src/ParallelError.php +++ b/src/ParallelError.php @@ -6,7 +6,7 @@ class ParallelError extends Exception { - public static function fromException($exception): ParallelError + public static function fromException($exception): self { return new self($exception); } diff --git a/src/ParallelProcess.php b/src/ParallelProcess.php index 71ec687f..592b0e4a 100644 --- a/src/ParallelProcess.php +++ b/src/ParallelProcess.php @@ -2,7 +2,6 @@ namespace Spatie\Async; -use Error; use Throwable; use Symfony\Component\Process\Process; use Spatie\Async\Output\SerializableException; @@ -142,7 +141,7 @@ public function triggerSuccess() if ($this->getErrorOutput()) { $this->triggerError(); - return null; + return; } $output = $this->getOutput();