Началось все с того, что у нас в компании решили сделать прокси/балансировщик нагрузки который бы, в зависимости от ключа, отправлял запрос на тот или иной инстанс Redis'а. Так как идеально сразу ничего не работает, то написанный на php проект, работающий с редисом(с помощью phpredis) через этот самый балансировщик, с завидной регулярности вылетал с критическими ошибками. Увы прокси не всегда правильно собирал сложные ответы сервера…
Работа с 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 у нас в коде не используется потому в классе его отдельно не выносил.
Учитывая, что иногда, хоть и крайне редко, коннект с редисом зависает даже без использования прокси, то данные обертки успешно используются и по сей день.