Календарь на Май 2024 года: calendar2008.ru/2024/may/
Навигация
Главная »  Новости 

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



 

 Что такое верстка сайта.
 Является ли копирайтинг достойным источником дохода.
 ESET запускает интернет-сообщество ESET CLUB.


Главная »  Новости 

© 2024 Team.Furia.Ru.
Частичное копирование материалов разрешено.