Coder Social home page Coder Social logo

reactphp-child-process-messenger's Introduction

ReactPHP Child Process Messenger

Continuous Integration Latest Stable Version Total Downloads Code Coverage License

Plain messages and RPC style wrapper around react/child-process. For pooling messengers take a look at wyrihaximus/react-child-process-pool

Installation

To install via Composer, use the command below, it will automatically detect the latest version and bind it with ~.

composer require wyrihaximus/react-child-process-messenger 

Hassle less Example

While this package supports several ways of setting up communication between parent and child the simplest way is to create class implementing WyriHaximus\React\ChildProcess\Messenger\ChildInterface. Up on calling create everything is set up and created to handle supported RPC's and messages.

<?php

use React\EventLoop\LoopInterface;
use WyriHaximus\React\ChildProcess\Messenger\ChildInterface;
use WyriHaximus\React\ChildProcess\Messenger\Messages\Payload;

use function React\Promise\resolve;

final class ExampleChild implements ChildInterface
{
    public static function create(Messenger $messenger, LoopInterface $loop)
    {
        $messenger->registerRpc('example', function (Payload $payload) {
            return resolve($payload->getPayload());
        });
    }
}

On the parent side you only need to call to spawn a child running that class:

MessengerFactory::parentFromClass('ExampleChild', $loop)->then(static function (Messenger $messenger): void {
    $messenger->rpc(/* etc etc */);
});

More Examples

For both message and RPC examples see the examples directory

Contributing

Please see CONTRIBUTING for details.

License

Copyright 2021 Cees-Jan Kiewiet

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

reactphp-child-process-messenger's People

Contributors

bartvanhoutte avatar dependabot-preview[bot] avatar dependabot-support avatar dependabot[bot] avatar elazar avatar fgikcm avatar fritz-gerneth avatar lucasnetau avatar mahmutbayri avatar ognjen-petrovic avatar rakdar avatar renovate[bot] avatar woytam avatar wyrihaximus avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

reactphp-child-process-messenger's Issues

/bin/child-process has absolute path to vendor/autoload.php breaking relocation of source

The new /bin/child-process file that is generated in v4.0 breaks deployments when mapping to seperate location due to hard-code absolute path to vendor/autoload.php

In development we install composer dependencies on the development machine and then this directory is mapped through to Docker containers through volume mapping. The absolute path to autoload.php in the Docker container is not the same as the absolute path on the development machine (different OS). This causes a Fatal error running our ReactPHP components

What is the purpose of hard-coding the path to autoload.php in a library?

locking the child processes

I am using the example time-format for testing.

in format.php file when I start some class, the child processes no longer work, see my code:

<?php

require dirname(dirname(__DIR__)) . '/vendor/autoload.php';
require dirname(dirname(__DIR__)) . '/myclass.php';

use React\EventLoop\Factory;
use React\Promise\Deferred;
use WyriHaximus\React\ChildProcess\Messenger\Messages\Invoke;
use WyriHaximus\React\ChildProcess\Messenger\Messages\Payload;
use WyriHaximus\React\ChildProcess\Messenger\Recipient;

$loop = Factory::create();

$myClass = new MyCLASS();

$recipient = \WyriHaximus\React\ChildProcess\Messenger\Factory::child($loop);
$recipient->registerRpc('format', function (Payload $payload) use ($loop, $myClass) {
    $myClass->executeAction();

    return \React\Promise\resolve([
        'formattedTime' => (new DateTime('@' . $payload['unixTime']))->format('c'),
    ]);
});

$loop->run();

But if I remove $myClass = new MyCLASS(); everything will work normally, which can be?

The examples do not work as expected

I tried to run the script but it seems that it did not work, I used the example folder, tried all the scripts it ...

But the echo's of scripts didnt work, example:

$messenger->rpc(MessageFactory::rpc('format', [
        'unixTime' => time(),
    ]))->then(function ($formattedTime) {
        echo $formattedTime['formattedTime'], PHP_EOL; // not are called
    });

are a bug?

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Detected dependencies

composer
composer.json
  • cakephp/utility ^4.2.4
  • doctrine/inflector ^2.0.3
  • evenement/evenement ^3.0.1
  • paragonie/random_compat ^9.0 || ^2.0
  • react/child-process ^0.6.2
  • react/event-loop ^1.2
  • react/promise ^2.8
  • react/promise-stream ^1.2
  • react/promise-timer ^1.6
  • react/socket ^1.9
  • symfony/polyfill-php81 ^1.23
  • thecodingmachine/safe ^1.3.3
  • wyrihaximus/composer-update-bin-autoload-path ^1.1.1
  • wyrihaximus/constants ^1.6
  • wyrihaximus/file-descriptors ^1.1
  • wyrihaximus/json-throwable ^4.2.0
  • wyrihaximus/ticking-promise ^3
  • phpstan/phpstan-phpunit ^0.12.22
  • wyrihaximus/async-test-utilities ^4.2.2
