PHP / [Из песочницы] Пишем обработчик ошибок для phpredis

в 9:08, , рубрики: php, redis, метки: ,

Началось все с того, что у нас в компании решили сделать прокси/балансировщик нагрузки который бы, в зависимости от ключа, отправлял запрос на тот или иной инстанс Redis'а. Так как идеально сразу ничего не работает, то написанный на php проект, работающий с редисом(с помощью phpredis) через этот самый балансировщик, с завидной регулярности вылетал с критическими ошибками. Увы прокси не всегда правильно собирал сложные ответы сервера…
Работа с Redis'ом в коде через каждых 10 строк, и оборачивать каждый вызов в try, cache не было ни малейшего желания, но и с постоянными вылетами дебажить было сильно не удобно. Тут мне и пришла в голову идея подменить объект Redis'a своим, изнутри которого я бы уже вызывал все методы настоящего объекта…
Естественно дублировать все методы исходного класса сильно накладно, да и не зачем, ибо существует замечательный метод __call, к которому идет обращение, при вызове несуществующего метода объекта. На вход мы получаем имя запрашиваемого метода и массив аргументов, после чего успешно вызываем с помощью call_user_func_array, нужный метод исходного объекта. Таким образом оборачивать в try, cache нам надо лишь один вызов 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($iip=$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($igetMessage();
$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 senttcall ".$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 у нас в коде не используется потому в классе его отдельно не выносил.
Учитывая, что иногда, хоть и крайне редко, коннект с редисом зависает даже без использования прокси, то данные обертки успешно используются и по сей день.

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js