PHP auto-kill a script if the HTTP request is cancelled/closed - php

The problem is that for a long process the PHP script keeps on executing whether or not the client browser is currently connected or not. Is there any possibility that if the client has terminated the Ajax call to a script then the script also terminates on server?

As pointed out by #metadings php does have a function to check for connection abort named connection_aborted(). It will return 1 if connection is terminated otherwise 0.
In a long server side process the user may need to know if the client
is disconnected from the server or he has closed the browser then the
server can safely shutdown the process.
Especially in a case where the application uses php sessions then if we left the long process running even after the client is disconnected then the server will get unresponsive for this session. And any other request from the same client will wait until the earlier process executes completely. The reason for this situation is that the session file is locked when the process is running. You can however intentioanlly call session_write_close() method to unlock it. But this is not feasible in all scenarios, may be one need to write something to session at the end of the process.
Now if we only call connection_aborted() in a loop then it will always
return 0 whether the connection is closed or not.
0 means that the connection is not aborted. It is misleading. However, after re-search and experiments if have discovered that the output buffer in php is the reason.
First of all in order to check for aborts the developer in a loop must send some output to the client by echoing some text. For example:
print " ";
As the process is still running, the output will not be sent to the client. Now to send output we then need to flush the output buffer.
flush ();
ob_flush ();
And then if we check for aborts then it will give correct results.
if (connection_aborted () != 0) {
die();
}
Following is the working example, this will work even if you are using PHP session:
session_start ();
ignore_user_abort ( TRUE );
file_put_contents ( "con-status.txt", "Process started..\n\n" );
for($i = 1; $i <= 15; $i ++) {
print " ";
file_put_contents ( "con-status.txt", "Running process unit $i \n", FILE_APPEND );
sleep ( 1 );
// Send output to client
flush ();
ob_flush ();
// Check for connection abort
if (connection_aborted () != 0) {
file_put_contents ( "con-status.txt", "\nUser terminated the process", FILE_APPEND );
die ();
}
}
file_put_contents ( "con-status.txt", "\nAll units completed.", FILE_APPEND );
EDIT 07-APR-2017
If someone is using Fast-Cgi on Windows then he can actually terminate
the CGI thread from memory when the connection is aborted using following code:
if (connection_aborted () != 0) {
apache_child_terminate();
exit;
}

Check out PHP connection_aborted() function. While doing your processing, you can sometimes check for the aborted connection to gracefully cancel the progress, as one would do in an interactive threading model.

One way I've found which is the easiest way to handle this time-out issue is as such:
1: set a value on the server as 'processing'. Start an independent thread to do the processing.
2: The initial ajax call returns a success
3: The javascript on the page goes into 'waiting mode' which sends a new ajax request every 10 or 30 or 60 seconds or five or ten minutes or whatever (depending on your situation) to find out whether the value on the server is still set to 'processing'.
4: The independent thread completes. It sets the value on the server to 'done'.
5: The javascript on the page makes its next waiting-mode query, and returns 'done' and the appropriate data.
4b: If an obscene amount of time goes by without a 'done', it registers as a failure. How much time is obscene depends upon your situation. Send an ajax call updating the value from 'processing' to 'cancel'.
5b: The independent thread periodically checks the status to make sure it's still set to 'processing'. If it sees a mode-shift to 'cancel' it cancels itself.

This is what you're looking for:
http://php.net/manual/en/function.ignore-user-abort.php
Stopping a script in the middle of execution can lead to unexpected results, so caveat emptor

Related

How to get a connection abort event from client side in an aws-lambda function that runs php script?