github-actions
.github/workflows/ci.yml
.github/workflows/craft-release.yaml
  • actions/checkout v4
  • WyriHaximus/github-action-jwage-changelog-generator v1
  • actions/checkout v4

  • Check this box to trigger a request for Renovate to run again on this repository

Upgrade AppVeyor config

Simple copy & paste issue for hacktoberfest.

The easiest way to do this is to edit the appveyor.yml file in this repo and create a pull request. Then go back to your fork and then use the Create new file button in the right just above the file listing.

The done promise after the rpc call is never resolved

With theexample code

Parent:

use React\EventLoop\Factory;
use WyriHaximus\React\ChildProcess\Messenger\Factory as MessengerFactory;
use WyriHaximus\React\ChildProcess\Messenger\Messages\Factory as MessageFactory;
use WyriHaximus\React\ChildProcess\Messenger\Messages\Payload;
use WyriHaximus\React\ChildProcess\Messenger\Messenger;

$loop = React\EventLoop\Factory::create();

echo "starting";
MessengerFactory::parentFromClass(\App\Service\ChildWorker::class, $loop)->then(function (Messenger $messenger) {
    return $messenger->rpc(
        MessageFactory::rpc('isPrime', ['number' => 66])
    )->always(function () use ($messenger) {
        $messenger->softTerminate(); // Be sure to terminate the child when we're done
    });
})->done(function (Payload $result) {
    echo 'Prime', PHP_EOL;


    if ($result['isPrime']) {
        echo 'Prime', PHP_EOL;
        return;
    }

    echo 'Not a prime', PHP_EOL;
});

$loop->run();

And child

namespace App\Service;
use React\EventLoop\LoopInterface;
use WyriHaximus\React\ChildProcess\Messenger\ChildInterface;
use WyriHaximus\React\ChildProcess\Messenger\Messages\Payload;
use WyriHaximus\React\ChildProcess\Messenger\Messenger;

class ChildWorker implements ChildInterface
{
    public static function create(Messenger $messenger, LoopInterface $loop)
    {    
        $messenger->registerRpc('isPrime', function (Payload $payload) {
            return [
                'isPrime' => self::isPrime($payload['number']),
            ];
        });
    }

    private static function isPrime(int $number)
    {
        for($i=$n>>1;$i&&$n%$i--;);return!$i&&$n>1;
    }
}

the

->done(function (Payload $result) {
    echo 'Prime', PHP_EOL;


    if ($result['isPrime']) {
        echo 'Prime', PHP_EOL;
        return;
    }

    echo 'Not a prime', PHP_EOL;
});

Promise is never resolved, it just close php

Exceptions thrown in calls to Messenger::callRpc() from Rpc::handle() are not handled

Hi

I discovered that the call to callRpc in \WyriHaximus\React\ChildProcess\Messenger\Messages\Rpc::handle doesn't handle exceptions thrown in user code ( closures registerd with registerRpc )

That's because Promise:done() of a FulfilledPromise should not never throw an exception ( the $onFulfilled callable in done() method of a FulfilledPromise is immediately called )

$this->callRpc($target, $payload)->done(
    function (array $payload) use ($uniqid,$target) {
        $this->write($this->createLine(Factory::rpcSuccess($uniqid, $payload)));
    },
    function ($error) use ($uniqid) {
        $this->write($this->createLine(Factory::rpcError($uniqid, $error)));
    },
    function ($payload) use ($uniqid) {
        $this->write($this->createLine(Factory::rpcNotify($uniqid, $payload)));
    }
);

A possible fix would be

try {
    $this->callRpc($target, $payload)->then(
        function (array $payload) use ($uniqid,$target) {
            $this->write($this->createLine(Factory::rpcSuccess($uniqid, $payload)));
        },
        function ($error) use ($uniqid) {
            $this->write($this->createLine(Factory::rpcError($uniqid, $error)));
        },
        function ($payload) use ($uniqid) {
            $this->write($this->createLine(Factory::rpcNotify($uniqid, $payload)));
        }
    );
}
catch (\Throwable $throwable) {
    $this->write($this->createLine(Factory::rpcError($uniqid, $throwable)));
}

To reproduce a similar error for example code in the ChildInterface an rpc with a closure throwing some \Error , for example

