Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run externals.io on ReactPHP to improve performances #48

Closed
wants to merge 9 commits into from
Closed

Conversation

mnapoli
Copy link
Owner

@mnapoli mnapoli commented Jul 23, 2017

This PR introduces ReactPHP in order to replace PHP-FPM.

The web server is handled by React as a long running PHP application. As a result response time are drastically reduced.

Also, this was fun.

Profiling

capture d ecran 2017-07-23 a 15 25 01

capture d ecran 2017-07-23 a 15 25 48

Demo

The branch is deployed thanks to Platform.sh: https://react-e5mxmca-2snxbat5vggso.eu.platform.sh/

root: 'web'
# The front-controller script which determines where to send
# non-static requests.
passthru: '/index.php'
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how we move from a PHP-FPM setup to a ReactPHP setup on Platform.sh (documentation: https://docs.platform.sh/languages/php.html#alternate-start-commands)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You probably still want the locations block, so that static assets are served as you expect. Just replace passthru: '/index.php' with passthru: true (once your application is talking HTTP directly, it has no concept of script anymore).


crons:
# Look for new emails every 5 minutes.
drush-queue:
sync:
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I simply renamed the job for clarity, this should not have been included in this PR, sorry for the noise.

});

$loop->run();
});
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ./console serve command runs the React webserver.

Since React supports PSR-7 this is extremely easy to integrate with Stratify or any other PSR-7 framework.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As documented thoroughly: http://reactphp.org/http/#request

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, from what I've read this should not be an issue here but I learned something!


$server = new Server(function (ServerRequestInterface $request) use ($http, $db) : ResponseInterface {
// Reset the DB connection
$db->close();
Copy link
Owner Author

@mnapoli mnapoli Jul 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should move this into a middleware.

This line resets the database connection on every HTTP request. This is necessary because database connections can break or timeout, and Doctrine will not reconnect automatically. This is a problem specific to long running applications (like ReactPHP), on a classic application the PHP process dies (with the DB connection) on every HTTP request so there is no need for that.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found this analysis of the issue:

http://brady.lucidgene.com/articles/pdo-lost-mysql-connection-error

There are some ideas for other approaches in the comment section...

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very interesting link thank you! I was starting to consider whether to abstract DBAL behind another abstraction that restarts connections automatically… But that's overkill I think, and I don't think DBAL has an interface for the Connection

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know why but in several projects I tried to use this interface but always ended up using Doctrine\DBAL\Connection directly instead.

'path.public' => __DIR__ . '/../../web',

// Platform.sh defines this variable
'http.port' => env('PORT', 8000),
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By default the application will run on port 8000 (e.g. in dev environment when running make preview).

@@ -27,8 +30,11 @@
* HTTP stack.
*/
return pipe([
BlackfireMiddleware::class,
NewRelicMiddleware::class,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since Blackfire and New Relic consider that 1 HTTP request == 1 PHP process, they will not work correctly out of the box.

Those 2 middlewares activate manually Blackfire and New Relic for each HTTP request.

Those 2 middlewares could be moved to their own open source projects though.

MaintenanceMiddleware::class,
ErrorHandlerMiddleware::class,
AssetsMiddleware::class,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This middleware is useful only in dev environment. It serves static files (aka assets) like CSS, JS, etc.

Previously we didn't need it because PHP's built-in webserver (php -S 127.0.0.1:8000) was handling it automatically. But since we are using React as the webserver, we need to serve the static files.

Using a middleware is not really efficient performance-wise, but this middleware is only serving files in dev environment, not in production.

In production the middleware will not do much work, still it could be optimized. However I don't really care, the application is super duper fast, that's maybe something to optimize later if we really care.

@@ -141,6 +147,7 @@

// Keep backward compatibility with old URLs (old threads)
'/thread/{id}' => route(function (int $id, EmailRepository $emailRepository, Connection $db) {
newrelic_name_transaction('thread_legacy');
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have been committed on master too, my mistake.

@@ -1,5 +1,5 @@
preview:
ENV=dev php -S localhost:8000 -t web
ENV=dev ./console serve
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make preview is the command used in development environment to run a small webserver.

We were previously using PHP's built-in webserver, now we simply launch the React application.

@staabm
Copy link
Contributor

staabm commented Jul 23, 2017

I would suggest to use amphp/aerys instead which als supports websockets etc. But still this is kinda nice!

@mnapoli
Copy link
Owner Author

mnapoli commented Jul 23, 2017

@staabm thanks, but we do not need websockets for now? I went for the most popular implementation that could work on Platform to be honest :)

$body = new Stream($file);

$array = explode('.', $file);
$extension = strtolower(array_pop($array));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use pathinfo ?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Thanks!

{
$path = $request->getUri()->getPath();

$file = $this->publicPath . $path;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to make sure that te resulting path is within your webroot. Otherwise one can read arbritrary files..

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do, I implemented this quickly for dev environments but you are right it's better to secure properly.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably best not to implement your own, e.g. Aerys has that built-in. Not sure whether ReactPHP's server has something like that yet.


$array = explode('.', $file);
$extension = strtolower(array_pop($array));
$mimeType = self::MIME_TYPES[$extension] ?? 'application/octet-stream';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dynamically determine the mimetype instead of your custom map?

http://php.net/manual/en/function.mime-content-type.php

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted a fast response time because for most assets this will work. Maybe that could be a fallback to the array solution.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good enough, Aerys does the same thing.

$extension = strtolower(array_pop($array));
$mimeType = self::MIME_TYPES[$extension] ?? 'application/octet-stream';

return new Response($body, 200, [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this support cache revalidation with http 304 or similar?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really important when it's only used for development.

"sentry/sentry": "^1.7"
"sentry/sentry": "^1.7",
"react/http": "^0.7.2",
"blackfire/php-sdk": "^1.8"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Afair you only need this sdk when the extension is not loaded.

You could also do a extension_loaded check before registering the middleware and drop this dependency

@mnapoli mnapoli closed this Sep 14, 2018
@mindplay-dk
Copy link

@mnapoli did you intentionally close this? if it didn't work out, I'd love to hear why :-)

@staabm
Copy link
Contributor

staabm commented Sep 14, 2018

closed because of #90

@WyriHaximus
Copy link

@staabm thanks, was also interested in the why 👍

@mnapoli
Copy link
Owner Author

mnapoli commented Sep 14, 2018

Right sorry for not leaving a comment ^^ Testing React was fun. This PR could be reopened later. But I want to leave platform.sh builds available for other PRs. And in the future (hopefully soon) I want to try and move externals.io to serverless/aws lambda using Bref. There is no strong reason except testing and learning so don’t expect any gains. In any case that wouldn’t work with React’s execution model.

@mindplay-dk
Copy link

@mnapoli as long as you're just messin' around, I recommend trying out Swoole as well - it's neat :-)

@mnapoli
Copy link
Owner Author

mnapoli commented Sep 15, 2018

Right I've been wanting to look at it, it's very promising!

Ping @Nyholm the diff of this PR might interest you ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants