PHP - проблема отката при обрыве соединения

shestero

Новичок
Задача весьма тривиальна:

Через Apache/PHP пользователем производится некий довольно долгий (нормальное время 1-2 минуты, но может и до 5 минут) запрос данных, в данном случае к СУБД MySQL.

Проблема: если пользователь передумал ждать и нажал на кнопку «отмена» либо соединение прекратилось по какой-то другой причине хочется прервать работу скрипта PHP и запроса в MySQL, что бы они бесполезно не тормозили сервер.

Что я узнал:

PHP не умеет определять обрыв соединения с браузером, если ничего не посылать.

Что я перепробовал:

1.a) Переодическая посылка байт в браузер из отдельного потока:
PHP:
class Ping0 extends Thread {
public function run() {
echo("<!-- 0 -->\n"); // or: echo(0);
//flush();
sleep(1);
}
}

function db_query_long($qstring,$conn)
{
ignore_user_abort(false);

$ping0 = new Ping0();
$ping0->start();
$ret = db_query($qstring,$conn);

// $ping0->stop();

return $ret;
}
Результат: при раскомментировании flush() скрипт не работает; в браузере ошибка:

Баг? Фича?

1.b) Посылка тестовых байт в основном потоке, запрос — во втором:
PHP:
class T extends Thread {

protected $arg;

protected $conn;

protected $done = false;

protected $res = null;



public function __construct($arg,$conn) {

$this->arg = $arg;

$this->conn = $conn;

}


public function isCompleated() {

return $this->done;

}


public function getResult() {

return $this->res;

}


public function run() {

$this->res = db_query($this->arg,$this->conn);

$this->done = true;

}

}


function db_query_long($qstring,$conn)

{

$tout = 300; // timeout, sec ***


ignore_user_abort(false);


$thread = new T($qstring,$conn);

$thread->start();


for ($i=1; $i<=$tout; $i++)

{

sleep(1);


if ($thread->isCompleated())

{

return $thread->getResult();

}


echo(0);

flush();

}


return false; // timeout

}
То же какая-то ошибка... :-(

2) Думал использовать mysql_unbuffered_query, но не нашёл, как определять, выполнился ли запрос или нет, не вызвав блокирующее чтение.

3) Переписал на MySQLi, сделал асинхронный запрос со сканированием готовности результата в цикле:
PHP:
function shutdown_mysqli()
{

global $gl_mysqli1;

if ($gl_mysqli1)

{

$thread_id = $gl_mysqli1->thread_id;

if ($thread_id)

{

$gl_mysqli1->kill($thread_id);

}


$gl_mysqli1->close();

//mysqli_close();

$gl_mysqli1 = null;

}

}

register_shutdown_function('shutdown_mysqli');


function db_query_long($qstring,$conn)

{

global $strLastSQL,$dDebug;

global $gl_mysqli1;


if (false) // ($gl_mysqli1==null)

{

return db_query($qstring,$conn); // using old mysql interface

}


$tout = 300; // timeout 300 sec = 5 min


if ($dDebug===true)

echo $qstring."<br>";

$strLastSQL=$qstring;


$r = $gl_mysqli1->query($qstring, MYSQLI_ASYNC ); // MYSQLI_USE_RESULT );

// $thread_id = $gl_mysqli1->thread_id; // checked: has right value


ignore_user_abort(false);


for ($i=0; $i<$tout*2; $i++)

{

$ready = $error = $reject = array($gl_mysqli1);

// $ready[] = $error[] = $reject[] = $gl_mysqli1;


mysqli_poll( $ready,$error,$reject, 0,500000); // wait 1/2 sec

if (count($ready)>0)

{

// ready

$r = $gl_mysqli1->reap_async_query();

if ($r)

{

// normal exit

return $r;

}

// some error

return $r;

}

if ( count($error)>0 || count($reject)>0 )

{

trigger_error("(" . $gl_mysqli1->connect_errno . ") "

. $gl_mysqli1->connect_error, E_USER_ERROR);


// error

return null;

}


// test connection

echo("\n"); // was: (0);

flush();

ob_flush();


if (connection_status()!=CONNECTION_NORMAL)

{

shutdown_mysqli();

return null;

}

}


// time out

return null;

}
(Посылку нуля пришлось заменить на «\n» так как похоже нули портят формат JSON, в котором транспортируются данные в браузер).

Запрос работает, но обрыва соединения не чувствует! Возможно что-то где-то ещё кешируется? Что 10k переводов строки каждую секунду посылать??


А есть ли решения поэлегантней?
 
Последнее редактирование модератором:

shestero

Новичок
Задачу сделал.
Одного байта посылать для проверки соединения не достаточно — информация походит через каскад буферов, в том числе через gzip-паковщих. Эксперимент показал, что в моём случае достаточно 32 байт.
Вот код:
PHP:
// global variables
$gl_tout     = 240;   // timeout 240 sec = 4 min
$gl_longsql     = "";

