roadrunner-server / roadrunner

🤯 High-performance PHP application server, process manager written in Go and powered with plugins
https://docs.roadrunner.dev
MIT License
7.86k stars 408 forks source link

Integration: How to use with Symfony HttpKernel #11

Closed georgeboot closed 6 years ago

georgeboot commented 6 years ago

Can someone please provide an example on how to use this with Symfony HttpKernel (also used by Laravel)?

tobias-kuendig commented 6 years ago

I guess we should be able to do something like that in the psr-worker.php:

https://github.com/mnvx/laravel-reactphp/blob/master/src/LaravelReactServer.php#L75

Unfortunately, if I run rr serve I don't get a running server under localhost:8080.

This is what I have so far. Even the simple demo script does not work for me:

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

ini_set('display_errors', 'stderr');

$relay = new Spiral\Goridge\StreamRelay(STDIN, STDOUT);
$psr7 = new Spiral\RoadRunner\PSR7Client(new Spiral\RoadRunner\Worker($relay));

while ($req = $psr7->acceptRequest()) {
    try {
        $resp = new \Zend\Diactoros\Response();
        $resp->getBody()->write("hello world");

        $psr7->respond($resp);
    } catch (\Throwable $e) {
        $psr7->getWorker()->error((string)$e);
    }
}
# rpc bus allows php application and external clients to talk to rr services.
rpc:
  # enable rpc server
  enable: true

  # rpc connection DSN. Supported TCP and Unix sockets.
  listen: tcp://127.0.0.1:6001

# http service configuration.
http:
  # set to false to disable http server.
  enable:     true

  # http host to listen.
  address:    0.0.0.0:8080

  # max POST request size, including file uploads in MB.
  maxRequest: 200

  # file upload configuration.
  uploads:
    # list of file extensions which are forbidden for uploading.
    forbid: [".php", ".exe", ".bat"]

  # http worker pool configuration.
  workers:
    # php worker command.
    command:  "php psr-worker.php"

    # connection method (pipes, tcp://:9000, unix://socket.unix).
    relay:    "pipes"

    # worker pool configuration.
    pool:
      # number of workers to be serving.
      numWorkers: 4

      # maximum jobs per worker, 0 - unlimited.
      maxJobs:  0

      # for how long pool should attempt to allocate free worker (request timeout). Nanoseconds atm. (60s)
      allocateTimeout: 60000000000

      # amount of time given to worker to gracefully destruct itself. Nanoseconds atm. (30s)
      destroyTimeout:  30000000000

# static file serving.
static:
  # serve http static files
  enable:  true

  # root directory for static file (http would not serve .php and .htaccess files).
  dir:   "public"

  # list of extensions to forbid for serving.
  forbid: [".php", ".htaccess"]
➜  ./rr serve -d -v  
DEBU[0000] [rpc]: started                               
DEBU[0000] [http]: started                              
DEBU[0000] [static]: started  
➜  sudo netstat -planet | grep -i 8080
# Empty!
wolfy-j commented 6 years ago

Are you using Mac? Try using different http port, like 8090. Sometimes OSx would block default http ports without telling developer anything about it.

Also, try running rr http:workers -i from the same folder to see if workers were properly initialized.

wolfy-j commented 6 years ago

Also, I just noticed that netstat on OSx won't show the listeners until first connection is made.

tobias-kuendig commented 6 years ago

Not on a Mac, I'm running Ubuntu 16.04. I have tried several ports already, unfortunately without luck. The strange thing is, that I got it running a few days earlier.

I configured rr to listed on port 8089: Only the rpc server is listed:

➜ sudo netstat -planet | grep -i rr
tcp        0      0 127.0.0.1:6001          0.0.0.0:*               LISTEN      1000       302305      25982/rr       

rr http:workers -i returns nothing. When i Ctrl + C the serve process it returns with an EOF error.

➜  ./rr http:workers -i
# No output
# Until Ctrl + C in the other terminal:
Error: unexpected EOF
wolfy-j commented 6 years ago

Mmmmm, can you try to turn RPC server down? Simply set enable to false.

wolfy-j commented 6 years ago

Also, please note we have released new version of Goridge (underlying protocol, 2.0 version is required) about a week ago, it changes the payload prefix to incorporate error detection. Make sure that your composer is up to date if you have used earlier. Thought i assume it's something with port mapping.

wolfy-j commented 6 years ago

I just verified your code working on Ubuntu 16.04. Maybe try to change 0.0.0.0 to 127.0.0.1, possibly some local firewall?

tobias-kuendig commented 6 years ago

A reboot solved the problem! Looks like it was just a problem with my setup.

I now have the following psr-worker script. Laravel responds correctly, but it somehow doesn't get the url /path correctly. I'm always shown the home page of my app. $request->path() always returns /.

<?php
use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;

require 'vendor/autoload.php';

ini_set('display_errors', 'stderr');

$relay = new Spiral\Goridge\StreamRelay(STDIN, STDOUT);
$psr7 = new Spiral\RoadRunner\PSR7Client(new Spiral\RoadRunner\Worker($relay));

while ($req = $psr7->acceptRequest()) {
    try {
        $app = require_once __DIR__.'/bootstrap/app.php';
        $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

        $request = Illuminate\Http\Request::capture();
        $response = $kernel->handle($request);

        $psr7factory = new DiactorosFactory();
        $psr7response = $psr7factory->createResponse($response);

        $psr7->respond($psr7response);
    } catch (\Throwable $e) {
        $psr7->getWorker()->error((string)$e);
    }
}
tobias-kuendig commented 6 years ago

I figured it out! This worker scripts works with Laravel. To get it working with Symfony it shouldn't be too much work.

This article helped me a lot: https://symfony.com/doc/current/components/psr7.html

