Writing an error handler for phpredis

It all started with the fact that in our company we decided to make a proxy / load balancer which, depending on the key, would send a request to a particular Redis instance. Since nothing works perfectly right away, a project written in php working with radishes (using phpredis) through this same balancer crashed with enormous regularity with critical errors. Alas, the proxy did not always correctly collect complex server responses ...
Working with Redis in the code every 10 lines, and there was no desire to wrap each call in try, catch, but it was not very convenient to debug with constant crashes. Then the idea came to me to replace the Redis object with my own, from the inside of which I would already call all the methods of the real object ...

It is natural to duplicate all the methods of the source class is very expensive, and why not, because there is a wonderful __call method that is accessed when invoking an object method that does not exist. At the input, we get the name of the requested method and an array of arguments, after which we successfully call the desired method of the source object using call_user_func_array . Thus, wrap in try, catch, we need only one call call_user_func_array.
The total __call method is as follows:
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');
		}
	}
}

If an error occurs, we send it to the handler, and we try again to call the same method. After 5 unsuccessful calls, we stop tormenting the proxy and go to smoke logs ...

The first version of the class looked like this:

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();
	}
}

He reconnected at each departure and “died” during the departure with the “protocol error” error, because it was precisely such errors that we were hunting for.

For its integration, all that had to be replaced was
$r=new Redis();
$r->connect('127.0.0.1',6379,10);
on the
$r=new RedisErrHandler('127.0.0.1',6379,10);

This option worked fine for the time being, until once the script crashed when working with multi. Since a separate object is allocated for transactions in phpredis, it became clear that you need to write a wrapper for it as well.
First, the multi method was added to the above class:
public function multi($type)
{
	return new RedisMultiErrHandler($this->obj,$type,$this->ip,$this->port,$this->timeout);
}

Well, a class has been written for handling errors in a transaction object, by analogy to the previous one:
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');
			}
		}
	}
}


In order to be able to resend all transaction commands on departure, all calls except exec (), which directly completes the transaction, were entered into the array and sent to the server when the latter was called. Discard is not used in our code because in the class I couldn’t stand it separately.

Considering that sometimes, although extremely rarely, the radish connection freezes even without using a proxy, the wrapper data has been successfully used to this day.

Also popular now: