Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The State of Asynchronous PHP

The State of Asynchronous PHP

Given at PHP Usergroup Hamburg's June 2019 Meetup: https://www.meetup.com/phpughh/events/fpnmzmyzjbpb/

The code examples can be found at https://github.com/sebastianheuer/phpughh-june-2019

Event-driven architectures heavily increased the need for developing software in an asynchronous fashion. Languages like Go make running multiple parallel tasks look easy, but what if you want to stick to PHP? Let's have a look at different approaches of doing asynchronous work in PHP and what they could be used for.

Sebastian Heuer

June 11, 2019
Tweet

More Decks by Sebastian Heuer

Other Decks in Technology

Transcript

  1. <?php require __DIR__ . '/vendor/autoload.php'; $loop = \React\EventLoop\Factory::create(); $read =

    new \React\Stream\ReadableResourceStream(fopen('php://stdin', 'r+'), $loop); $write = new \React\Stream\WritableResourceStream(fopen('php://stdout', 'w+'), $loop); $superFabulous = new \Fab\SuperFab(); $write->on('close', function() { echo Kitten::get() . ' says bye!' . PHP_EOL; }); $read->on('data', function(string $input) use ($read, $write, $superFabulous) { $input = trim($input); if ($input === 'bye') { $read->close(); $write->close(); } $write->write( $superFabulous->paint(sprintf('%s says: "%s"', Kitten::get(), $input) . PHP_EOL) ); }); $loop->run();
  2. <?php require __DIR__ . '/vendor/autoload.php'; $loop = \React\EventLoop\Factory::create(); $read =

    new \React\Stream\ReadableResourceStream(fopen('php://stdin', 'r+'), $loop); $write = new \React\Stream\WritableResourceStream(fopen('php://stdout', 'w+'), $loop); $superFabulous = new \Fab\SuperFab(); $write->on('close', function() { echo Kitten::get() . ' says bye!' . PHP_EOL; }); $read->on('data', function(string $input) use ($read, $write, $superFabulous) { $input = trim($input); if ($input === 'bye') { $read->close(); $write->close(); } $write->write( $superFabulous->paint(sprintf('%s says: "%s"', Kitten::get(), $input) . PHP_EOL) ); }); $loop->run();
  3. FPM

  4. <?php declare(strict_types=1); use hollodotme\FastCGI\Client; use hollodotme\FastCGI\Requests\PostRequest; use hollodotme\FastCGI\Responses\Response; class FastCgiEventListener

    { /** @var Event[] */ private $activeTasks = []; /** @var Client */ private $fastCgiClient; /** @var RedisEventStream */ private $stream; public function __construct(Client $fastCgiClient, RedisEventStream $stream) { $this->fastCgiClient = $fastCgiClient; $this->stream = $stream; } public function listen(): void { while (true) { if ($this->fastCgiClient->hasUnhandledResponses()) { $this->fastCgiClient->handleReadyResponses(); } if (!$event = $this->stream->getNextEvent()) { FastCgiEventListener.php
  5. $this->stream = $stream; } public function listen(): void { while

    (true) { if ($this->fastCgiClient->hasUnhandledResponses()) { $this->fastCgiClient->handleReadyResponses(); } if (!$event = $this->stream->getNextEvent()) { usleep(15000); continue; } $request = new PostRequest( '/app/handleEvent.php', $event->getPayload() ); $request->addResponseCallbacks([$this, 'handleResponse']); $requestId = $this->fastCgiClient->sendAsyncRequest($request); $this->activeTasks[$requestId] = $event; } } public function handleResponse(Response $response): void { if ($response->getError() !== '') { echo 'F'; unset($this->activeTasks[$response->getRequestId()]); return; } echo 'S'; FastCgiEventListener.php
  6. '/app/handleEvent.php', $event->getPayload() ); $request->addResponseCallbacks([$this, 'handleResponse']); $requestId = $this->fastCgiClient->sendAsyncRequest($request); $this->activeTasks[$requestId] =

    $event; } } public function handleResponse(Response $response): void { if ($response->getError() !== '') { echo 'F'; unset($this->activeTasks[$response->getRequestId()]); return; } echo 'S'; $this->stream->acknowledge($this->activeTasks[$response->getRequestId()]); unset($this->activeTasks[$response->getRequestId()]); } } FastCgiEventListener.php
  7. class GenerateScoresCommand { private const MAX_CONCURRENT_REQUESTS = 32; /** *

    @var SimilarityMapWriter */ private $similarityMapWriter; /** * @var ProductListReader */ private $productListReader; /** * @var CalculationTaskList */ private $taskList; /** * @var FastCgiTaskRunner */ private $taskRunner; public function __construct( SimilarityMapWriter $similarityMapWriter, ProductListReader $productListReader, FastCgiTaskRunner $taskRunner ) { $this->similarityMapWriter = $similarityMapWriter; GenerateScoresCommand.php
  8. ProductListReader $productListReader, FastCgiTaskRunner $taskRunner ) { $this->similarityMapWriter = $similarityMapWriter; $this->productListReader

    = $productListReader; $this->taskRunner = $taskRunner; } public function execute(StoreCode $storeCode): void { $productIdentifiers = $this->productListReader->read($storeCode); $this->taskList = new CalculationTaskList(self::MAX_CONCURRENT_REQUESTS); do { $this->taskRunner->updateTaskStatuses(); if (!$this->taskList->acceptsAnotherTask() || !$productIdentifiers->hasCurrent()) { usleep(15000); continue; } $task = new CalculationTask($productIdentifiers->getCurrent(), $storeCode); $taskId = $this->taskRunner->run($task, [$this, 'handleCompletedTask']); $this->taskList->addTask($taskId, $task); $productIdentifiers->moveToNext(); } while ($this->taskList->hasTasks()); } public function handleCompletedTask(int $taskId, $taskResult): void { GenerateScoresCommand.php
  9. continue; } $task = new CalculationTask($productIdentifiers->getCurrent(), $storeCode); $taskId = $this->taskRunner->run($task,

    [$this, 'handleCompletedTask']); $this->taskList->addTask($taskId, $task); $productIdentifiers->moveToNext(); } while ($this->taskList->hasTasks()); } public function handleCompletedTask(int $taskId, $taskResult): void { $task = $this->taskList->getTask($taskId); $scores = @unserialize($taskResult); if ($scores === false) { throw new InvalidTaskResultException(sprintf('Could not unserialize task result %s', $taskResult)); } $this->similarityMapWriter->write($task->getStoreCode(), $task->getProduct()->getIdentifier(), $scores); $this->taskList->removeTask($taskId); } } GenerateScoresCommand.php
  10. <?php use Kartenmacherei\SimilarProductsService\Backend\BackendFactory; use Kartenmacherei\SimilarProductsService\Backend\Exception\UnsupportedTaskTypeException; use Kartenmacherei\SimilarProductsService\Backend\ScoreCalculation\CalculationTask; use Kartenmacherei\SimilarProductsService\Shared\SharedFactory; require

    __DIR__ . '/../vendor/autoload.php'; $factory = new BackendFactory(new SharedFactory()); $task = unserialize(file_get_contents('php://input')); if (!$task instanceof CalculationTask) { throw new UnsupportedTaskTypeException(sprintf('Task Type %s is not supported.', get_class($task))); } $scores = $factory->createCalculationTaskHandler()->handle($task); echo serialize($scores->getTop(24)); calculate.php
  11. PHP Application Worker FastCGI Proxy Server B Server C Server

    A FPM FPM Worker Worker Worker Worker Worker
  12. CalculateCommand.php <?php declare(strict_types=1); namespace ParallelPhp; class CalculateCommand { /** *

    @var ProgressOutput */ private $progressOutput; /** * @var \parallel\Events */ private $events; /** * @var array \parallel\Runtime[] */ private $runtimes = []; /** * @var \React\EventLoop\LoopInterface */ private $loop; public function __construct(ProgressOutput $progressOutput) { $this->progressOutput = $progressOutput;
  13. CalculateCommand.php private $loop; public function __construct(ProgressOutput $progressOutput) { $this->progressOutput =

    $progressOutput; } public function execute(array $tasks): void { $this->progressOutput->init(count($tasks)); $this->events = new \parallel\Events(); $this->events->setTimeout(1); $this->loop = \React\EventLoop\Factory::create(); $this->catchSigintSignal(); $this->exitWhenNoRuntimesAreLeft(); $this->checkForEventsPeriodically(); $this->registerRuntimes($tasks); $this->loop->run(); $this->progressOutput->finalize(); } private function registerRuntimes(array $tasks): void { foreach ($tasks as $id => $task) { $this->events->addChannel(\parallel\Channel::make((string)$id, \parallel\Channel::Infinite));
  14. CalculateCommand.php $this->registerRuntimes($tasks); $this->loop->run(); $this->progressOutput->finalize(); } private function registerRuntimes(array $tasks): void

    { foreach ($tasks as $id => $task) { $this->events->addChannel(\parallel\Channel::make((string)$id, \parallel\Channel::Infinite)); $runtime = new \parallel\Runtime(__DIR__ . '/../vendor/autoload.php'); $future = $runtime->run(function (int $id, string $serializedTask) { $taskHandler = new TaskHandler(); $task = unserialize($serializedTask); return $taskHandler->handle($id, $task); }, [$id, serialize($task)]); $this->events->addFuture('task'.(string)$id, $future); $this->runtimes[$id] = $runtime; unset($tasks[$id]); } } private function checkForEventsPeriodically(): void { $this->loop->addPeriodicTimer(0.001, function () use (&$timer) { try {
  15. CalculateCommand.php }, [$id, serialize($task)]); $this->events->addFuture('task'.(string)$id, $future); $this->runtimes[$id] = $runtime; unset($tasks[$id]);

    } } private function checkForEventsPeriodically(): void { $this->loop->addPeriodicTimer(0.001, function () use (&$timer) { try { while ($event = $this->events->poll()) { $this->handleEvent($event); } } catch (\parallel\Events\Error\Timeout $exception) { return; } }); } private function handleEvent(\parallel\Events\Event $event) { if ($event->type !== \parallel\Events\Event\Type::Read) { return; } if ($event->object instanceof \parallel\Channel) { $id = (int)$event->source; if(!array_key_exists($id, $this->runtimes)) { return; }
  16. CalculateCommand.php private function handleEvent(\parallel\Events\Event $event) { if ($event->type !== \parallel\Events\Event\Type::Read)

    { return; } if ($event->object instanceof \parallel\Channel) { $id = (int)$event->source; if(!array_key_exists($id, $this->runtimes)) { return; } try { $progress = (int)$event->object->recv(); $this->progressOutput->update($id, $progress); $this->events->addChannel($event->object); } catch (\parallel\Channel\Error\Closed $exception) { } return; } if ($event->object instanceof \parallel\Future) { $result = $event->value; /** @var TaskResult $result */ $id = $result->getId(); if (!$result->succeeded()) { $this->progressOutput->markAsFailed($id); unset($this->runtimes[$id]); return; } $this->progressOutput->markAsSucceeded($id); unset($this->runtimes[$id]); } }
  17. CalculateCommand.php unset($this->runtimes[$id]); } } private function exitWhenNoRuntimesAreLeft(): void { $this->loop->addPeriodicTimer(0.05,

    function () use (&$timer) { if (count($this->runtimes) === 0) { $this->progressOutput->finalize(); exit; } }); } private function catchSigintSignal(): void { $this->loop->addSignal(SIGINT, function() { echo "SIGINT received \n"; foreach ($this->runtimes as $runtime) { $runtime->close(); } $this->loop->stop(); exit; }); } }
  18. <?php declare(strict_types=1); namespace ParallelPhp; class TaskHandler { public function handle(int

    $id, Task $task): TaskResult { $channel = \parallel\Channel::open((string)$id); $cycles = random_int(1, 200); for ($i = 0; $i < $cycles; $i++) { usleep(25000); $channel->send((int)round($i / $cycles * 100)); } $channel->close(); return new TaskSucceededResult($id); } } TaskHandler.php
  19. FPM

  20. FPM ๏ WEB CONTEXT ๏ CONTINUOUSLY RUNNING LISTENER ๏ CAN

    BE SCALED HORIZONTALLY ๏ CLI CONTEXT PARALLEL
  21. FPM ๏ WEB CONTEXT ๏ CONTINUOUSLY RUNNING LISTENER ๏ CAN

    BE SCALED HORIZONTALLY ๏ CLI CONTEXT ๏ LIMITED TO ONE MACHINE PARALLEL
  22. FPM ๏ WEB CONTEXT ๏ CONTINUOUSLY RUNNING LISTENER ๏ CAN

    BE SCALED HORIZONTALLY ๏ CLI CONTEXT ๏ LIMITED TO ONE MACHINE ๏ BI-DIRECTIONAL COMMUNICATION PARALLEL
  23. FPM ๏ WEB CONTEXT ๏ CONTINUOUSLY RUNNING LISTENER ๏ CAN

    BE SCALED HORIZONTALLY ๏ CLI CONTEXT ๏ LIMITED TO ONE MACHINE ๏ BI-DIRECTIONAL COMMUNICATION ๏ (ZTS REQUIRED) PARALLEL