PHP: Отмена запроса в MySQL по обрыву соединения с браузером
От: shestero  
Дата: 19.12.13 21:20
Оценка:
Проблема: Если во время выполнения долгого запроса через PHP к MySQL соединения с браузером обрывается (таймаут, сброс со стороны пользователя, аварийный обрыв связи итп), запрос продолжает выполнятся в СУБД, бесполезно потребляя ресурсы.

Предполагаемое решение:
Как я прочитал, PHP не умеет определять состояние соединения с браузером, когда ничего туда не посылается. Предполагаю посылать нулевые байты.
(Кстати, не повредит ли это формату JSON?)
Итак, мне нужно отправить SQL-запрос в СУБД в фоновом режиме и переодически (например раз в секунду) посылая эти нули в браузер(?), делая flush() и проверяя connection_status() как-то сканировать состояние-ход выполнения SQL запроса в MySQL. Если состояние запроса — «готов» — выходить из цикла и приступить к передаче результатов из СУБД в браузер. Если connection_status не 0, прибить запрос в MySQL и завершить скрипт.
Попробовал это сделать с помощью mysql_unbuffered_query и mysql_thread_id — не вышло — похоже mysql_unbuffered_query блокирует работу до получения первой строки результата ( а запросы бывают SELECT COUNT(*) ). К тому же не нашёл удобный способ проверять состояние запорса по id.

Итак — остаётся только mysql::query с MYSQLI_ASYNC — mysqli_poll ?? Есть предложения по-проще?
Вариант решения — А может можно использовать register_tick_function или thread, посылающий нули только во время выполнения mysql_query, выключив ignore_user_abort (что бы скрипт сумел завершится аварийно с надеждой, что соединение-запрос MySQL прибъётся при этом автоматически) ?

Есть ли какие-то мысли, комментарии?
php ассинхронно
Re: Странная ошибка
От: shestero  
Дата: 20.12.13 05:11
Оценка:
Приступил к реализации проверки сборса соединения через поток.
(Ожидаемое поведение: при разрыве соединения с браузером 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() приводит к ошибке в браузере:

This webpage is not found
No webpage was found for the web address: http://client.d-inform.com/gtk1/trans_list.php
Error 6 (net::ERR_FILE_NOT_FOUND): The file or directory could not be found.

Что это значит??
Баг PHP?

PS Пришлось перейти на PHP 5.4.23, т.к 5.3 не компилился:

Bug #65685 PHP 5.3 git fails to compile with ZTS
https://bugs.php.net/bug.php?id=65685
http://grokbase.com/t/php/php-bugs/139g3x2xgw/php-bug-bug-65685-new-php-5-3-git-fails-to-compile-with-zts
php thread flush
Re: PHP: Отмена запроса в MySQL по обрыву соединения с браузером
От: shestero  
Дата: 24.12.13 04:55
Оценка:
Задачу сделал.
Одного байта посылать для проверки соединения не достаточно — информация походит через каскад буферов, в том числе через gzip-паковщих. Эксперимент показал, что в моём случае достаточно 32 байт.
Вот код:
// 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://php.ru/manual/mysqli.kill.html :
    // 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. Вот такие чудеса.
Re[2]: PHP: Отмена запроса в MySQL по обрыву соединения с браузером
От: loginx  
Дата: 04.01.14 18:33
Оценка:
имхо странная какая то задача, ну оборвалось соединение и Бог с ним, зачем еще что-то такое наворачивать?
Зачем какие-то пассы делать с майскл?!

И еще такой момент — на некоторых моих хостингах буфер PHP не 32 байта а 1 МБ да-да
на других 100кб, еще на одном 40КБ так что ...

Несмотря на странность задачи я бы предложил ее решать на ПЕРЛ
там можно организовать встречные потоки на ввод и вывод,
также там можно повесить кол-бэк на входной поток от браузера
и там каждые скажем 1 сек или N пришедших байт выдавать порцию на выходной поток
так как перл обычно в cgi режиме и там такая схема как бы родная! и буфера не мешают
на большинстве хостингов.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.