Unfortunately POST data seems to go missing somewhere along the way...

<?php
use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;

require 'vendor/autoload.php';

ini_set('display_errors', 'stderr');

$relay = new Spiral\Goridge\StreamRelay(STDIN, STDOUT);
$psr7 = new Spiral\RoadRunner\PSR7Client(new Spiral\RoadRunner\Worker($relay));

while ($req = $psr7->acceptRequest()) {
    try {
        $app = require_once __DIR__.'/bootstrap/app.php';
        $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

        $httpFoundationFactory = new HttpFoundationFactory();
        $request = Illuminate\Http\Request::createFromBase($httpFoundationFactory->createRequest($req));

        $response = $kernel->handle($request);

        $psr7factory = new DiactorosFactory();
        $psr7response = $psr7factory->createResponse($response);

        $psr7->respond($psr7response);
    } catch (\Throwable $e) {
        $psr7->getWorker()->error((string)$e);
    }
}
wolfy-j commented 6 years ago

Awesome!

georgeboot commented 6 years ago

@tobias-kuendig nice work! However, in your example you are still instantiating a kernel on every request. Wouldn't it be better to only have the PSR-7 request being handled by a concurring kernel? Something like Swoole?

wolfy-j commented 6 years ago

I guess you can just move

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

outside of the loop and it should do the trick.

tobias-kuendig commented 6 years ago

Yes, I actually tried moving the $kernel out of the while loop and it seems to work.

I did a quick ab test with only GET requests and that seemed to work with 1000 requests.

My application gets around 40 req/s with roadrunner. With the conventional nginx on docker setup I get around 20 req/s. The application is quiet heavy but it looks already like it's already twice as fast!

The only problem remaining is that the POST data isn't in the request object parsed by Laravel. I guess it gets lost somewhere on the way.

<?php
use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;

require 'vendor/autoload.php';

ini_set('display_errors', 'stderr');

$relay = new Spiral\Goridge\StreamRelay(STDIN, STDOUT);
$psr7 = new Spiral\RoadRunner\PSR7Client(new Spiral\RoadRunner\Worker($relay));

$app = require_once __DIR__.'/bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

while ($req = $psr7->acceptRequest()) {
    try {
        $httpFoundationFactory = new HttpFoundationFactory();
        $request = Illuminate\Http\Request::createFromBase($httpFoundationFactory->createRequest($req));

        $response = $kernel->handle($request);

        $psr7factory = new DiactorosFactory();
        $psr7response = $psr7factory->createResponse($response);

        $psr7->respond($psr7response);
    } catch (\Throwable $e) {
        $psr7->getWorker()->error((string)$e);
    }
}
wolfy-j commented 6 years ago

Yes, I actually tried moving the $kernel out of the while loop and it seems to work. I would recommend you to be super careful about it and watch for the workers memory usage.

In case of memory leak try to: 1) limit the number of worker usages (quick and dirty) 2) use fail switch ($psr->getWorker()->stop()), this will forward the request to next worker (more elegant).

Overall, we have it running with bootloaded core for weeks with no memory issues but we have put the long-running concept into the design of our framework and i'm not aware of Laravel behaviour in such scenarios.

oraoto commented 6 years ago

Currently, roadrunner doesn't parse JSON body automatically, we can do the parsing manually, based on @tobias-kuendig 's code:

<?php

require __DIR__ . "/vendor/autoload.php";

use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;

$relay = new Spiral\Goridge\StreamRelay(STDIN, STDOUT);
$psr7 = new Spiral\RoadRunner\PSR7Client(new Spiral\RoadRunner\Worker($relay));

$app = require_once __DIR__ . '/bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

while ($req = $psr7->acceptRequest()) {
    try {
        $httpFoundationFactory = new HttpFoundationFactory();

        if (strpos($req->getHeaderLine("content-type"), "application/json") === 0) {
            $body = $req->getBody();
            $parsedBody = json_decode($body, true);
            $req = $req->withParsedBody($parsedBody);
        }
        $symfonyReq = $httpFoundationFactory->createRequest($req);
        $request = Illuminate\Http\Request::createFromBase($symfonyReq);

        $response = $kernel->handle($request);
        $kernel->terminate($request, $response);

        $psr7factory = new DiactorosFactory();
        $psr7response = $psr7factory->createResponse($response);
        $psr7->respond($psr7response);
    } catch (\Throwable $e) {
        $psr7->getWorker()->error((string)$e);
    }
}
ihipop commented 6 years ago

@oraoto I tested it with my production code on my localhost,not wok if the app code is stateful, and some code is hard coded to read php_sapi_name()/PHP_SAPI to some special behavior or logic in laravel or other component, This is definitely not a ZERO conf/code silver bullet for the traditional frameworks

wolfy-j commented 6 years ago

The application and framework must be designed to work in stateless mode. RoadRunner does work out of the box with Symfony, Spiral and Slim frameworks.

This is other recommendations: https://www.reddit.com/r/PHP/comments/8s30bf/roadrunner_php_psr7_application_server_and_load/e11swdu/

Richard87 commented 6 years ago

Hi!

I am looking into trying this out soon, but I was thinking $kernel->terminate() should occure after $psr7->respond($psr7response);, and not before... (unless there are a specific roadrunner reason for it?)

Also HttpFoundationFactory can also be instantiated outside of the loop :)

oraoto commented 6 years ago

Projects like laravel-swoole and laravel-s actually make long runing Laravel application possible by resetting needed states on every request.

wolfy-j commented 6 years ago

@Richard87 would you mind posting your worker script or creating a repo i can link to rr once your integration is complete?

Richard87 commented 6 years ago

Hi @wolfy-j ! I posted my workerscript here: https://github.com/spiral/roadrunner/issues/18 :)