Пишем обработчик ошибок для phpredis

Источник: habrahabr
habrahabr

Пишем обработчик ошибок для phpredis

Началось все с того, что у нас в компании решили сделать прокси/балансировщик нагрузки который бы, в зависимости от ключа, отправлял запрос на тот или иной инстанс 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 у нас в коде не используется потому в классе его отдельно не выносил.

Учитывая, что иногда, хоть и крайне редко, коннект с редисом зависает даже без использования прокси, то данные обертки успешно используются и по сей день.


Страница сайта http://www.interface.ru
Оригинал находится по адресу http://www.interface.ru/home.asp?artId=28099