Задача весьма тривиальна: Через Apache/PHP пользователем производится некий довольно долгий (нормальное время 1-2 минуты, но может и до 5 минут) запрос данных, в данном случае к СУБД MySQL. Проблема: если пользователь передумал ждать и нажал на кнопку «отмена» либо соединение прекратилось по какой-то другой причине хочется прервать работу скрипта PHP и запроса в MySQL, что бы они бесполезно не тормозили сервер. Что я узнал: PHP не умеет определять обрыв соединения с браузером, если ничего не посылать. Что я перепробовал: 1.a) Переодическая посылка байт в браузер из отдельного потока: Код (Text): 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) Посылка тестовых байт в основном потоке, запрос — во втором: Код (Text): 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, сделал асинхронный запрос со сканированием готовности результата в цикле: Код (Text): 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 переводов строки каждую секунду посылать?? А есть ли решения поэлегантней?
Ваша задача на PHP не релизуема. Дело в том, что после того, как Apache начал обрабатывать обрабатывать запрос и запустил PHP скрипт, до окончания работы с браузером скрипт никак не связан. Только после того, как скрипт полностью отработает, он посылает результат обратно.
скажем так, не для людей. Вам стоит поступить так: - в бд завести таблицу, куда помещать некий идентификатор запущенного процесса, и писать туда статус и результат. - когда посетитель запрашивает, запустить процесс, вернуть идентификатор - потом раз в несколько секунд опрашивать сервер о статусе по этому идентификатору
С SQL запросами в MySQL всё понятно. Но как получить id и затем опросить статус PHP-процесса? Как его завершать из другого процесса? Честно говоря, не думаю что вот это возможно. Добавлено спустя 14 минут 50 секунд: Нда. Посмотрите пример на этой же странице, там в комментариях однозначно идёт речь о работе с браузером. Впрочем, этот пример схематичен; он прямо не соответсвтует примечанию, которое идёт сразу после него - вывода в цикле и вызова flush в нём нет. Посмотрите тогда общую статью "Работа с соединениями" (доступна с указанной статьи по ссылке номер три в "Смотрите также"). Там в самом начале следующее: Тут совершенно явно речь не о коммандной строке, не так ли?
Задачу сделал. Одного байта посылать для проверки соединения не достаточно — информация походит через каскад буферов, в том числе через gzip-паковщих. Эксперимент показал, что в моём случае достаточно 32 байт. Вот код: Код (Text): // 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. Вот такие чудеса.