I am successfully running php code on an aws-lambda function which is initialized by an aws api gateway and runs on nodejs run time.
I want to capture how much time a client spent on my page.
The time capture logic is working fine on my server but I can't get it to run on aws-lambda.
Please, can any one help me out? How can I get a client disconnect notification in my php code ?
The following code was used to get a connection abort in php :
ignore_user_abort( true );
while ( 1 )
{
ob_flush();
flush();
echo "1";
if ( connection_aborted() )
{
Log::info('connection aborted detected');
$end_time1 = microtime( true );
break;
}
else
{
$end_time1 = microtime( true );
$second = $end_time1 - $start_time;
if ( $second >= 25 )
{
break;
}
}
// Sleep
usleep( 300000 );
}
Assume that all php variables are set with appropriate values.
I just can't get the if condition if ( connection_aborted() ) to be true when the client closes the connection by either closing browser or any another way.
Or is there any way to know that the API gateway disconnected from client side?
Note: the above code is working fine in my local server as well as on the stage server, but it's not working on the aws-lambda function.
This is not something Lambda can do, because the condition is impossible to detect inside the Lambda runtime environment due to the decoupling built in to Lambda.
When a Lambda function is invoked, the client (browser) isn't actually connected to the container running the function. It's connected to the Lambda service which has used a separate connection to the container, to invoke the function.
If the client closes the connection, the function still runs to completion but the response is discarded by the service, since there is nobody to return it to.
flush() and ob_flush() are almost certainly no-ops, because Lambda is strictly request/response. The entire response is returned when execution is complete -- nothing is returned prior to that, so there is nothing to flush while the function is still executing.
If this is a really important capability, consider using API Gateway Web Sockets, which fire Lambda functions when connections are opened and again when closed. It's an unusual use case for that feature, but it would avoid the expense of long-running Lambda functions (which can't be used, anyway).

PHP: How to check if session_start will block or make it time out

In a certain instance I want to cancel calls of users that already have an open session.
I use session_start to make sure, a logged in user can only execute one request at a time and that works fine. But all subsequent calls will simply block indefinitely until all previous calls went through which is unsatisfying in certain circumstances like misbehaving users.
Normally all blocking calls I know have a timeout parameter you give with them. Is there something like this for start_session?
Or is there a call in the spirit of session_opened_by_other_script that I can do before calling session_start?
For now my solution is to check if there is already a lock on the session file using exec an shell scripting. I don't recommend anyone using it who does not fully understand it.
Basically it tries to get a lock on the session file for the specified timeout value using flock. If it fails to do so it exists with 408 Request timeout. (or 429 Too many requests, if available)
For this to work you need to...
know your session ID at that point in time
have file based sessions
Note, that this is not atomic. It still can happen that multiple requests end up waiting in session_start. But it should be a rare event. Most calls should be canceled correctly, which was my agenda.
class Session {
static public function openWhenClosed() {
if (session_status() == PHP_SESSION_NONE) {
$sessionId = session_id();
if ($sessionId == null)
$sessionId = $_COOKIE[session_name()];
if ($sessionId != null) {
$sessFile = session_save_path()."/sess_".$sessionId;
if (file_exists($sessFile)) {
$timeout = 30; //How long to try to get hold of the session
$fd = 9; //File descriptor to use to try locking the session file.
/*
* This 'trick' is not atomic!!
* After exec returned and session_start() is called there is a time window
* where it can happen that other waiting calls get a successful lock and also
* proceed and get then blocked by session_start(). The longer the sleep value
* the less likely this is to happen. But also the longer the extra delay
* for the call
*/
$sleep = "0.01"; //10ms
//Check if session file is already locked by trying to get a lock on it.
//If it is, try again for $timeout seconds every $sleep seconds
exec("
exec $fd>>$sessFile;
while [ \$SECONDS -lt $timeout ]; do
flock -n $fd;
if [ \$? -eq 0 ]; then exit 0; fi;
sleep $sleep;
done;
exit 1;
", $null, $timedOut);
if ($timedOut) {
http_response_code(408); //408: Request Timeout. Or even better 429 if your apache supports it
die("Request canceled because another request is still running");
}
}
}
session_start();
}
}
}
Additional thoughts:
It is tempting to use flock -w <timeout> but that way far more
waiting in line calls will manage to use the time between exec and
start_session to obtain a lock and end up blocking in
session_start
If you use the browser for testing this, be aware that most browsers do command queing and reuse a limited amount of connectiosn. So they do not start sending your request before others finish. This can lead to seemingly strange results if you are not aware of this. You can more reliably test using several parallel wget commands.
I do not recommend to activate this for normal browser request. As mentioned in 2) this is already handled by the browser anyway in most cases. I only use it to protect my API against rouge implementations that do not wait for an answer before sending the next request.
The performance hit was negligible in my tests for my overall load. But I would advice to test in your environment yourself using microtime() calls

