Laravel hangs when running command via exec without sending it to background

你。 提交于 2021-02-17 06:34:07

问题


I have a weird issue that I've been stuck with for a couple of days now. I'm trying to generate a pdf in a Laravel app using chrome headless with this command
google-chrome --headless --disable-gpu --print-to-pdf=outputfile.pdf http://localurl/pdf-html

The command basically opens chrome in headless mode, navigates to the given url and prints it as pdf saving the file in the specified location. This command is working perfectly when run in my system's shell (I'm using Ubuntu 18.04). Now, my issue arises when trying to run the same command from a Laravel controller, I've tried exec, shell_exec, system and passthru and all give me the same problem. If I run the command without redirecting output and running the process on the backgroung, by adding >> tmpfile 2>&1 & to the end of the command then the request hangs. Running the command in the background would not be a problem normally, except that I need for the command to finish in order to send the file back to the client as a download. By running it on the background this basically executes it asynchrounously and I have no way of knowing when the process ends (or to wait until it ends) to then send the file as a dowload on the response.

I've tried other alternatives to no avail. I've tried using Symfony's Process which comes bundled with Laravel and it also fails. I've tried using puppeteer and instead of running the google-chrome command use a node.js script with code from the puppeteer documentation (which by the way also works when run directly in my system shell), but when run from Laravel throws a Navigation Timeout Error exception.

Finally I created a simple php file with the following code:

<?php

$chromeBinary = 'google-chrome';
$pdfRenderUrl = "http://localhost:8000/pdf-html";
$fileName = 'invoice.pdf';
$outputDirectory = "/path/to/my/file/" . $fileName;

$command = sprintf(
    '%s --headless --disable-gpu --print-to-pdf=%s %s',
    escapeshellarg($chromeBinary),
    escapeshellarg($outputDirectory),
    escapeshellarg($pdfRenderUrl)
);
exec( $command  );
echo ( file_exists("/path/to/my/file/" . $fileName) ? 'TRUE' : 'FALSE');

?>

And the code runs just fine when run from shell like php thefile.php printing TRUE, meaning the command in exec was launched and after it ended then the file exists; and THAT is the exact code I'm using on Laravel except it only works, as mentioned above, when I send the process to the background. Can anybody throw me a line here, please? Thanks

EDIT: @namoshek thanks for the quick reply and sorry if I did not made myself clear. The problem is not long waiting times, perhaps I could live with that. The problem is that exec never finishes and I eventually have to forcefully terminate the process (nor exec, nor any other alternative, they all freeze the request completely forever, with the exception of Process which fails by throwing a TimeoutException). I'm using postman to query the endpoint. The frontend is an Angular app, meaning the request for the invoice download will be made asynchronously eventually. Furthermore the task itself is not a long running task, as a matter of facts it finishes pretty quick. Using a polling strategy or a notification system, to me, does not seem like a viable solution. Imagine an app with a download button to download a simple document and you have to click the button and then wait for the app to notify you via email (or some other way) that the document is ready. I could understand it if it were a more complicated process, but a document download seems like something trivial. But what has me at a loss is why is it that running the task from a php script works as I want it to (synchonously) and I can't replicate the behaviour on the laravel controller

EDIT: I've also tried using BrowserShot, which, BTW also fails. Browsershot provides a way to interact, behind the scenes with puppeteer by using Process, and generate a pdf file. And even though it's an external program, it still seems to me that the behaviour I'm getting is not normal, I should be able to obtain the download even if the request took 10secs to finish because it executed the external program synchronously. But in my case it's failing due to a timeout error

EDIT: So after a while I came upon the apparent reason of the server hang up. The problem is that I was using artisan's development server. This, initially, did not seem like a problem to me but it seems that artisan can't handle that load. In the feature I'm implementing I'm performing a request to a particular endpoint, let's call it endpoint 1, to generate the pdf, the code on this endpoint triggers the external command, and when executed synchronously it means the code in endpoint 1 is waiting for the external command to finish. The external command in turn needs to browse to endpoint 2 on the same server, endpoint 2 contains an html view with the content to be put on the pdf, since the server is still waiting on endpoint 1 for the return of the external command then endpoint 2 is unresponsive, which apparently creates a loop which artisan's development server can't handle. Problem is I did a quick search and I found nothing that indicated that defficiency on artisan's development server. I moved the environment to Apache just to test my theory and it worked, though it should be noted that the request takes a very long time to finish (around 10-20 secs). This, so far, seems like the only reasonable explanation as to why that issue was happenning. If anyone knows how I can improve performance on this request, or anyone can provide a better explanation to the original issue I'd appreciate it.


回答1:


@hrivera I'm a bit late to the game here, but regarding your last edit I believe you're almost correct, but my thoughts on this is that PHP's built-in server, which Laravel uses for development, is single threaded. The issue I had is that any assets within the page that was being passed to Chrome couldn't be loaded (CSS, js, etc) as the thread was already in use, and so it hung. Removing any assets from the HTML fixed the issue.

Production servers are multi-threaded, so we should have no issues. Not entirely sure I'm right, but wanted to comment anyway.




回答2:


I don't really get what you are asking for, because it seems you already understood that executing a long running task like creating a snapshot will block the request if being run synchronously. Using other software such as Puppeteer will not change that. If your requests needs to wait for the result of this process to return, then the only way to have your request return faster is by speeding up the task itself. Which is most likely not possible.

So, there are basically only two options left: Live with the long wait times (if you want to perform the task synchronously) or execute the request/task asynchronously. The latter can be achieved in two ways:

  1. Make the actual HTTP request be run in the background (using ajax) and use a loading indicator to keep your users patient. This way you could still run the process synchronously, but I would not recommend doing so as you would have to use high timeout times for the ajax request and in some situations the requests would probably still timeout (depending on the workload of your server).
  2. Use the power of Laravel and make use of queue workers to perform the calculation in background. When the snapshot generation is finished, you can then use one of the following three options to return the result:
    • Use polling on the client side to see if the result is available.
    • Send the result or a link to the result per mail or something similar to the user.
    • Use a notification system to notify the user about the finished process and return the result in some way (i.e. fetch it or send it as part of the notification - there are plenty of options available). A built-in notification system that does exactly what I described is Laravel Echo. On receiving the notification that tells you the process finished, you could then fetch the result from the server.

In current times, the standard for web apps and user experience is option 2 with the notification system (3rd point).



来源:https://stackoverflow.com/questions/52806443/laravel-hangs-when-running-command-via-exec-without-sending-it-to-background

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!