Argument 1 passed to WyriHaximus\React\ChildProcess\Messenger\Messages\Rpc::WyriHaximus\React\ChildProcess\Messenger\Messages{closure}() must be of the type array, string given, called in /usr/src/myapp/vendor/react/promise/src/FulfilledPromise.php on line 39

$messenger->registerRpc('example', function (Payload $payload) {
    return \React\Promise\resolve('i am not an array');
});

these kind of errors will be totally skipped and parent process will not notify any problem

Emitting of an error from Messenger in Child process is never handled

In a Child Process the Messenger class can emit errors in crashed() and iterateMessages() however there is no listener for these error events. The RPC message is never responded to and the parent waits forever.

A quick fix is adding the following in the Process::create() factory

$messenger->on('error', function($throwable) use(&$reject, $messenger) {
    $reject($throwable);
});

This will terminate the child process and the parent process will reject the RPC call with CommunicationWithProcessUnexpectedEndException

There may be a better way however I don't know the code well enough and if the error occurs in MessagesFactory::fromLine due to an invalid rpc method then we can't cleanly handle the error.

It requires a process to wait for the other to run?

a process is necessary to wait for other processes to run?

Because, I created a Web Socket server, and for it to be completely asynchronous I did that way, I am doing right? He will have locks?(the code is merely illustrative)

namespace MyApp;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;

class Chat implements MessageComponentInterface {
    public function onOpen(ConnectionInterface $conn) {
        $messenger->rpc(MessageFactory::rpc('open', [
            'data' => $conn->resourceId,
        ]))->then(function () {});
    }

    public function onMessage(ConnectionInterface $from, $msg) {
        $messenger->rpc(MessageFactory::rpc('message', [
            'data' => $msg,
        ]))->then(function () {});
    }

    public function onClose(ConnectionInterface $conn) {
        $messenger->rpc(MessageFactory::rpc('close', [
            'data' => $conn->resourceId,
        ]))->then(function () {});
    }

    public function onError(ConnectionInterface $conn, \Exception $e) {
        $messenger->rpc(MessageFactory::rpc('error', [
            'data' => $e,
        ]))->then(function () {});
    }
}

child method never resolves

The Factory::child method returns a promise that is not resolving for me.
If I dump the $messenger in the last chain, I see that is was created successfully, but for some reason the promise generated is not resolving, so I never get a resolution.

