send data to php background thread script - php

Sometimes I have a long task which I don't want the current PHP thread to wait for, and I've done something like the following. Passing data in a session is a little kludgy, but seems to work.
I have another similar application, except file1.php is not being accessed by the user's client, but by another server, and the user is only accessing the other server. As such, a session cookie will not be available to the session_start() in file1.php, and it will have to create a separate session file for each occurrence.
What are other options to pass data to a background worker script? I am not passing huge amount of data, but it still will be 1kb or so.
file1.php
session_start();
$_SESSION['_xfr']=$_POST;
$status=exec($'/usr/bin/php -q /path/to/my/worker.php'.' '.session_id().' >/dev/null &');
echo('Tell client we are done.');
file2.php
session_id($argv[1]);//Set by parent
session_start();
$data=$_SESSION['_xfr'];
//Do some task

You can just send the data via args, as json:
file1.php
$status=exec($'/usr/bin/php -q /path/to/my/worker.php'.' '.json_encode($_POST).' >/dev/null &');
echo('Tell client we are done.');
file2.php
$data=json_decode($argv[1]);
//Do some task

If the goal is to return a status to a client and continue processing, you might find it easier to simply do something like.
public function closeConnection($body, $responseCode){
// Cause we are clever and don't want the rest of the script to be bound by a timeout.
// Set to zero so no time limit is imposed from here on out.
set_time_limit(0);
// Client disconnect should NOT abort our script execution
ignore_user_abort(true);
// Clean (erase) the output buffer and turn off output buffering
// in case there was anything up in there to begin with.
ob_end_clean();
// Turn on output buffering, because ... we just turned it off ...
// if it was on.
ob_start();
echo $body;
// Return the length of the output buffer
$size = ob_get_length();
// send headers to tell the browser to close the connection
// remember, the headers must be called prior to any actual
// input being sent via our flush(es) below.
header("Connection: close\r\n");
header("Content-Encoding: none\r\n");
header("Content-Length: $size");
// Set the HTTP response code
// this is only available in PHP 5.4.0 or greater
http_response_code($responseCode);
// Flush (send) the output buffer and turn off output buffering
ob_end_flush();
// Flush (send) the output buffer
// This looks like overkill, but trust me. I know, you really don't need this
// unless you do need it, in which case, you will be glad you had it!
#ob_flush();
// Flush system output buffer
// I know, more over kill looking stuff, but this
// Flushes the system write buffers of PHP and whatever backend PHP is using
// (CGI, a web server, etc). This attempts to push current output all the way
// to the browser with a few caveats.
flush();
}
Otherwise your options are limited for multithreading in the HTTP server, and shelling out via exec as you are doing it the other way to go ... however, you may want to nohup the command as opposed simply running it as a child process of the current thread.

Related

Run clean up code after die()

I'm working on speeding up the response time of some php code that generates html. One of the issues with the code, is that when determines a piece of information does not need to be displayed, it makes a sql call to delete the item from the database. This isn't visible to the user, and the won't be visible to the server until the next time the page is loaded, so that sql query does not need to be run as soon as the system knows that it should be run.
What I would like to do is return the response to the user, with the generated html, and then make the sql queries. I was trying this flush and ob_flush, but the page response is still not loaded until I make a call to die.
Is there anyway in PHP to run code after a call to die() so that the user gets their data and then I can run my database clean up code and the client is no long waiting on me to close the connection?
You can register shutdown functions using register_shutdown_function:
register_shutdown_function(function () {
// cleanup stuff
});
Or in older versions of PHP:
function myFunc() {
// cleanup stuff
}
register_shutdown_function("myFunc");
Thanks to #robbrit and #Luis Siquot. I was looking at register_shutdown_function and because of Luis' comment, I was reading the comments on that page, and I came across "When using php-fpm, fastcgi_finish_request() should be used instead of register_shutdown_function() and exit()"
which lead me to fastcgi_finish_request which says:
"This function flushes all response data to the client and finishes the request. This allows for time consuming tasks to be performed without leaving the connection to the client open."
So it looks like fastcgi_finish_request() is what I'm looking for, not register_shutdown_function()
Edit: It seems that fastcgi_finish_request() requires another library, so instead use:
ob_end_clean();
header("Connection: close");
ignore_user_abort(true); // just to be safe
ob_start();
echo "The client will see this!";
$size = ob_get_length();
header("Content-Length: $size");
//Both of these flush methods must be called, otherwise strange things happen.
ob_end_flush();
flush();
echo "The client will never see this";

