diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 8df9393..0000000 --- a/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -# Compiled Object files -*.slo -*.lo -*.o - -# Compiled Dynamic libraries -*.so - -# Compiled Static libraries -*.lai -*.la -*.a diff --git a/README.md b/README.md index 9d8ce74..e7975b0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,164 @@ -php-redissentinelclient -======================= +# php-redissentinelclient # -redis sentinel client for php \ No newline at end of file +Redis Sentinel client for PHP + +This class is written by Ryota Namiki and Casper Langemeijer. It's methods are the same as the available +sentinel commands. For more information on Redis Sentinel see http://redis.io/topics/sentinel + +## Usage ## +This class is constructed with a hostname or ip parameter, and a secondary optional port parameter. If not +specified 26379 is used as the default port. + + $RedisSentinel = new RedisSentinelClient('127.0.0.1', 26379); + +The Sentinel commands implemented as class methods: + + +### ping ### + + $ok = $RedisSentinel->ping(); + +This method returns true on success, false on failure. + + +### masters ### + + $masters = $RedisSentinel->masters(); + +This method returns an array of all masters currently known to the sentinel. The array looks like this: + + array ( + 0 => array ( + 'name' => 'MASTER2', + 'ip' => '127.0.0.1', + 'port' => '6380', + 'runid' => 'a3e509206a1dd6068e2d6ece73e6b856bca62c5e', + 'flags' => 'master', + 'pending-commands' => '0', + 'last-ok-ping-reply' => '463', + 'last-ping-reply' => '463', + 'info-refresh' => '8993', + 'num-slaves' => '0', + 'num-other-sentinels' => '0', + 'quorum' => '1', + ), + 1 => array ( + 'name' => 'MASTER1', + 'ip' => '127.0.0.1', + 'port' => '6379', + 'runid' => 'b91e0679fc593da147b1ba32a4e026fac228b124', + 'flags' => 'master', + 'pending-commands' => '0', + 'last-ok-ping-reply' => '62', + 'last-ping-reply' => '62', + 'info-refresh' => '8993', + 'num-slaves' => '1', + 'num-other-sentinels' => '0', + 'quorum' => '1', + ), + ) + +Note that the list of masters is not always in the same order. If you use this to build an array to feed +RedisArray(), make sure you sort it first.' + + +### slaves ### + + $slaves = $RedisSentinel->slaves('MASTER1'); + +This method returns an array of all slaves for a specific master, as currently known to the sentinel. The array +looks like this: + + array ( + 0 => array ( + 'name' => '127.0.0.1:6381', + 'ip' => '127.0.0.1', + 'port' => '6381', + 'runid' => 'b5576a2c95c804e42e812e9b034f34bd5e3f754f', + 'flags' => 'slave', + 'pending-commands' => '0', + 'last-ok-ping-reply' => '849', + 'last-ping-reply' => '849', + 'info-refresh' => '3759', + 'master-link-down-time' => '0', + 'master-link-status' => 'ok', + 'master-host' => '127.0.0.1', + 'master-port' => '6379', + 'slave-priority' => '100', + ), + ) + + +### is-master-down-by-addr ### + + $data = $RedisSentinel->is_master_down_by_addr('127.0.0.1', 6379); + +This method returns an array of two elements where the first is 0 or 1 (0 if the master with that address +is known and is in SDOWN state, 1 otherwise). The second element of the reply is the subjective leader +for this master, that is, the runid of the Redis Sentinel instance that should perform the failover +accordingly to the queried instance. + + array( + 0 => '0', + 1 => '13a81bb82272e22a66a9398cba7786ed0f1d67b7', + ) + + +### get-master-addr-by-name ### + + $addr = $RedisSentinel->get-master-addr-by-name('MASTER1'); + +This method returns the ip and port number of the master with that name. If a failover is in progress +or terminated successfully for this master it returns the address and port of the promoted slave. + + array( + 0 => array( + "127.0.0.1" (9 chars, 9 bytes) => "6380" (4 chars, 4 bytes), + ), + ) + + +### reset ### + + $number = $RedisSentinel->reset('MASTER*'); + +This command will reset all the masters with matching name. The pattern argument is a glob-style pattern. +The reset process clears any previous state in a master (including a failover in progress), and removes +every slave and sentinel already discovered and associated with the master. + +The return value is the number of master that matched the pattern + + +### info ### + + $info = $RedisSentinel->info(); + +The INFO command returns information and statistics about the server in a format that is simple to parse by +computers and easy to read by humans. The return value is a multiline string. \n is used for line-endings. + +An example of the string returned: + + # Server + redis_version:2.6.13 + redis_git_sha1:00000000 + redis_git_dirty:0 + redis_mode:sentinel + os:Linux 2.6.32-5-xen-amd64 x86_64 + arch_bits:64 + multiplexing_api:epoll + gcc_version:4.4.5 + process_id:12265 + run_id:5671125a89caf7ca24bc3fa1be0ec3702594223b + tcp_port:26379 + uptime_in_seconds:6146 + uptime_in_days:0 + hz:10 + lru_clock:1282700 + + # Sentinel + sentinel_masters:32 + sentinel_tilt:0 + sentinel_running_scripts:0 + sentinel_scripts_queue_length:0 + master0:name=M12,status=ok,address=192.168.1.1:6012,slaves=1,sentinels=2 + master1:name=M13,status=ok,address=192.168.1.1:6013,slaves=1,sentinels=2 diff --git a/RedisSentinelClient.php b/RedisSentinelClient.php index 8e16f8d..da6d573 100644 --- a/RedisSentinelClient.php +++ b/RedisSentinelClient.php @@ -1,13 +1,17 @@ + * @author Casper Langemeijer */ -class RedisSentinelClientNoConnectionExecption extends Exception { } -/*! +class RedisSentinelClientNoConnectionException extends Exception +{ +} + +/** * @class RedisSentinelClient * - * Redis Sentinel クライアントクラス + * Redis Sentinel client class */ class RedisSentinelClient { @@ -15,41 +19,60 @@ class RedisSentinelClient protected $_host; protected $_port; - public function __construct($h, $p = 26379) { - $this->_host = $h; - $this->_port = $p; + public function __construct($host, $port = 26379) + { + $this->_host = $host; + $this->_port = $port; } - public function __destruct() { + + public function __destruct() + { if ($this->_socket) { - $this->_close(); + @fclose($this->_socket); + } + } + + /** + * Issue PING command + * + * @return boolean true on success, false on failure + */ + public function ping() + { + if (!$this->_connect()) { + return false; } + + $this->_write('PING'); + $data = $this->_getLine(); + + return ($data === '+PONG'); } - /*! - * PING コマンド発行 + /** + * Issue INFO command * - * @retval boolean true 疎通成功 - * @retval boolean false 疎通失敗 + * @return string containing some information the redis sentinel wants to share with us */ - public function ping() { - if ($this->_connect()) { - $this->_write('PING'); - $this->_write('QUIT'); - $data = $this->_get(); - $this->_close(); - return ($data === '+PONG'); - } else { + public function info() + { + if (!$this->_connect()) { return false; } + + $this->_write('INFO'); + $data = $this->_readReply(); + + return $data; } - /*! - * SENTINEL masters コマンド発行 + /** + * Issue SENTINEL masters command * - * @retval array サーバからの返却値を読みやすくした値 + * @return array of masters, contains the fields returned by the sentinel * @code * array ( - * [0] => // master の index + * [0] => // master index * array( * 'name' => 'mymaster', * 'host' => 'localhost', @@ -59,24 +82,25 @@ public function ping() { * ... * ) * @endcode + * @throws RedisSentinelClientNoConnectionException */ - public function masters() { - if($this->_connect()) { - $this->_write('SENTINEL masters'); - $this->_write('QUIT'); - $data = $this->_extract($this->_get()); - $this->_close(); - return $data; - } else { - throw new RedisSentinelClientNoConnectionExecption; + public function masters() + { + if (!$this->_connect()) { + throw new RedisSentinelClientNoConnectionException; } + + $this->_write('SENTINEL masters'); + $data = $this->_readReply(); + + return $data; } - /*! - * SENTINEL slaves コマンド発行 + /** + * Issue SENTINEL slaves command * - * @param [in] $master string マスター名称 - * @retval array サーバからの返却値を読みやすくした値 + * @param $master string master name + * @return array of slaves for the specified masters. returns data array with fields returned by the sentinel. * @code * array ( * [0] => @@ -89,51 +113,55 @@ public function masters() { * ... * ) * @endcode + * @throws RedisSentinelClientNoConnectionException */ - public function slaves($master) { - if ($this->_connect()) { - $this->_write('SENTINEL slaves ' . $master); - $this->_write('QUIT'); - $data = $this->_extract($this->_get()); - $this->_close(); - return $data; - } else { - throw new RedisSentinelClientNoConnectionExecption; + public function slaves($master) + { + if (!$this->_connect()) { + throw new RedisSentinelClientNoConnectionException; } + + $this->_write('SENTINEL slaves ' . $master); + $data = $this->_readReply(); + + return $data; } - /*! - * SENTINEL is-master-down-by-addr コマンド発行 + /** + * Issue SENTINEL is-master-down-by-addr command * - * @param [in] $ip string 対象サーバIPアドレス - * @param [in] $port integer ポート番号 - * @retval array サーバからの返却値を読みやすくした値 + * @param $ip string target server IP address + * @param $port integer port number + * @return array with fields returned by the sentinel. * @code * array ( * [0] => 1 * [1] => leader * ) * @endcode + * @throws RedisSentinelClientNoConnectionException */ - public function is_master_down_by_addr($ip, $port) { - if ($this->_connect()) { - $this->_write('SENTINEL is-master-down-by-addr ' . $ip . ' ' . $port); - $this->_write('QUIT'); - $data = $this->_get(); - $lines = explode("\r\n", $data, 4); - list (/* elem num*/, $state, /* length */, $leader) = $lines; - $this->_close(); - return array(ltrim($state, ':'), $leader); - } else { - throw new RedisSentinelClientNoConnectionExecption; + public function is_master_down_by_addr($ip, $port) + { + if (!$this->_connect()) { + throw new RedisSentinelClientNoConnectionException; } + + $this->_write('SENTINEL is-master-down-by-addr ' . $ip . ' ' . $port); + + $this->_getLine(); + $state = $this->_getLine(); + $this->_getLine(); + $leader = $this->_getLine(); + + return array($state, $leader); } - /*! - * SENTINEL get-master-addr-by-name コマンド発行 + /** + * Issue SENTINEL get-master-addr-by-name command * - * @param [in] $master string マスター名称 - * @retval array サーバからの返却値を読みやすくした値 + * @param $master string master name + * @return array with fields returned by the sentinel * @code * array ( * [0] => @@ -142,133 +170,148 @@ public function is_master_down_by_addr($ip, $port) { * ) * ) * @endcode + * @throws RedisSentinelClientNoConnectionException */ - public function get_master_addr_by_name($master) { - if ($this->_connect()) { - $this->_write('SENTINEL get-master-addr-by-name ' . $master); - $this->_write('QUIT'); - $data = $this->_extract($this->_get()); - $this->_close(); - return $data; - } else { - throw new RedisSentinelClientNoConnectionExecption; + public function get_master_addr_by_name($master) + { + if (!$this->_connect()) { + throw new RedisSentinelClientNoConnectionException; } + + $this->_write('SENTINEL get-master-addr-by-name ' . $master); + $data = $this->_readReply(); + + return $data; } - /*! - * SENTINEL reset コマンド発行 + /** + * Issue SENTINEL reset command * - * @param [in] $pattern string マスター名称パターン(globスタイル) - * @retval integer pattern にマッチしたマスターの数 + * @param $pattern string Master name pattern (glob style) + * @return integer The number of master that matched + * @throws RedisSentinelClientNoConnectionException */ - public function reset($pattern) { - if ($this->_connect()) { - $this->_write('SENTINEL reset ' . $pattern); - $this->_write('QUIT'); - $data = $this->_get(); - $this->_close(); - return ltrim($data, ':'); - } else { - throw new RedisSentinelClientNoConnectionExecption; + public function reset($pattern) + { + if (!$this->_connect()) { + throw new RedisSentinelClientNoConnectionException; } + + $this->_write('SENTINEL reset ' . $pattern); + $data = $this->_getLine(); + + return $data; } - /*! - * Sentinel サーバとの接続を行う + /** + * This method connects to the sentinel * - * @retval boolean true 接続成功 - * @retval boolean false 接続失敗 + * @return boolean true on success, false on failure */ - protected function _connect() { + protected function _connect() + { + if ($this->_socket !== null) { + return !feof($this->_socket); + } + $this->_socket = @fsockopen($this->_host, $this->_port, $en, $es); - return !!($this->_socket); + return (bool)$this->_socket; } - /*! - * Sentinel サーバとの接続を切断する + /** + * Write a command to the sentinel * - * @retval boolean true 切断成功 - * @retval boolean false 切断失敗 + * @param $c string command + * @return mixed integer number of bytes written + * @return mixed boolean false on failure */ - protected function _close() { - $ret = @fclose($this->_socket); - $this->_socket = null; - return $ret; + protected function _write($c) + { + return fwrite($this->_socket, $c . "\r\n"); } - /*! - * Sentinel サーバからの返却がまだあるか - * - * @retval boolean true 残データ有り - * @retval boolean false 残データ無し - */ - protected function _receiving() { - return !feof($this->_socket); + private function _getLine() + { + return substr(fgets($this->_socket), 0, -2); // strips CRLF } - /*! - * Sentinel サーバへコマンド発行 - * - * @param [in] $c string コマンド文字列 - * @retval mixed integer 書き込んだバイト数 - * @retval mixed boolean false エラー発生 - */ - protected function _write($c) { - return fwrite($this->_socket, $c . "\r\n"); + private function _getData($size) + { + $size += 2; + $data = ''; + while (strlen($data) < $size) { + $data = fread($this->_socket, $size - strlen($data)); + } + $data = substr($data, 0, -2); // strips last CRLF + $data = str_replace("\r\n", "\n", $data); // convert CRLF to LF + return $data; } - /*! - * Sentinel サーバからの返却値を取得 - * - * @retval string 返却値 + /** + * This function parses the reply on a sentinel command + * @return array */ - protected function _get() { - $buf = ''; - while($this->_receiving()) { - $buf .= fgets($this->_socket); + private function _readReply() + { + + if (feof($this->_socket)) { + return false; } - return rtrim($buf, "\r\n+OK\n"); - } - /*! - * 多次元階層を表す Redis レスポンス文字列を配列へ変換 - * - * @param [in] $data string サーバからの返却値文字列 - * @retval array 配列1 - */ - protected function _extract($data) { - if (!$data) return array(); - $lines = explode("\r\n", $data); - $is_root = $is_child = false; - $c = count($lines); - $results = $current = array(); - for ($i = 0; $i < $c; $i++) { - $str = $lines[$i]; - $prefix = substr($str, 0, 1); - if ($prefix === '*') { - if (!$is_root) { - $is_root = true; - $current = array(); - continue; - } else if (!$is_child) { - $is_child = true; - continue; - } else { - $is_root = $is_child = false; - $results[] = $current; - continue; + $str = $this->_getLine(); + $prefix = $str[0]; + $payload = substr($str, 1); + + switch ($prefix) { + case '+': + return $payload; + + case '$': + $size = (int)$payload; + if ($size == -1) { + return null; + } + $str = $this->_getData($size); + return $str; + + case '*': //array + $size = (int)$payload; + if ($size == -1) { + return null; } - } - $keylen = $lines[$i++]; - $key = $lines[$i++]; - $vallen = $lines[$i++]; - $val = $lines[$i++]; - $current[$key] = $val; - - --$i; + + $allStrings = ($size % 2) == 0; + $multibulk = array(); + $multibulkAssoc = array(); + + for ($i = 0; $i < $size; $i++) { + $reply = $this->_readReply(); + if (!is_string($reply)) { + $allStrings = false; + } + + if ($allStrings) { + if (($i % 2) == 0) { + $key = $reply; + } else { + $multibulkAssoc[$key] = $reply; + } + } + $multibulk[$i] = $reply; + } + + if ($allStrings) { + return $multibulkAssoc; + } + return $multibulk; + + case ':': // int + return (int)$payload; + + case '-': + return false; + } - $results[] = $current; - return $results; } }