ReactPHP Socket Server: What triggers the write (to client)?

When trying to write to the client, the message is getting buffered, and in some cases, it's not being written at all.
CURRENT STATUS:
When I telnet into the server, the Server Ready: message is readily printed as expected.
When I send random data (other than "close"), the server's terminal nicely shows progress every second, but the clients output waits until after all the sleeping, and then prints it all at once.
Most importantly, when sending "close", it just waits the obligatory second, and then closes without ANY writeout in the client.
GOAL:
My main goal is for a quick message to be written to the client prior closing a connection.
CODE:
// server.php
$loop = React\EventLoop\Factory::create();
$socket = new React\Socket\Server($loop);
$socket->on('connection', function ($conn)
{
$conn->write("Server ready:\n");
$conn->on('data', function ($data) use ($conn)
{
$data = trim($data);
if( $data == 'close')
{
$conn->write("Bye\n");
sleep(1);
$conn->close();
}
for ($i = 1; $i<5; $i++) {
$conn->write(". ");
echo '. ';
sleep(1);
}
$conn->write(".\n");
echo ".\n";
$conn->write("You said \"".$data."\"\n");
});
});
$socket->listen(1337, '127.0.0.1');
$loop->run();
SUMMARY:
Why can't I get anything written to the client before closing?
The problem your are encountering is because you are forgetting about the event loop that drives ReactPHP. I ran into this issue recently when building a server and after following the code around, I found 2 things out that should help you solve your problem.
If you close the connection after writing to it, it simply closes the connection before it can write. Solving this issue can help you fix the next issue... The correct call for writing something to the client, THEN closing the connection is called $conn->end('msg'); If you follow this chain of code the behaviour becomes clear; First it basically writes to the connection just as if you ran $conn->write('msg'); but then it registers a new event handler for the full-drain event, this handler simple calls $conn->close();; the full-drain event is only dispatched when the write buffer is completely emptied. So you can see that the use of end, simply waits to write before it closes the connection.
The drain and full-drain are only dispatched after writing to a stream. full-drain occurs when the write buffer is completely empty. drain is dispatched after the write buffer is emptied past its softLimit which by default is 2048 bytes.
The reason your writes are not making it through is because $conn->write('msg') only adds the string to the write buffer; it does not actually write. The event loop needs to run before it will be given time to write. Your use of sleep() is incorrect with react because this blocks the call at that line. In react you don't want to block any code from executing. If you are done a task, then let your function return, and code execution returns to the react main event loop. If you wish to have things run on certain intervals, or simply on the next iteration of the loop, you can schedule callbacks for the main event loop with the use of $loop->addTimer($seconds, $callback), $loop->addPeriodicTimer($seconds, $callback), $loop->nextTick($callback) or $loop->futureTick($callback)
Ultimately it is because you are programming without acknowledging that we are still in a blocking thread. Anything your code that blocks, blocks the entire react event loop, in turn blocking everything that react does for you. Give up processing time back to the loop to ensure it can do the reads/writes that you have queued up. You only need on iteration of the loop to occur for the write buffer to begin emptying (depending on the size of the buffer it may or may not write it all out)
If you're goal here is just to fix the connection closing bit, switch your call to $conn->end('msg') instead of the write -> close. However I believe that the other loop you have printing the dots also does not behave in the way that I think you expect/desire it to work. As it's purpose is not as clear, if you can tell me what your goal was for it I can possibly help you restructure that step as well.