I can see that the messenger is being created successfully if I dump the variable here:

})->then(static function (Messenger $messenger) use ($loop, $termiteCallable): Messenger {
if ($termiteCallable === null) {
$termiteCallable = static function () use ($loop): void {
$loop->addTimer(
self::TERMINATE_TIMEOUT,
[
$loop,
'stop',
]
);
};
}
$messenger->registerRpc(
Messenger::TERMINATE_RPC,
static function (Payload $payload, Messenger $messenger) use ($termiteCallable): Promise\PromiseInterface {
$messenger->emit('terminate', [$messenger]);
$termiteCallable();
return Promise\resolve([]);
}
);
return $messenger;

What would I be doing that would prevent it from resolving?

Thank you!

Messenger doesn't handle child-process getting killed unexpectedly

Hi WyriHaximus,

i found an edge-case which this library doesnt yet support.

If you have a child process running, which for example exceeds the php memory_limit-value, it doesn't get caught by the messenger.

Here is some example code to reproduce the problem:

ChildProcess class

<?php

namespace Application\ChildProcess;

use React\EventLoop\LoopInterface;
use WyriHaximus\React\ChildProcess\Messenger\ChildInterface;
use WyriHaximus\React\ChildProcess\Messenger\Messenger;

class Overflow implements ChildInterface
{
    public static function create(Messenger $messenger, LoopInterface $loop)
    {
        $messenger->registerRpc('overflow', function () {
            ini_set('memory_limit','20M');

            $string = '';
            while (true) {
                $string .= '0123456789';
            }
        });
    }
}

main process that never realizes the child process has been killed

<?php

use Application\ChildProcess\Overflow;
use React\EventLoop\Factory;
use WyriHaximus\React\ChildProcess\Messenger\Factory as MessengerFactory;
use WyriHaximus\React\ChildProcess\Messenger\Messenger;
use WyriHaximus\React\ChildProcess\Messenger\Messages\Factory as MessageFactory;

require '../vendor/autoload.php';

$loop = Factory::create();

MessengerFactory::parentFromClass(Overflow::class, $loop)->then(function (Messenger $messenger) {
    $messenger->rpc(MessageFactory::rpc('overflow', []))->then(
        function () {
            echo "Obviously never gets here";
        },
        function () {
            echo "But also never gets here";
        }
    );
});

$loop->run();

env helper problem

cakephp/utility require cakephp/core which load vendor/cakephp/core/functions.php and overvride laravel env function

Child process is blocked by sleep()

I am working on a proof of concept where I would like to send percent completed messages from a long running child process to parent. I've modified the ping pong example and using a sleep(2) in a for loop in the child to simulate work or blocking processes.

The issue is that when i run this script I'm not seeing any results until the very end where i get all my percentages at once.

Parent process:

<?php
use React\ChildProcess\Process;
use React\EventLoop\Factory as EventLoopFactory;
use React\EventLoop\Timer\Timer;
use WyriHaximus\React\ChildProcess\Messenger\Factory as MessengerFactory;
use WyriHaximus\React\ChildProcess\Messenger\Messages\Factory as MessagesFactory;
use WyriHaximus\React\ChildProcess\Messenger\Messages\Payload;
use WyriHaximus\React\ChildProcess\Messenger\Messenger;
require __DIR__ . '/../bootstrap/bootstrap.php';
$loop = EventLoopFactory::create();

$process = new Process('exec php pong.php');

MessengerFactory::parent($process, $loop)->then(function (Messenger $messenger) use ($loop) {
    $messenger->on('message', function (Payload $payload) {
        echo $payload['percent'], PHP_EOL;
    });
    $messenger->on('error', function ($e) {
        echo 'Error: ', var_export($e, true), PHP_EOL;
    });                                     
    $messenger->message(MessagesFactory::message([
            'start' =>true,    
    ]));
});

$loop->run();

Child Process:

<?php
use React\EventLoop\Factory as EventLoopFactory;
use WyriHaximus\React\ChildProcess\Messenger\Factory as MessengerFactory;
use WyriHaximus\React\ChildProcess\Messenger\Messages\Factory as MessagesFactory;
use React\EventLoop\Timer\Timer;
use WyriHaximus\React\ChildProcess\Messenger\Messages\Payload;
use WyriHaximus\React\ChildProcess\Messenger\Messenger;
use WyriHaximus\React\ChildProcess\Messenger\Recipient;
require __DIR__ . '/../bootstrap/bootstrap.php';
$loop = EventLoopFactory::create();
$recipient = MessengerFactory::child($loop); 
$recipient->on('message', function (Payload $payload, Messenger $messenger) {
    $step = 0;                 
    $last_percent = 0;         
    $total_rows = 100000;      
    for($i=0;$i<$total_rows; $i++){ 
        sleep(2);              
        $step++;               
        $percent= ($step * 100) / $total_rows;
        if ($last_percent != round($percent)){
          $last_percent = round($percent);
          if (($last_percent % 10) == 0){ 
            $messenger->message(MessagesFactory::message([
            'percent' => $last_percent      
            ]));               
          }
        }
    }
});

$loop->run();

High memory usage and churn due to usage of Doctrine\Inflector

Doctrine\Inflector is used in WyriHaximus\React\ChildProcess\Messenger\Messages\Factory::fromLine to convert rpc_error and rpc_success to the camelCase rpcError / rpcSuccess.

$method = InflectorFactory::create()->build()->camelize($line['type']);

Running the benchmark/memory.php with PHP8 shows that using Doctrine increases peak memory usage from 4.1MB to 44-109MB. Additionally since the Inflector is initialised on every messenger line, then released, we have both a high memory churn but also high CPU cost related to that initialisation. This can be seen on an XDebug trace.

The commit history doesn't explain why Inflector is used instead of just doing a straight string equality comparison with $method === 'rpcSuccess' || $method === 'rpc_success' etc.

I can provide a PR. This would also remove the Doctrine\Inflector dependancy

Proposal - drop indigophp/hash-compat

Hello,

The indigophp/hash-compat project has been abandoned.
It was made to add the hash_* functions to old PHP versions.

On each composer update we do, we now have a warning telling us we should avoid using it.

After reading the doc of hash_equals, I see that it was introduced in PHP 5.6: https://www.php.net/manual/en/function.hash-equals.php
The PHP end of life branches page states that PHP 5.5 was abandoned on 2016 and is no more supported:
https://www.php.net/eol.php

So I propose to not use the indigophp/hash-compat project anymore, and just use the internal PHP versions.

Custom autoload.php not read in child process

In child-process.php, you check for autoload.php in specific folders. But if we use a custom autoload.php in our project, which is on another place, this one is not read, and obviously the child class can't be instancied.

It would be nice to have an option in Flexible::createFromClass() (or equivalent) to specify some files to require in the child-process.php

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.