За последние 24 часа нас посетили 18065 программистов и 1599 роботов. Сейчас ищут 880 программистов ...

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

Тема в разделе "Прочие вопросы по PHP", создана пользователем shestero, 21 дек 2013.

  1. shestero

    shestero Новичок

    С нами с:
    21 дек 2013
    Сообщения:
    4
    Симпатии:
    0
    Задача весьма тривиальна:
    Через Apache/PHP пользователем производится некий довольно долгий (нормальное время 1-2 минуты, но может и до 5 минут) запрос данных, в данном случае к СУБД MySQL.
    Проблема: если пользователь передумал ждать и нажал на кнопку «отмена» либо соединение прекратилось по какой-то другой причине хочется прервать работу скрипта PHP и запроса в MySQL, что бы они бесполезно не тормозили сервер.
    Что я узнал:
    PHP не умеет определять обрыв соединения с браузером, если ничего не посылать.
    Что я перепробовал:
    1.a) Переодическая посылка байт в браузер из отдельного потока:
    Код (Text):
    1. class Ping0 extends Thread {
    2.   public function run() {
    3.     echo("<!-- 0 -->\n"); // or: echo(0);
    4.     //flush();
    5.     sleep(1);
    6.   }
    7. }
    8.  
    9. function db_query_long($qstring,$conn)
    10. {
    11.   ignore_user_abort(false);
    12.  
    13.   $ping0 = new Ping0();
    14.   $ping0->start();
    15.  
    16.   $ret = db_query($qstring,$conn);
    17.  
    18.   // $ping0->stop();
    19.  
    20.   return $ret;
    21. }
    Результат: при раскомментировании flush() скрипт не работает; в браузере ошибка:
    Баг? Фича?
    1.b) Посылка тестовых байт в основном потоке, запрос — во втором:
    Код (Text):
    1. class T extends Thread {
    2.   protected $arg;
    3.   protected $conn;
    4.   protected $done = false;
    5.   protected $res  = null;
    6.  
    7.   public function __construct($arg,$conn) {
    8.     $this->arg  = $arg;
    9.     $this->conn = $conn;
    10.   }
    11.  
    12.   public function isCompleated() {
    13.     return $this->done;
    14.   }
    15.  
    16.   public function getResult() {
    17.     return $this->res;
    18.   }
    19.  
    20.   public function run() {
    21.     $this->res = db_query($this->arg,$this->conn);
    22.     $this->done = true;
    23.   }
    24. }
    25.  
    26. function db_query_long($qstring,$conn)
    27. {
    28.   $tout = 300; // timeout, sec ***
    29.  
    30.   ignore_user_abort(false);
    31.  
    32.   $thread = new T($qstring,$conn);
    33.   $thread->start();
    34.  
    35.   for ($i=1; $i<=$tout; $i++)
    36.   {
    37.     sleep(1);
    38.     if ($thread->isCompleated())
    39.     {
    40.       return $thread->getResult();
    41.     }
    42.  
    43.     echo(0);
    44.     flush();
    45.   }
    46.  
    47.   return false; // timeout
    48. }
    То же какая-то ошибка... :-(
    2) Думал использовать mysql_unbuffered_query, но не нашёл, как определять, выполнился ли запрос или нет, не вызвав блокирующее чтение.
    3) Переписал на MySQLi, сделал асинхронный запрос со сканированием готовности результата в цикле:
    Код (Text):
    1. function shutdown_mysqli()
    2. {
    3.     global $gl_mysqli1;
    4.     if ($gl_mysqli1)
    5.     {
    6.       $thread_id = $gl_mysqli1->thread_id;
    7.       if ($thread_id)
    8.       {
    9.             $gl_mysqli1->kill($thread_id);
    10.       }
    11.  
    12.       $gl_mysqli1->close();
    13.       //mysqli_close();
    14.       $gl_mysqli1 = null;
    15.     }
    16. }
    17. register_shutdown_function('shutdown_mysqli');
    18.  
    19. function db_query_long($qstring,$conn)
    20. {
    21.     global $strLastSQL,$dDebug;
    22.     global $gl_mysqli1;
    23.  
    24.     if (false) // ($gl_mysqli1==null)
    25.     {
    26.       return db_query($qstring,$conn); // using old mysql interface
    27.     }
    28.  
    29.     $tout = 300; // timeout 300 sec = 5 min
    30.  
    31.     if ($dDebug===true)
    32.        echo $qstring."<br>";
    33.     $strLastSQL=$qstring;
    34.  
    35.     $r = $gl_mysqli1->query($qstring, MYSQLI_ASYNC ); // MYSQLI_USE_RESULT );
    36.     // $thread_id = $gl_mysqli1->thread_id; // checked: has right value
    37.  
    38.     ignore_user_abort(false);
    39.  
    40.     for ($i=0; $i<$tout*2; $i++)
    41.     {
    42.       $ready = $error = $reject = array($gl_mysqli1);
    43.       // $ready[] = $error[] = $reject[] = $gl_mysqli1;
    44.  
    45.       mysqli_poll( $ready,$error,$reject, 0,500000); // wait 1/2 sec
    46.       if (count($ready)>0)
    47.       {
    48.       // ready
    49.       $r = $gl_mysqli1->reap_async_query();
    50.       if ($r)
    51.       {
    52.         // normal exit
    53.         return $r;
    54.       }
    55.       // some error
    56.       return $r;
    57.       }
    58.       if ( count($error)>0 || count($reject)>0 )
    59.       {
    60.       trigger_error("(" . $gl_mysqli1->connect_errno . ") "
    61.         . $gl_mysqli1->connect_error, E_USER_ERROR);
    62.  
    63.       // error
    64.       return null;
    65.       }
    66.  
    67.       // test connection
    68.       echo("\n"); // was: (0);
    69.       flush();
    70.       ob_flush();
    71.  
    72.       if (connection_status()!=CONNECTION_NORMAL)
    73.       {
    74.     shutdown_mysqli();
    75.         return null;
    76.       }
    77.     }
    78.  
    79.     // time out
    80.     return null;
    81. }
    (Посылку нуля пришлось заменить на «\n» так как похоже нули портят формат JSON, в котором транспортируются данные в браузер).
    Запрос работает, но обрыва соединения не чувствует! Возможно что-то где-то ещё кешируется? Что 10k переводов строки каждую секунду посылать??

    А есть ли решения поэлегантней?
     
  2. mkramer

    mkramer Суперстар
    Команда форума Модератор

    С нами с:
    20 июн 2012
    Сообщения:
    8.600
    Симпатии:
    1.764
    Ваша задача на PHP не релизуема. Дело в том, что после того, как Apache начал обрабатывать обрабатывать запрос и запустил PHP скрипт, до окончания работы с браузером скрипт никак не связан. Только после того, как скрипт полностью отработает, он посылает результат обратно.
     
  3. shestero

    shestero Новичок

    С нами с:
    21 дек 2013
    Сообщения:
    4
    Симпатии:
    0
    А зачем же тогда функции ignore_user_abort, connection_status, connection_aborted ?
     
  4. igordata

    igordata Суперстар
    Команда форума Модератор

    С нами с:
    18 мар 2010
    Сообщения:
    32.408
    Симпатии:
    1.768
    скажем так, не для людей.

    Вам стоит поступить так:
    - в бд завести таблицу, куда помещать некий идентификатор запущенного процесса, и писать туда статус и результат.
    - когда посетитель запрашивает, запустить процесс, вернуть идентификатор
    - потом раз в несколько секунд опрашивать сервер о статусе по этому идентификатору
     
  5. mkramer

    mkramer Суперстар
    Команда форума Модератор

    С нами с:
    20 июн 2012
    Сообщения:
    8.600
    Симпатии:
    1.764
    www.ru2.php.net/manual/ru/function.ignore-user-abort.php
     
  6. shestero

    shestero Новичок

    С нами с:
    21 дек 2013
    Сообщения:
    4
    Симпатии:
    0
    С SQL запросами в MySQL всё понятно. Но как получить id и затем опросить статус PHP-процесса? Как его завершать из другого процесса?
    Честно говоря, не думаю что вот это возможно.

    Добавлено спустя 14 минут 50 секунд:
    Нда.
    Посмотрите пример на этой же странице, там в комментариях однозначно идёт речь о работе с браузером. Впрочем, этот пример схематичен; он прямо не соответсвтует примечанию, которое идёт сразу после него - вывода в цикле и вызова flush в нём нет.
    Посмотрите тогда общую статью "Работа с соединениями" (доступна с указанной статьи по ссылке номер три в "Смотрите также").
    Там в самом начале следующее:
    Тут совершенно явно речь не о коммандной строке, не так ли? :)
     
  7. shestero

    shestero Новичок

    С нами с:
    21 дек 2013
    Сообщения:
    4
    Симпатии:
    0
    Задачу сделал.
    Одного байта посылать для проверки соединения не достаточно — информация походит через каскад буферов, в том числе через gzip-паковщих. Эксперимент показал, что в моём случае достаточно 32 байт.
    Вот код:
    Код (Text):
    1. // global variables
    2. $gl_tout    = 240;   // timeout 240 sec = 4 min
    3. $gl_longsql     = "";
    4.  
    5. function shutdown_mysqli($cause, $tout, $sql = "")
    6. {
    7.     global $gl_mysqli1;
    8.     if ($gl_mysqli1)
    9.     {
    10.       $thread_id = $gl_mysqli1->thread_id;
    11.       if ($thread_id)
    12.       {
    13.     $gl_mysqli1->kill($thread_id);
    14.     // Note from http://php.ru/manual/mysqli.kill.html :
    15.     // Be careful using this before mysqli::close.
    16.     // Killing the thread before actually closing the connection will leave the connection open!
    17.     // And depending on your max_connections and max_user_connections (by default the same),
    18.     // this could result in a "Max connections reached for **** user" message.
    19.       }
    20.  
    21.       $gl_mysqli1->close(); // or mysqli_close();
    22.       $gl_mysqli1 = null;
    23.  
    24.       // it look's like $gl_mysqli1->kill($thread_id); and closing MySQLi connection above
    25.       // doesn't actually KILLs the request sometimes. Why?
    26.       if ($thread_id)
    27.       {
    28.     mysql_query("KILL $thread_id"); // KILLing using $conn - the old default connection
    29.       }
    30.  
    31.       // just log event into special table
    32.       log_long_query($cause, $tout, $thread_id, $sql );
    33.     }
    34. }
    35. register_shutdown_function("shutdown_mysqli", "shutdown", $gl_toutm, $gl_longsql);
    36.  
    37. function db_query_long($qstring,$conn)
    38. {
    39.     global $strLastSQL,$dDebug;
    40.     global $gl_mysqli1;
    41.     global $gl_tout;
    42.     global $gl_longsql; $gl_longsql = $qstring;
    43.  
    44.     if (false) // ($gl_mysqli1==null)
    45.     {
    46.       return db_query($qstring,$conn); // using old mysql interface
    47.     }
    48.  
    49.     if ($dDebug===true)
    50.         echo $qstring."<br>";
    51.     $strLastSQL=$qstring;
    52.  
    53.     $r = $gl_mysqli1->query($qstring, MYSQLI_ASYNC ); // MYSQLI_USE_RESULT );
    54.     // $thread_id = $gl_mysqli1->thread_id; // checked: has right value
    55.  
    56.     ob_implicit_flush(true);
    57.     ignore_user_abort(false);
    58.  
    59.     for ($i=0; $i<$gl_tout; $i++)
    60.     {
    61.       $ready = $error = $reject = array($gl_mysqli1);
    62.       // $ready[] = $error[] = $reject[] = $gl_mysqli1;
    63.  
    64.       mysqli_poll( $ready,$error,$reject, 0, 1000000); // wait 1 sec
    65.       if (count($ready)>0)
    66.       {
    67.       // ready
    68.       $r = $gl_mysqli1->reap_async_query();
    69.       if ($r)
    70.       {
    71.         // normal exit
    72.         $gl_longsql = ""; // no log needed
    73.         return $r;
    74.       }
    75.       // some error ??
    76.       return $r;
    77.       }
    78.       if ( count($error)>0 || count($reject)>0 )
    79.       {
    80.       // error
    81.       trigger_error("(" . $gl_mysqli1->connect_errno . ") "
    82.         . $gl_mysqli1->connect_error, E_USER_ERROR);
    83.  
    84.       shutdown_mysqli("error", $gl_tout, $qstring);
    85.       return null;
    86.       }
    87.  
    88.       // test connection
    89.       echo str_repeat("\n",32); // was: (0);
    90.       flush();
    91.       ob_flush();
    92.  
    93.       if (connection_status()!=CONNECTION_NORMAL)
    94.       {
    95.     shutdown_mysqli("disconnect", $gl_tout, $qstring);
    96.         return null;
    97.       }
    98.  
    99.       // normal stage, but results not ready yet
    100.       // log_long_query("test",0,$gl_mysqli1->thread_id,$qstring);
    101.     }
    102.  
    103.     // time over
    104.     shutdown_mysqli("time out", $gl_tout, $qstring);
    105.     return null;
    106. }
    Для непонятливых - соединения старым и новым интерфейсом с 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. Вот такие чудеса.
     
  8. runcore

    runcore Старожил

    С нами с:
    12 окт 2012
    Сообщения:
    3.625
    Симпатии:
    158
    странные задачи вызывают неадекватные решения