Postgresql cancel query on client disconnect with php

I have a very expensive query which gets executed from php and it can take a while to execute. Is there a way, in php, to detect if a user disconnects prior to the query being done and cancel it?
A possible solution is to use pg_send_query(), that function sends a query to the database and returns immediatly without blocking. Then you can poll to see if the user disconnected before the query finished. See this:
ignore_user_abort(false);
$db = pg_connect(DATABASE_DSN);
pg_send_query($db, "SELECT pg_sleep(10000)"); // long running query
while(pg_connection_busy($db)) {
/* You need to output something to the browser so PHP can know when the
browser disconnected. The 0 character will be ignored.
*/
echo chr(0);
/* Need to do both flushes to make sure the chr is sent to the browser */
ob_flush();
flush();
usleep(100000); // 0.1s to avoid starving the CPU
if(connection_status() != CONNECTION_NORMAL || connection_aborted()) {
// Browser disconnected, cleanup an die
pg_cancel_query($db);
pg_query($db, "ROLLBACK");
pg_close($db);
die();
}
}
// At this point the query finished and you can continue fetching the rows
This approach works but has a big problem: you really need to send something to the browser to detect the browser disconnection. If you don't, connection_status() and connection_aborted() will not work. This seems to be an old PHP bug, see here: https://bugs.php.net/bug.php?id=30301
So this method doesn't work when, for example, you query Postgres in the middle of a PDF generation routine. In that case the needed chr(0) will break the generated binary file.
You would want to use connection_aborted to detect if the user has disconnected it returns 1 if the client has disconnected otherwise it returns 0. There is some documentation here, however its usage is self documenting and you should have no problem using it.

PHP script doesn't exit on browser exit

Why this dummy script keeps running event if the client closes the browser (so the connection to the server)?
while ( true )
{
sleep( 1 );
file_put_contents( '/tmp/foo' , "I'm alive ".getmypid()."\n" , FILE_APPEND );
}
It's unexpected to me according to this. Also this example doesn't seem to work.
And set_time_limit with a non-zero parameter simply does nothing.
I'd like some clarifications.
If you try to write some output to the browser in that loop, you should find the script aborts if the connection has been terminated. This behaviour is hinted at in the documentation for ignore_user_abort
When running PHP as a command line script, and the script's tty goes
away without the script being terminated then the script will die the
next time it tries to write anything, unless value is set to TRUE
I tried a few experiments myself, and found that even if you do attempt some browser output, the script will keep running if the output buffer isn't full yet. If you turn output buffering off, the script will abort when output is attempted. This makes sense - the SAPI layer should notice the request has been terminated when it tries to transmit the output.
Here's an example...
//ensure we're not ignoring aborts..
ignore_user_abort(false);
//find out how big the output buffer is
$buffersize=max(1, ini_get('output_buffering'));
while (true)
{
sleep( 1 );
//ensure we fill the output buffer - if the user has aborted, then the script
//will get aborted here
echo str_repeat('*', $buffersize)."\n";
file_put_contents( '/tmp/foo' , "I'm alive ".getmypid()."\n" , FILE_APPEND );
}
That demonstrates what triggers the abort. If you had a script which was prone to enter an endless loop with no output, you could use connection_aborted() to test whether the connection is still open.
set_time_limit only restricts the time spent in php code. Most of the time(I'd guess >99.9%) in your program is spent in system calls, writing data to a file and sleeping.
ignore_user_abort only aborts when you're writing something to the client (not in a local file) - there's simply no way you can distinguish an unused and a terminated connection otherwise in TCP, unless the client excplicitely terminates the connection with an RST packet.

Categories