Началось все с того, что у нас в компании решили сделать прокси/балансировщик нагрузки который бы, в зависимости от ключа, отправлял запрос на тот или иной инстанс Redis'а. Так как идеально сразу ничего не работает, то написанный на php проект, работающий с редисом(с помощью phpredis) через этот самый балансировщик, с завидной регулярности вылетал с критическими ошибками. Увы прокси не всегда правильно собирал сложные ответы сервера…
Работа с Redis'ом в коде через каждых 10 строк, и оборачивать каждый вызов в try, catch не было ни малейшего желания, но и с постоянными вылетами дебажить было сильно не удобно. Тут мне и пришла в голову идея подменить объект Redis'a своим, изнутри которого я бы уже вызывал все методы настоящего объекта…
Естественно дублировать все методы исходного класса сильно накладно, да и не зачем, ибо существует замечательный метод __call, к которому идет обращение, при вызове несуществующего метода объекта. На вход мы получаем имя запрашиваемого метода и массив аргументов, после чего успешно вызываем с помощью call_user_func_array, нужный метод исходного объекта. Таким образом оборачивать в try, catch нам надо лишь один вызов call_user_func_array.
Итого метод __call выглядит следующим образом:
Если вылазит ошибка, мы отправляем ее на обработчик, а сами пробуем еще раз вызвать тот же метод. После 5ти неудачных вызовов перестаем мучить проксю и идем курить логи…
Первый вариант класса выглядел так:
Он реконнектился при каждом вылете и «умирал» при вылете с ошибкой «protocol error», ибо именно на такие ошибки мы и охотились.
Для его интеграции надо было всего то заменить
Этот вариант прекрасно работал до поры до времени, пока один раз скрипт не вылетел при работе с multi. Так как для транзакций в phpredis выделен отдельный объект, то стало понятно что надо писать обертку еще и для него.
В первую очередь был добавлен метод multi в приведенный выше класс:
Ну и написан класс для обработки ошибок в объекте транзакций, по аналогии к предыдущему:
Дабы иметь возможность повторной отправки всех команд транзакции при вылете, все вызовы, кроме exec(), которая непосредственно завершает транзакцию, заносились в массив и отправлялись на сервер при вызове последней. Discard у нас в коде не используется потому в классе его отдельно не выносил.
Учитывая, что иногда, хоть и крайне редко, коннект с редисом зависает даже без использования прокси, то данные обертки успешно используются и по сей день.
Работа с Redis'ом в коде через каждых 10 строк, и оборачивать каждый вызов в try, catch не было ни малейшего желания, но и с постоянными вылетами дебажить было сильно не удобно. Тут мне и пришла в голову идея подменить объект Redis'a своим, изнутри которого я бы уже вызывал все методы настоящего объекта…
Естественно дублировать все методы исходного класса сильно накладно, да и не зачем, ибо существует замечательный метод __call, к которому идет обращение, при вызове несуществующего метода объекта. На вход мы получаем имя запрашиваемого метода и массив аргументов, после чего успешно вызываем с помощью call_user_func_array, нужный метод исходного объекта. Таким образом оборачивать в try, catch нам надо лишь один вызов call_user_func_array.
Итого метод __call выглядит следующим образом:
public function __call($name, $arguments) { $i=0; while(true) { try{ return call_user_func_array(array($this->obj, $name), $arguments); break; } catch (Exception $e) { $this->handle_exception($e,$name,$arguments); if($i<5) $i++; else die('5 time redis lug'); } } }
Если вылазит ошибка, мы отправляем ее на обработчик, а сами пробуем еще раз вызвать тот же метод. После 5ти неудачных вызовов перестаем мучить проксю и идем курить логи…
Первый вариант класса выглядел так:
class RedisErrHandler { private $obj; private $ip; private $port; private $timeout; public function __construct($ip,$port,$timeout=0) { $this->ip=$ip; $this->port=$port; $this->timeout=$timeout; $this->rconnect(); } private function rconnect() { $this->obj=new Redis; $this->obj->connect($this->ip,$this->port,$this->timeout) or die('Error connecting redis'); } public function __call($name, $arguments) { $i=0; while(true) { try{ return call_user_func_array(array($this->obj, $name), $arguments); break; } catch (Exception $e) { $this->handle_exception($e,$name,$arguments); if($i<5) $i++; else die('5 time redis lug'); } } } private function handle_exception($e,$name,$args) { $err=$e->getMessage(); $msg="Caught exception: ".$err."\tcall ".$name."\targs ".implode(" ",$args)."\n"; if($_SERVER['LOG']) { $handle2=fopen('redis_log.txt','a'); fwrite($handle2,date('H:i:s')."\t$msg"); fclose($handle2); } echo $msg; if(substr(trim($err),0,37)=='Caught exception: protocol error, got') die('bye'); $this->rconnect(); } }
Он реконнектился при каждом вылете и «умирал» при вылете с ошибкой «protocol error», ибо именно на такие ошибки мы и охотились.
Для его интеграции надо было всего то заменить
на$r=new Redis(); $r->connect('127.0.0.1',6379,10);
$r=new RedisErrHandler('127.0.0.1',6379,10);
Этот вариант прекрасно работал до поры до времени, пока один раз скрипт не вылетел при работе с multi. Так как для транзакций в phpredis выделен отдельный объект, то стало понятно что надо писать обертку еще и для него.
В первую очередь был добавлен метод multi в приведенный выше класс:
public function multi($type) { return new RedisMultiErrHandler($this->obj,$type,$this->ip,$this->port,$this->timeout); }
Ну и написан класс для обработки ошибок в объекте транзакций, по аналогии к предыдущему:
class RedisMultiErrHandler { private $obj; private $ip; private $port; private $timeout; private $m; private $type; private $commands; public function __construct(&$redis,$type,$ip,$port,$timeout=0) { $this->ip=$ip; $this->port=$port; $this->timeout=$timeout; $this->type=$type; $this->obj=$redis; $this->m=$this->obj->multi($type); } private function rconnect() { $this->obj=new Redis; $this->obj->connect($this->ip,$this->port,$this->timeout) or die('Error connecting redis'); $this->m=$this->obj->multi($this->type); } public function __call($name, $arguments) { $this->commands[]=array('name'=>$name, 'arguments'=>$arguments); return $this; } private function handle_exception($e) { $err=$e->getMessage(); $msg=''; foreach($this->commands as $command) { $msg.="Multi sent\tcall ".$command['name']."\targs ".implode(" ",$command['arguments'])."\n"; } $msg.="Caught exception: ".$err."\n"; if($_SERVER['LOG']) { $handle2=fopen('redis_multi_log.txt','a'); fwrite($handle2,date('H:i:s')."\t$msg"); fclose($handle2); } echo $msg; if(substr(trim($err),0,37)=='Caught exception: protocol error, got') die('bye'); $this->rconnect(); } public function exec() { $i=0; while(true) { foreach($this->commands as $command) { call_user_func_array(array($this->m, $command['name']), $command['arguments']); } try{ return $this->m->exec(); break; } catch (Exception $e) { $this->handle_exception($e); if($i<5) $i++; else die('5 time mredis lug'); } } } }
Дабы иметь возможность повторной отправки всех команд транзакции при вылете, все вызовы, кроме exec(), которая непосредственно завершает транзакцию, заносились в массив и отправлялись на сервер при вызове последней. Discard у нас в коде не используется потому в классе его отдельно не выносил.
Учитывая, что иногда, хоть и крайне редко, коннект с редисом зависает даже без использования прокси, то данные обертки успешно используются и по сей день.