function shutdown_mysqli($cause, $tout, $sql = "")
{
    global $gl_mysqli1;
    if ($gl_mysqli1)
    {
      $thread_id = $gl_mysqli1->thread_id;
      if ($thread_id)
      {
    $gl_mysqli1->kill($thread_id);
    // Note from http://www.php.net/manual/en/mysqli.kill.php :
    // Be careful using this before mysqli::close.
    // Killing the thread before actually closing the connection will leave the connection open!
    // And depending on your max_connections and max_user_connections (by default the same),
    // this could result in a "Max connections reached for **** user" message.
      }

      $gl_mysqli1->close(); // or mysqli_close();
      $gl_mysqli1 = null;

      // it look's like $gl_mysqli1->kill($thread_id); and closing MySQLi connection above
      // doesn't actually KILLs the request sometimes. Why?
      if ($thread_id)
      {
    mysql_query("KILL $thread_id"); // KILLing using $conn - the old default connection
      }

      // just log event into special table
      log_long_query($cause, $tout, $thread_id, $sql );
    }
}
register_shutdown_function("shutdown_mysqli", "shutdown", $gl_toutm, $gl_longsql);

function db_query_long($qstring,$conn)
{
    global $strLastSQL,$dDebug;
    global $gl_mysqli1;
    global $gl_tout;
    global $gl_longsql; $gl_longsql = $qstring;

    if (false) // ($gl_mysqli1==null)
    {
      return db_query($qstring,$conn); // using old mysql interface
    }

    if ($dDebug===true)
       echo $qstring."<br>";
    $strLastSQL=$qstring;

    $r = $gl_mysqli1->query($qstring, MYSQLI_ASYNC ); // MYSQLI_USE_RESULT );
    // $thread_id = $gl_mysqli1->thread_id; // checked: has right value

    ob_implicit_flush(true);
    ignore_user_abort(false);

    for ($i=0; $i<$gl_tout; $i++)
    {
      $ready = $error = $reject = array($gl_mysqli1);
      // $ready[] = $error[] = $reject[] = $gl_mysqli1;

      mysqli_poll( $ready,$error,$reject, 0, 1000000); // wait 1 sec
      if (count($ready)>0)
      {
     // ready
     $r = $gl_mysqli1->reap_async_query();
     if ($r)
     {
       // normal exit
       $gl_longsql = ""; // no log needed
       return $r;
     }
     // some error ??
     return $r;
      }
      if ( count($error)>0 || count($reject)>0 )
      {
     // error
     trigger_error("(" . $gl_mysqli1->connect_errno . ") "
        . $gl_mysqli1->connect_error, E_USER_ERROR);

     shutdown_mysqli("error", $gl_tout, $qstring);
     return null;
      }

      // test connection
      echo str_repeat("\n",32); // was: (0);
      flush();
      ob_flush();

      if (connection_status()!=CONNECTION_NORMAL)
      {
    shutdown_mysqli("disconnect", $gl_tout, $qstring);
        return null;
      }
      // normal stage, but results not ready yet
      // log_long_query("test",0,$gl_mysqli1->thread_id,$qstring);
    }

    // time over
    shutdown_mysqli("time out", $gl_tout, $qstring);
    return null;
}
Для непонятливых - соединения старым и новым интерфейсом с MySQL используются параллельно.
Вроде всё работает как надо, но осталось много вопросов, основной: неужели нельзя попроще?
Например, а почему нельзя сделать способом №1, используя Thread? Не баг ли там? Кстати, в первом моём посте забыл вставить ошибку (вид из браузера): Error 6 (net::ERR_FILE_NOT_FOUND): The file or directory could not be found.
Напомню, она возникает если вызвать функию flush в фоновом потоке. Это что так и должно быть? :-O Только что словил ту же ошибку с тем же PHP 5.4.23 на мирной функции readfile, выдающий бинарный файл пользователю (файл разумеется на месте). Выяснилось что это происходило из-за большого размера и при причина соответственно в переполнении какого-то буфера, излечилось if (ob_get_level()) ob_end_clean(); непосредственно перед readfile. Вот такие чудеса.
 

dimitrius

Новичок
Хочу тоже вставить своих пять копеек.
Возможно лучше обрабатывать и передавать данные частями? Или данные представляют собой единое целое?
 

Dovg

Продвинутый новичок
Какой же ад.
shestero, Хочешь быстрого отклика - ставь в очередь и показывай пользователю, что задача в обработке. Все остальное, применительно к этой задаче - костыли. Твое решение - это сложный, неподдерживаемый костыль.
 

dimitrius

Новичок
Думаю стоит сделать слушателя и переменную состояния. А дальше через определенное время изменять переменную состояния через ajax. И соответственно проверять ее состояние и если был обрыв, то состояние не обновляется и соответственно выходим из скрипта.
 
Сверху