-
-
Notifications
You must be signed in to change notification settings - Fork 34
Description
While investigating the suitability of runtime/swoole:0.3.0
, I ran into a blocker caused by streamed responses.
I was evaluating runtime/swoole
because I ran into a problem similar to #50, and as of roadrunner-server/roadrunner#923 (comment), I also couldn't use RoadRunner (which I much prefer to Swoole, at first glance).
A controller producing large, slow responses
Testing this in automation is a bit of a mess (suggestions welcome though), but the idea is that I can stream a response as follows:
final class MyGiantCsvDownloadController
{
public function someAction(): Response
{
return new StreamedResponse(function () {
foreach (range(1, 5) as $i) {
echo str_repeat((string) $i, 100000), "\n";
sleep(1);
}
}, Response::HTTP_OK, [
'Content-Type' => 'text/plain',
]);
}
}
First problem: Symfony\Component\HttpKernel\EventListener\StreamedResponseListener
When interacting with this controller, through runtime/swoole
, the respone is not sent via \Runtime\Swoole\SymfonyHttpBridge
, where it should happen:
runtime/src/swoole/src/SymfonyHttpBridge.php
Lines 48 to 57 in 420d39e
case $sfResponse instanceof StreamedResponse: | |
ob_start(function ($buffer) use ($response) { | |
$response->write($buffer); | |
return ''; | |
}); | |
$sfResponse->sendContent(); | |
ob_end_clean(); | |
$response->end(); | |
break; |
In this block, $response->write($buffer);
should receive a massive chunk of 111111...22222...<SNIP>...55555
, but instead, all output is sent to STDOUT in the worker (not to the response object), and a warning is produced:
<SNIP>55555555555
PHP Warning: Swoole\Http\Response::write(): data to send is empty in /srv/share/vendor/runtime/swoole/src/SymfonyHttpBridge.php on line 50
Effectively, this write produces nothing, because the response was already sent by symfony/http-kernel
:
$response->write($buffer); |
After some investigation, I found that the culprit is that symfony/http-kernel
sends StreamedResponse
through a listener:
I disabled this listener by monkey-patching (while trying out stuff), but if you know a "clean" way to remove it completely from my application, lemme know.
Second problem: response buffer is completely sent in one shot
Assuming we disabled the StreamedResponseListener
(clean solution pending), the response content now makes it to Swoole\Http\Response#write($buffer);
, but in one big chunk: the HTTP client sees the first byte when swoole finished collecting the whole response.
This is because of this ob_start()
call not having a defined buffer size specified:
runtime/src/swoole/src/SymfonyHttpBridge.php
Lines 49 to 53 in 420d39e
ob_start(function ($buffer) use ($response) { | |
$response->write($buffer); | |
return ''; | |
}); |
According to the documentation for ob_start()
:
If the optional parameter
chunk_size
is passed, the buffer will be flushed after any output call which causes the buffer's length to equal or exceedchunk_size
. The default value0
means that the output function will only be called when the output buffer is closed.
Given that PHP uses a default buffer size of 4096
, perhaps it would be a good idea to add one here too? I tried it locally, and my HTTP client starts receiving bytes much earlier than 5 seconds, this way :)