Why does echo not work in a do while loop or even when ignore_user_abort(1)?

I used a log printer to test to see if the ignore_user_abort works, it does. However the echo does not want to work. Will an echo only work when a loop compleates?
<?php
ignore_user_abort(1); // run script in background
set_time_limit(0); // run script forever
$interval=2; // do every 2 sec...
$i=0;
$lastRunLog = 'lastrun.log';
do{
// add the script that has to be ran every 2 sec here
// ...
echo 'Test: '.$i;
file_put_contents($lastRunLog, time());
sleep($interval); // wait 2 sec
$i=$i+1;
}while(true);
?>
Yes, and no.
There are three output buffers you need to worry about:
PHP can buffer output. You can use the ob_ functions to control that buffer and force output to apache/iis (the web server)
Apache/IIS have their own buffers, and will only send information when they think they have enough to send. This is also compunded by mod_gzip or other compression. You can't control this through PHP.
There are also potentially other caches/proxies between your browser and the server. These MAY hold up output until it receives a full response.
So, echo puts into the PHP buffer immediately. You can send the buffer to teh webserver, but after that it's out of your hands.

How to flush data to browser but continue executing

I have a ob_start() and a corresponding ob_flush(). I would like to flush a portion of data and continue executing the rest. Using ob_flush() didn't help. Also if possible rest needs to happen without showing loading in browser.
EDIT:
I don't want to use ajax
I have done this in the past and this is how I solved it:
ob_start();
/*
* Generate your output here
*/
// Ignore connection-closing by the client/user
ignore_user_abort(true);
// Set your timelimit to a length long enough for your script to run,
// but not so long it will bog down your server in case multiple versions run
// or this script get's in an endless loop.
if (
!ini_get('safe_mode')
&& strpos(ini_get('disable_functions'), 'set_time_limit') === FALSE
){
set_time_limit(60);
}
// Get your output and send it to the client
$content = ob_get_contents(); // Get the content of the output buffer
ob_end_clean(); // Close current output buffer
$len = strlen($content); // Get the length
header('Connection: close'); // Tell the client to close connection
header("Content-Length: $len"); // Close connection after $len characters
echo $content; // Output content
flush(); // Force php-output-cache to flush to browser.
// See caveats below.
// Optional: kill all other output buffering
while (ob_get_level() > 0) {
ob_end_clean();
}
As I said in a couple of comments before, you should watch out for gzipping your content, since that will alter the length of your content, but not change the header about it. It also can buffer your output, so it won't get send to the client instantly.
You could try letting apache know to not gzip your content by using apache_setenv('no-gzip', '1');. But this will not work if you use rewrite-rules to go to your page, since then it will also modify those environment variables. At least, it did so for me.
See more caveats about flushing your content to the user in the manual.
ob_flush writes the buffer. In other words, ob_flush tells PHP to give Apache (or nginx/lighttpd/whatever) the output and then for PHP to forget about it. Once Apache has the output, it does whatever it wants with it. (In other words, after ob_flush it's out of your control whether or not it gets immediately written to the browser).
So, short answer: There's no guaranteed way to do that.
Just a guess, you're likely looking for AJAX. Whenever people are trying to manipulate when page content loads as you're doing, AJAX is almost always the correct path.
If you want to continue a task in the background, you can use ignore_user_abort, as detailed here, however, that is often not the optimal approach. You essentially lose control over that thread, and in my opinion, a web server thread is not where heavy processing belongs.
I would try to extract it out of the web facing stuff. This could mean a cron entry or just spawning a background process from inside of PHP (a process that though started from inside of script execution will not die with the script, and the script will not wait for it to finish before dying).
If you do go that route, it will mean that you can even make some kind of status system if necessary. Then you could monitor the execution and give the user periodic updates on the progress. (Technically you could make a status system with a ignore_user_abort-ed script too, but it doesn't seem as clean to me.)
this is my function
function bg_process($fn, $arr) {
$call = function($fn, $arr){
header('Connection: close');
header('Content-length: '.ob_get_length());
ob_flush();
flush();
call_user_func_array($fn, $arr);
};
register_shutdown_function($call, $fn, $arr);
}
wrap the function to be executed in the end, after php close the connection. and of course the browser will stop buffering.
function test() {
while (true) {
echo 'this text will never seen by user';
}
}
this is how to call the function
bg_process('test');
first argument is callable,
second argument is an array to be passed to 'test' function with an indexed array
Note : I don't use ob_start() at the beginning of the script.
I have an article explaining how this can be achieved using apache/mod_php on my blog here: http://codehackit.blogspot.com/2011/07/how-to-kill-http-connection-and.html Hope this helps, cheers
If you are using PHP-FPM:
ignore_user_abort(true);
fastcgi_finish_request();
Above two functions are the key factors which ignore_user_abort prevents error and fastcgi_finish_request closes client connection.
fastcgi_finish_request
This function flushes all response data to the client and finishes the request. This allows for time consuming tasks to be performed without leaving the connection to the client open.
not working on Apache.(PHP 5 >= 5.3.3, PHP 7)
Use:
header("Content-Length: $len");
..where $len is the length of the data to be flushed to the client.
I don't have the background to know when and where this is going to work, but I tried on a few browsers, and all returned instantly with:
<?PHP
header("Content-length:5");
echo "this is more than 5";
sleep(5);
?>
edit: Chrome, IE, and Opera showed this, while FireFox showed this is more than 5. All of them closed the request after that though.

Fire-and-forget in PHP

Final update
Seems like I did make a very simple error. Since I already have a stream implementation I can just not start reading from the stream :D
I'm trying to achieve fire-and-forget like functionality in PHP.
From php.net
<?php
ignore_user_abort(true);
header("Content-Length: 4");
header("Connection: Close");
echo "abcd";
flush();
sleep(5);
echo "Text user should not see"; // because it should have terminated
?>
This works if I open the script with a browser. (shows "abcd").
But if I open it with file_get_contents or some stream library it will wait for ~5 seconds and show the second text as well.
I'm using PHP 5.2.11 / Apache 2.0
Update
I seems there is some confusion about what I'm trying to accomplish.
I don't want to hide output using output buffers (that's stupid). I want to have the client terminate before the server starts a possibly lengthy process (sleep(5)) and I don't want the client to wait for it (this is what fire-and-forget means, sort off).
The use of output buffers is merely a side effect. I've amended the sample code without the use of output buffers.
What I don't understand is: why does this script behave differently when accessing it from the browser vs. fetching it in PHP with file_get_contents("http://dev/test.php") or some stream library? What I've seen in testing is that for instance stream_get_contents will actually block for 5 seconds before it returns any output at all, the is quite the opposite of what I want.
Update2
Some more results:
The browser somehow responds to the flush(). I can't figure out how to replicate this behavior with streams in PHP, my streams keep blocking.
I've tried fread and found that it behaves similar to stream_get_contents.
Specifying a maxlength has no effect, it will still block for ~5 seconds.
Changing the blocking mode has no effect (other than generating a bunch more calls to stream_get_contents()). It will wait ~5 seconds before returning anything.
stream_set_read_buffer has no effect (tested on a PHP 5.3.5 sever)
The second portion of text is showing up because you're stopping output buffering with ob_end_flush() and ob_end_clean(). When that happens PHP outputs content as normal. Try something like the following:
<?php
ob_start(); // turn on output buffering
print "Text the user will see.";
ob_flush(); // send above output to the user and keep output buffering on
print "Text the user will never see";
ob_end_clean(); // empty the buffer and turn off output buffering. your script should end here.
?>
It's important for ob_end_clean() to appear at the end of the script. It empties the buffer and does not send its contents to the user, thus keeping everything after ob_flush() hidden.
How do you access the script using file_get_contents? How do you access it with your browser? If you access the script without "http://", of course it will never get executed. Use the same URL as in the browser.
Edit:
The browser will render the page even before the connection is closed. Even if you flush, I don't think the connection is closed. You can fire up Wireshark and check. stream_get_contents and file_get_contents will block until they have all the output. Even if you flushed, they can't be sure that there isn't more content. Since the content-length header didn't seem to make {file,stream}_get_contents return earlier, you probably need to implement your own buffering, ala. fopen, read, fclose.
Seems like I did make a very simple error. Since I already have a stream implementation I can just not start reading from the stream :D

Can a PHP script trick the browser into thinking the HTTP request is over?

I first configure my script to run even after the HTTP request is over
ignore_user_abort(true);
then flush out some text.
echo "Thats all folks!";
flush();
Now how can I trick the browser into thinking the HTTP request is over? so I can continue doing my own work without the browser showing "page loading".
header(??) // something like this?
Here's how to do it. You tell the browser to read in the first N characters of output and then close the connection, while your script keeps running until it's done.
<?php
ob_end_clean();
header("Connection: close");
ignore_user_abort(true); // optional
ob_start();
echo ('Text the user will see');
$size = ob_get_length();
header("Content-Length: $size");
ob_end_flush(); // Will not work
flush(); // Unless both are called !
// At this point, the browser has closed connection to the web server
// Do processing here
echo('Text user will never see');
?>
Headers won't work (they're headers, so they come first)
I don't know of any way to close the http connection without terminating the script, though I suppose there's some obscure way of doing it.
Telling us what you want to do after the request is done would help us give better suggestions.
But generally, I'd be thinking about one of the following:
1) Execute some simple command-line script (using exec()) that looks like:
#!/bin/sh
php myscript.php <arg1> <arg2> .. <argN> &
Then kick that off from your http-bound script like:
<?PHP
exec('/path/to/my/script.sh');
?>
Or:
2) Write another program (possibly a continuously-running daemon, or just some script that is cronned ever so often), and figure out how your in-request code can pass it instructions. You could have a database table that queues work, or try to make it work with a flat file of some sort. You could also have your web-based script call some command-line command that causes your out-of-request script to queue some work.
At the end of the day, you don't want your script to keep executing after the http request. Assuming you're using mod_php, that means you'll be tying up an apache process until the script terminates.
Maybe this particular comment on php.net manual page will help: http://www.php.net/manual/en/features.connection-handling.php#71172
Theoretically, if HTTP 1.1 keep-alive is enabled and the client receives the amount of characters it expects from the server, it should treat it as the end of the response and go ahead and render the page (while keeping the connection still open.) Try sending these headers (if you can't enable them another way):
Connection: keep-alive
Content-Length: n
Where n is the amount of characters that you've sent in the response body (output buffering can help you count that.) I'm sorry that I don't have the time to test this out myself. I'm just throwing in the suggestion in case it works.
The best way to accomplish this is using output buffering. PHP sends the headers when it's good and ready, but if you wrap your output to the browser with ob_* you can control the headers every step of the way.
You can hold a rendered page in the buffer if you want and send headers till the sun comes up in china. This practice is why you may see a lot of opening <?php tags, but no closing tags nowadays. It keeps the script from sending any headers prematurely since there might some includes to consider.

Categories