📜 ⬆️ ⬇️

Writing an error handler for phpredis

It all started with the fact that in our company we decided to make a proxy / load balancer that, depending on the key, would send a request to one or another Redis instance. Since ideally nothing works right away, the project written in php, working with radish (using phpredis) through this same balancer, took off with enviable 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 departures. Then I came up with the idea to replace the Redis object with my own, from the inside of which I would have called all the methods of a real object ...

It is natural to duplicate all the methods of the original class is very expensive, and why not, because there is a wonderful method __call , which is accessed, when calling a non-existent method of the object. At the input we get the name of the requested method and an array of arguments, and then successfully call the required method of the original 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 to call the same method again. After 5 unsuccessful calls, we stop torturing the proxy and go to the 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" when departing with the error "protocol error", because it was precisely these errors that we hunted.

To integrate it, you just had to replace it.
 $r=new Redis(); $r->connect('127.0.0.1',6379,10); 
on
 $r=new RedisErrHandler('127.0.0.1',6379,10); 

This option worked fine for the time being, until once the script crashed while 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 of all, 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, the class for error handling in the transaction object is written, 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 re-send all the transaction commands on departure, all calls, except exec (), which directly completes the transaction, were entered into an array and sent to the server when the latter was called. Discard is not used in our code because in the class it could not stand it separately.

Considering that sometimes, although extremely rarely, the connection with radish hangs even without the use of a proxy, these wrappers are successfully used to this day.

Source: https://habr.com/ru/post/137536/


All Articles