Pull to refresh

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

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

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

Articles