Why Does This PHP Hang The Page? - php

On a page I have buttons to launch and cancel tasks to be performed in PHP on the server side. While these PHP tasks are running the user cannot navigate away from the page, it just hangs up until the job is done. I'm trying to find what's causing the synchronous behavior.
Jquery:
$(document).ready(function()
{
var id = 0;
$('#tool-execute-btn').click(function()
{
// Pull data from page
var last_response_len = false;
$.ajax(
{
xhrFields:
{
onprogress: function(e)
{
var this_response, response = e.currentTarget.response;
if(last_response_len === false)
{
this_response = response;
last_response_len = response.length;
}
else
{
this_response = response.substring(last_response_len);
last_response_len = response.length;
}
// Get the data from the stream (backend php)
$('#tool-output-area').append(this_response + '<br>');
}
},
method: 'POST',
url: 'run-tool.php', // See block below
data:
{
// Data from page
}
}) // ... Continue
The backend processing PHP page:
/* Setup the page so I can stream the content
* line by line back to the user */
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
/* Set Maximum Execution Time By Plan */
ini_set('max_execution_time', getMaxExeTime());
ini_set('output_buffering', 'off');
ini_set('zlib.output_compression', false);
ini_set('implicit_flush', true);
ob_implicit_flush(true);
while (ob_get_level() > 0)
{
$level = ob_get_level();
ob_end_clean();
if (ob_get_level() == $level) break;
}
if (function_exists('apache_setenv'))
{
apache_setenv('no-gzip', '1');
apache_setenv('dont-vary', '1');
}
/* This section runs a ping and echos out the terminal window */
$descriptorspec =
[
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w']
];
// Input checking for the post data and interaction with a MySQL database is done here
$tool_command = 'ping 8.8.8.8 -n 10';
$process = proc_open($tool_command, $descriptorspec, $pipes);
stream_set_blocking($pipes[1], false);
stream_set_blocking($pipes[2], false);
$proc_details = proc_get_status($process);
while ($s = fgets($pipes[1]))
{
echo $s;
flush();
sleep(0.3);
}
If at any time during this while loop the user attempts to reload the page, they will just get the tab loading icon. If I attempt to run another PHP script from jQuery, it'll just queue until the while loop is done. What is causing the problem here? PHP, Ajax, the apache server, HTTP requests, something else?

When you use session variables in a script, only one script invocation can access the same session at a time, and PHP blocks any other scripts from the same client.
Use session_write_close() in the script when it's done accessing the session variables, which will allow another script to run. If it needs to access session variables again, it needs to call session_start() again.

Related

Run PHP code after ending the HTTP request

I'm writing a simple code to simply show to clients, data that is actually loaded from another HTTP server. The problem is that loading it from the remote server can take up to multiple seconds, and I don't want that much page load delay. So, I make my server cache a copy of this data. So that whenever a client sends a request to my server, it sends the ready-loaded copy and then loads a new copy from the remote server to update the local copy in case any changes were made.
So here's my pseudo code:
if(file_exists($cache_path)){
echo file_get_contents($cache_path);
// I need to end the HTTP request and close the connection here while continuing with the code.
$uptodate_content = file_get_contents("https://docs.google.com/document/export?format=pdf&id=$id");
// I don't want the user to wait for nothing, until this line.
}
else {
$uptodate_content = file_get_contents("https://someremotehost.com/someresource");
echo $uptodate_content;
}
echo file_put_contents($cache_path, $uptodate_content);
Hi I think the best solution is using a queue For example if you use the the queue, you can send it to the queue and then your consumer can pick it from the queue when it has time and user do not need to wait for it
This link is helpful
And this link will help you to use redis for this problem
This is a bad practice.
The connection can never end and you should be careful with such code
The better method is to run a cron job/queue every houerget data from remote server, or alternatively the remote server will trigger a trigger when updating data.
<?php
ob_end_clean();
header("Connection: close");
ignore_user_abort();
ob_start();
//your code
//your code
//your code
echo "response foo bar";
$obSize = ob_get_length();
header("Content-Length: $obSize");
ob_end_flush();
flush();
session_write_close();
// Do processing here
request_to_remote_server();
One way of doing it:
First, create a new PHP file, let's call it update.php, and write the following:
if (isset($argv[1])) {
storeDocumentToCache($argv[1]);
}
And in your current file, change the code to:
echo readDocumentFromCache($id) ?? storeDocumentToCache($id);
In old PHP versions (<7) it should be:
$content = readDocumentFromCache($id);
echo isset($content) ? $content : storeDocumentToCache($id);
Then require the following helper functions in both files (and set $cache_path):
function readDocumentFromCache($id, $fetch = true)
{
$cache_path = "?";
if (file_exists($cache_path)) {
return file_get_contents($cache_path);
}
if ($fetch) {
execInBackground("php " . __DIR__ . "/update.php $id");
}
return null;
}
funciton storeDocumentToCache($id)
{
$cache_path = "?";
$uptodate_content = file_get_contents("https://docs.google.com/document/export?format=pdf&id=$id");
file_put_contents($cache_path, $uptodate_content);
return $uptodate_content;
}
function execInBackground($cmd)
{
if (substr(php_uname(), 0, 7) == "Windows") {
pclose(popen("start /B " . $cmd, "r"));
} else {
exec($cmd . ' > /dev/null 2>/dev/null &');
}
}

How to increase Server-Sent Event's reopen time?

I'm working to show notifications from Server-Sent Event. I checked that every time the browser tries to reconnect with the source about 3 seconds after each connection is closed. That event is getting a call too fast, so my server is loaded too.
So how do I change the reopening time to increase? I have to do at least 60 seconds, so tell me how to do it?
I'm trying the following code.
<table class="table" id="notification"></table>
<script type="text/javascript">
var ssevent = null;
if (!!window.EventSource) {
ssevent = new EventSource('ssevent.php');
ssevent.addEventListener('message', function(e){
if(e.data){
json = JSON.parse(e.data);
if(json){
json.forEach(function(v,i){
html = "<tr><td>"+ v.text +"</td><td>"+ v.date +"</td></tr>";
});
$('#notification').append(html);
}
}
}, false);
ssevent.addEventListener('error', function(e) {
if (e.readyState == EventSource.CLOSED){
console.log("Connection was closed.");
}
}, false);
} else {
console.log('Server-Sent Events not support in your browser');
}
</script>
The file of event stream is as follow.
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
include_once "config.php";
$time = isset($_SESSION['last_event_time']) ? $_SESSION['last_event_time'] : time();
$result = $db->quesry("SELECT * FROM event_noti WHERE event_time < {$time} ")->rows;
$_SESSION['last_event_time'] = time();
if($result){
$json = array();
foreach ($result as $row){
$json[] = array(
'text' => $row['event_text'],
'date' => date("Y-m-d H:i", $row['event_time']),
);
}
echo "data: ". json_encode($json) . "\n\n";
flush();
}
Fundamentally you cannot control this: it is a browser-specific setting.
If your browser is Firefox it appears to be controlled by this setting: dom.server-events.default-reconnection-time with a default of 5000ms.
Taking a step back: the reconnect only happens if the server closes the connection. Why are you closing the connection? (*) Why is a 3-second re-connection too fast?
The point of SSE is to minimize latency; the trade-off is more resource usage, in particular having to keep a dedicated socket open for each client.
So it sounds like you don't want to be using SSE, and instead want to use a simple AJAX poll, on a 60-second setInterval() call?
*: If you did intend to keep it open, you need to wrap your query and processing the result code in a while(true){...} loop. Put e.g. a one-second sleep at the end of the while loop to stop the DB server being overloaded.
Now I have my answer.
Controlling Reconnection-Timeout:
The browser attempts to reconnect the connection to the source within about 3 seconds after each server-sent event connection is closed. Before trying to reconnect, you can change that timeout by starting the line with retry: and then adding the millisecond number to wait.
I changed the code below and started working as I wanted.
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
include_once "config.php";
$time = isset($_SESSION['last_event_time']) ? $_SESSION['last_event_time'] : time();
$result = $db->quesry("SELECT * FROM event_noti WHERE event_time < {$time} ")->rows;
$_SESSION['last_event_time'] = time();
echo "retry: 60000\n"; // 60 seconds, to wait for next connection.
$json = array();
if($result){
foreach ($result as $row){
$json[] = array(
'text' => $row['event_text'],
'date' => date("Y-m-d H:i", $row['event_time']),
);
}
}
echo "data: ". json_encode($json) . "\n\n";
flush();
Source from

Data getting lost in server-sent event with PHP handler

I'm working on a one-way messaging system using server-sent events. I have a file (server.html) which sends the contents of a textarea to a PHP file (handler.php).
function sendSubtitle(val) {
var xhr = new XMLHttpRequest();
var url = "handler.php";
var postdata = "s=" + val;
xhr.open('POST', url, true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send(postdata);
//alert(val);
}
This works (alert(val) displays the text in the textarea).
My handler.php code looks like this:
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
$stringData = $_POST['s'];
echo "data: Data is {$stringData}\n\n";
flush();
And the relevant part of my SSE receiver file (client.html) is as follows:
if(typeof(EventSource) !== "undefined") {
var source = new EventSource("handler.php");
source.onmessage = function(event) {
var textarea = document.getElementById('subtitles');
textarea.value += event.data + "<br>";
textarea.scrollTop = textarea.scrollHeight;
};
} else {
document.getElementById("subtitles").value = "Server-sent events not supported.";
}
The problem is that client.html only displays "data: Data is", so the text from server.html is getting lost somewhere along the way. I imagine it's the PHP code that's falling over, but I can't work out what's wrong. If anyone can help, I'd appreciate it.
EDIT
I chose to use SSE as opposed to websockets as I only need one-way communication: server.html should push the contents of its textarea to client.html whenever it changes. All the examples of SSE that I've looked at (and I've looked at a lot!) send "automatic" time-based data. I haven't seen any that use real-time user input. So perhaps I should clarify my original question and ask, "How can I use SSE to update a DIV (or whatever) in web page B whenever the user types in a textarea in web page A?"
UPDATE
I've narrowed the issue down to the while loop in the PHP file and have therefore asked a new question: Server-side PHP event page not loading when using while loop
Assuming you want to send a value from server.html and a value at client.html will be automatically updated...
You will need to store the new value somewhere because multiple instances of a script do not share variables just like that. This new value can be stored in a file, database or as a session variable, etc.
Steps:
Send new value to phpScript1 with clientScript1.
Store new value with phpScript1.
Connect clientScript2 to phpScript2.
Send stored value to clientScript2 if it is changed.
Getting the new value 'on the fly' means phpScript2 must loop execution and send a message to clientScript2 whenever the value has been changed by clientScript1.
Of course there are more and different approaches to achieve the same results.
Below there's some code from a scratchpad I've used in previous project.
Most parts come from a class (which is in development) so I had to adopt quite a lot of code. Also I've tried to fit it into your existing code.
Hopefully I didn't introduce any errors.
Do note I did not take any validation of your value into account! Also the code isn't debugged or optimized, so it's not ready for production.
Client side (send new value, e.g. your code):
function sendSubtitle(val) {
var xhr = new XMLHttpRequest();
var url = "handler.php";
var postdata = "s=" + val;
xhr.open('POST', url, true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send(postdata);
//alert(val);
}
Server side (store new value):
<?php
session_start();
$_SESSION['s'] = $_POST['s'];
Client side (get new value):
//Check for SSE support at client side.
if (!!window.EventSource) {
var es = new EventSource("SSE_server.php");
} else {
console.log("SSE is not supported by your client");
//You could fallback on XHR requests.
}
//Define eventhandler for opening connection.
es.addEventListener('open', function(e) {
console.log("Connection opened!");
}, false);
//Define evenhandler for failing SSE request.
es.addEventListener('error', function(event) {
/*
* readyState defines the connection status:
* 0 = CONNECTING: Connecting
* 1 = OPEN: Open
* 2 = CLOSED: Closed
*/
if (es.readyState == EventSource.CLOSED) {
// Connection was closed.
} else {
es.close(); //Close to prevent a reconnection.
console.log("EventSource failed.");
}
});
//Define evenhandler for any response recieved.
es.addEventListener('message', function(event) {
console.log('Response recieved: ' + event.data);
}, false);
// Or define a listener for named event: event1
es.addEventListener('event1', function(event) {
var response = JSON.parse(event.data);
var textarea = document.getElementById("subtitles");
textarea.value += response + "<br>";
textarea.scrollTop = textarea.scrollHeight;
});
Server side (send new value):
<?php
$id = 0;
$event = 'event1';
$oldValue = null;
session_start();
//Validate the clients request headers.
if (headers_sent($file, $line)) {
header("HTTP/1.1 400 Bad Request");
exit('Headers already sent in %s at line %d, cannot send data to client correctly.');
}
if (isset($_SERVER['HTTP_ACCEPT']) && $_SERVER['HTTP_ACCEPT'] != 'text/event-stream') {
header("HTTP/1.1 400 Bad Request");
exit('The client does not accept the correct response format.');
}
//Disable time limit
#set_time_limit(0);
//Initialize the output buffer
if(function_exists('apache_setenv')){
#apache_setenv('no-gzip', 1);
}
#ini_set('zlib.output_compression', 0);
#ini_set('implicit_flush', 1);
while (ob_get_level() != 0) {
ob_end_flush();
}
ob_implicit_flush(1);
ob_start();
//Send the proper headers
header('Content-Type: text/event-stream; charset=UTF-8');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no'); // Disables FastCGI Buffering on Nginx
//Record start time
$start = time();
//Keep the script running
while(true){
if((time() - $start) % 300 == 0){
//Send a random message every 300ms to keep the connection alive.
echo ': ' . sha1( mt_rand() ) . "\n\n";
}
//If a new value hasn't been sent yet, set it to default.
session_start();
if (!array_key_exists('s', $_SESSION)) {
$_SESSION['s'] = null;
}
//Check if value has been changed.
if ($oldValue !== $_SESSION['s']) {
//Value is changed
$oldValue = $_SESSION['s'];
echo 'id: ' . $id++ . PHP_EOL; //Id of message
echo 'event: ' . $event . PHP_EOL; //Event Name to trigger the client side eventhandler
echo 'retry: 5000' . PHP_EOL; //Define custom reconnection time. (Default to 3000ms when not specified)
echo 'data: ' . json_encode($_SESSION['s']) . PHP_EOL; //Data to send to client side eventhandler
//Note: When sending html, you might need to encode with flags: JSON_HEX_QUOT | JSON_HEX_TAG
echo PHP_EOL;
//Send Data in the output buffer buffer to client.
#ob_flush();
#flush();
}
//Close session to release the lock
session_write_close();
if ( connection_aborted() ) {
//Connection is aborted at client side.
break;
}
if((time() - $start) > 600) {
//break if the time exceeds the limit of 600ms.
//Client will retry to open the connection and start this script again.
//The limit should be larger than the time needed by the script for a single loop.
break;
}
//Sleep for reducing processor load.
usleep(500000);
}
You called handler.php first time in the server.html and again in client.html. Both are different processes. The variable state won't be retained in the web server. You need to store it somewhere if you want that value in another PHP process. May be you can use sessions or database.
While using sessions you can store the values in two files like:
<?php
//server.php
session_start();
$_SESSION['s'] = $_POST['s'];
And in client.php
<?php
//client.php
session_start();
echo "data: Data is ".$_SESSION['s']."\n\n";

PHP and events - flush to client does not happen until the script dies

I am trying to create a simple page that sends events to a web page but I cannot get the PHP to send the output before the page terminates.
I am using PHP-FPM and php5.6.27.
this is my simple HTML page:
<html>
<head>
<title>test events</title>
</head>
<body>
testing events
<ul id="pingEventList" style="float: left"></ul>
<ul id="messageEventList" style="float: left"></ul>
<script>
var evtSource=new EventSource("./s_events.php?auth=e3b164ef33d802c45da829b8f1240d16");
var pingEventList=document.getElementById('pingEventList');
var messageEventList=document.getElementById('messageEventList');
evtSource.onmessage=function (e){
var newElement=document.createElement("li");
newElement.innerHTML="message: "+e.data;
messageEventList.appendChild(newElement);
};
evtSource.addEventListener("initial", function (e){
var newElement=document.createElement("li");
// var obj=JSON.parse(e.data);
newElement.innerHTML="initial info "+e.data;
console.log("initial info "+e.data);
pingEventList.appendChild(newElement);
}, false);
evtSource.addEventListener("modified", function (e){
var newElement=document.createElement("li");
// var obj=JSON.parse(e.data);
newElement.innerHTML="modified info "+e.data;
console.log("modified info "+e.data);
pingEventList.appendChild(newElement);
}, false);
evtSource.onerror=function (e){
console.log("EventSource failed.", e);
// while(pingEventList.firstChild){
// pingEventList.removeChild(pingEventList.firstChild);
// }
// alert("EventSource failed.");
};
// evtSource.close();
</script>
</body>
</html>
and this is my PHP page:
<?php
#set_time_limit(0); // Disable time limit
// Prevent buffering
if(function_exists('apache_setenv')){
#apache_setenv('no-gzip', 1);
}
#ini_set('zlib.output_compression', 0);
#ini_set('implicit_flush', 1);
while(ob_get_level() !=0){
ob_end_flush();
}
ob_implicit_flush(1);
ignore_user_abort(false);
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Credentials: true');
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache'); // recommended to prevent caching of event data.
header('X-Accel-Buffering: off'); // Disables FastCGI Buffering on Nginx
$sleep_time = 1; //0.5 // seconds to sleep after the data has been sent
$exec_limit_time = 15; //600; // the time limit of the script in seconds
$keep_alive_time = 30; //300; // The interval of sending a signal to keep the connection alive
$client_reconnect = 1; // the time client to reconnect after connection has lost in seconds
$keep_alive_start = time();
$exec_limit_start = time();
$ts_last_used = intval(isset($_SERVER["HTTP_LAST_EVENT_ID"])?$_SERVER["HTTP_LAST_EVENT_ID"]:0);
echo 'retry:'.($client_reconnect*1000)."\n";
$event = [
'id' => $ts_last_used++,
'event' => 'initial',
'data' => "[\ndata:".'INITIAL DATA'."\ndata:]",
];
_send_event_data($event);
while(1){
// Did the connection fail?
if(connection_status() !=CONNECTION_NORMAL){
break;
}else{
$event = [
'id' => $ts_last_used++,
'event' => 'modified',
'data' => '{"id":"stuff here too"}',
];
_send_event_data($event);
}
// Sleep for x seconds
usleep($sleep_time*1000000);
if($exec_limit_start+$exec_limit_time<time()){
break;
}
if($keep_alive_start+$keep_alive_time<time()){
$keep_alive_start = time();
echo ': '.sha1(mt_rand())."\n\n";
}
}
// If this is reached, then the 'break'
// was triggered from inside the while loop
//
// So here we can log, or perform any other tasks
// we need without actually being dependent on the
// browser.
function _send_event_data($info){
global $keep_alive_start;
if(isset($info['id'])){
echo 'id:'.$info['id']."\n";
}
if(isset($info['event'])){
echo 'event:'.$info['event']."\n";
}
echo 'data:'.$info['data']."\n";
if(!isset($info['more_to_come'])||!$info['more_to_come']){
echo "\n";
}
$keep_alive_start = time();
#ob_end_flush();
flush();
}
I have tried various possible solutions, but I cannot figure it out. I have also changed PHP-FPM to mod_php (I am using apache) just in case it was PHP_FPM, but I didn't have any success.
I am open to suggestions
I figured it out... it was apache that was holding the output, this is my new configuration for fast-cgi usign php-fpm:
<IfModule fastcgi_module>
#php-fpm
AddHandler php5-fcgi .php .html
Action php5-fcgi /php5-fcgi
Alias /php5-fcgi /usr/local/www/cgi-bin/php5-fcgi
FastCgiExternalServer /usr/local/www/cgi-bin/php5-fcgi -flush -socket /var/run/php-fpm.sock -pass-header Authorization -idle-timeout 600
<Directory /usr/local/www/cgi-bin>
Require all granted
</Directory>
</IfModule>
What made the difference was the -flush added to FastCgiExternalServer, now the output is not buffered anymore and it output right away (I was able to find the answer thanks to this page: http://www.jeffgeerling.com/blog/2016/streaming-php-disabling-output-buffering-php-apache-nginx-and-varnish )

Check for database change with SSE

Trying to create a small notification system. When user fills out the profile then his verification status is set to 1 in database and then I would like to show a notification once that "hey you are now verified". Been searching a lot on the internet, but nothing has helped me to reach my goal. If the status is 1 in database I get the Event: verification_ok in the test.php but if it is 0 I get Maximum execution time of 120 seconds exceeded. Also I don't see any response in my client side code.
This is the server side code (test.php).
<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header("Connection: Keep-alive");
require_once $_SERVER['DOCUMENT_ROOT'].'/PHP/scripts/no_session_redirect.php';
$key = true;
$ver = $user_home->runQuery("SELECT verification_status FROM verification WHERE user_id=:user_id");
$ver->execute(array(":user_id"=>$user_id));
$status = $ver->fetch(PDO::FETCH_ASSOC);
while($key){
if($status["verification_status"] == 1){
pushNotification($status["verification_status"]);
$key = false;
}else{
$status["verification_status"];
sleep(10);
}
}
function pushNotification() {
echo "Event: verification_ok\n";
}
And here is the client side code:
$(document).ready(function() {
if (typeof(EventSource) !== "undefined") {
// Yes! Server-sent events support!
var source = new EventSource("test.php");
source.addEventListener("verification_ok", function(e) {
console.log(e.data);
}, false);
source.addEventListener("open", function(e) {
}, false);
source.addEventListener("error", function(e) {
if (e.readyState == EventSource.CLOSED) {
console.log("Error - connection was lost.");
}
}, false);
} else {
// Sorry! No server-sent events support..
}
});

Categories