RedisConnRef.php 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. <?php
  2. /**
  3. * This program is free software; you can redistribute it and/or modify
  4. * it under the terms of the GNU General Public License as published by
  5. * the Free Software Foundation; either version 2 of the License, or
  6. * (at your option) any later version.
  7. *
  8. * This program is distributed in the hope that it will be useful,
  9. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. * GNU General Public License for more details.
  12. *
  13. * You should have received a copy of the GNU General Public License along
  14. * with this program; if not, write to the Free Software Foundation, Inc.,
  15. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  16. * http://www.gnu.org/copyleft/gpl.html
  17. *
  18. * @file
  19. */
  20. use Psr\Log\LoggerInterface;
  21. use Psr\Log\LoggerAwareInterface;
  22. /**
  23. * Helper class to handle automatically marking connectons as reusable (via RAII pattern)
  24. *
  25. * This class simply wraps the Redis class and can be used the same way
  26. *
  27. * @ingroup Redis
  28. * @since 1.21
  29. */
  30. class RedisConnRef implements LoggerAwareInterface {
  31. /** @var RedisConnectionPool */
  32. protected $pool;
  33. /** @var Redis */
  34. protected $conn;
  35. /** @var string */
  36. protected $server;
  37. /** @var string|null */
  38. protected $lastError;
  39. /**
  40. * @var LoggerInterface
  41. */
  42. protected $logger;
  43. /**
  44. * No authentication errors.
  45. */
  46. const AUTH_NO_ERROR = 200;
  47. /**
  48. * Temporary authentication error; recovered by reauthenticating.
  49. */
  50. const AUTH_ERROR_TEMPORARY = 201;
  51. /**
  52. * Authentication error was permanent and could not be recovered.
  53. */
  54. const AUTH_ERROR_PERMANENT = 202;
  55. /**
  56. * @param RedisConnectionPool $pool
  57. * @param string $server
  58. * @param Redis $conn
  59. * @param LoggerInterface $logger
  60. */
  61. public function __construct(
  62. RedisConnectionPool $pool, $server, Redis $conn, LoggerInterface $logger
  63. ) {
  64. $this->pool = $pool;
  65. $this->server = $server;
  66. $this->conn = $conn;
  67. $this->logger = $logger;
  68. }
  69. public function setLogger( LoggerInterface $logger ) {
  70. $this->logger = $logger;
  71. }
  72. /**
  73. * @return string
  74. * @since 1.23
  75. */
  76. public function getServer() {
  77. return $this->server;
  78. }
  79. public function getLastError() {
  80. return $this->lastError;
  81. }
  82. public function clearLastError() {
  83. $this->lastError = null;
  84. }
  85. /**
  86. * Magic __call handler for most Redis functions.
  87. *
  88. * @param string $name
  89. * @param array $arguments
  90. * @return mixed $res
  91. * @throws RedisException
  92. */
  93. public function __call( $name, $arguments ) {
  94. // Work around https://github.com/nicolasff/phpredis/issues/70
  95. $lname = strtolower( $name );
  96. if (
  97. ( $lname === 'blpop' || $lname === 'brpop' || $lname === 'brpoplpush' )
  98. && count( $arguments ) > 1
  99. ) {
  100. // Get timeout off the end since it is always required and argument length can vary
  101. $timeout = end( $arguments );
  102. // Only give the additional one second buffer if not requesting an infinite timeout
  103. $this->pool->resetTimeout( $this->conn, ( $timeout > 0 ? $timeout + 1 : $timeout ) );
  104. }
  105. return $this->tryCall( $name, $arguments );
  106. }
  107. /**
  108. * Do the method call in the common try catch handler.
  109. *
  110. * @param string $method
  111. * @param array $arguments
  112. * @return mixed $res
  113. * @throws RedisException
  114. */
  115. private function tryCall( $method, $arguments ) {
  116. $this->conn->clearLastError();
  117. try {
  118. $res = $this->conn->$method( ...$arguments );
  119. $authError = $this->checkAuthentication();
  120. if ( $authError === self::AUTH_ERROR_TEMPORARY ) {
  121. $res = $this->conn->$method( ...$arguments );
  122. }
  123. if ( $authError === self::AUTH_ERROR_PERMANENT ) {
  124. throw new RedisException( "Failure reauthenticating to Redis." );
  125. }
  126. } finally {
  127. $this->postCallCleanup();
  128. }
  129. return $res;
  130. }
  131. /**
  132. * Key Scan
  133. * Handle this explicity due to needing the iterator passed by reference.
  134. * See: https://github.com/phpredis/phpredis#scan
  135. *
  136. * @param int &$iterator
  137. * @param string|null $pattern
  138. * @param int|null $count
  139. * @return array $res
  140. */
  141. public function scan( &$iterator, $pattern = null, $count = null ) {
  142. return $this->tryCall( 'scan', [ &$iterator, $pattern, $count ] );
  143. }
  144. /**
  145. * Set Scan
  146. * Handle this explicity due to needing the iterator passed by reference.
  147. * See: https://github.com/phpredis/phpredis#sScan
  148. *
  149. * @param string $key
  150. * @param int &$iterator
  151. * @param string|null $pattern
  152. * @param int|null $count
  153. * @return array $res
  154. */
  155. public function sScan( $key, &$iterator, $pattern = null, $count = null ) {
  156. return $this->tryCall( 'sScan', [ $key, &$iterator, $pattern, $count ] );
  157. }
  158. /**
  159. * Hash Scan
  160. * Handle this explicity due to needing the iterator passed by reference.
  161. * See: https://github.com/phpredis/phpredis#hScan
  162. *
  163. * @param string $key
  164. * @param int &$iterator
  165. * @param string|null $pattern
  166. * @param int|null $count
  167. * @return array $res
  168. */
  169. public function hScan( $key, &$iterator, $pattern = null, $count = null ) {
  170. return $this->tryCall( 'hScan', [ $key, &$iterator, $pattern, $count ] );
  171. }
  172. /**
  173. * Sorted Set Scan
  174. * Handle this explicity due to needing the iterator passed by reference.
  175. * See: https://github.com/phpredis/phpredis#hScan
  176. *
  177. * @param string $key
  178. * @param int &$iterator
  179. * @param string|null $pattern
  180. * @param int|null $count
  181. * @return array $res
  182. */
  183. public function zScan( $key, &$iterator, $pattern = null, $count = null ) {
  184. return $this->tryCall( 'zScan', [ $key, &$iterator, $pattern, $count ] );
  185. }
  186. /**
  187. * Handle authentication errors and automatically reauthenticate.
  188. *
  189. * @return int self::AUTH_NO_ERROR, self::AUTH_ERROR_TEMPORARY, or self::AUTH_ERROR_PERMANENT
  190. */
  191. private function checkAuthentication() {
  192. if ( preg_match( '/^ERR operation not permitted\b/', $this->conn->getLastError() ) ) {
  193. if ( !$this->pool->reauthenticateConnection( $this->server, $this->conn ) ) {
  194. return self::AUTH_ERROR_PERMANENT;
  195. }
  196. $this->conn->clearLastError();
  197. $this->logger->info(
  198. "Used automatic re-authentication for Redis.",
  199. [ 'redis_server' => $this->server ]
  200. );
  201. return self::AUTH_ERROR_TEMPORARY;
  202. }
  203. return self::AUTH_NO_ERROR;
  204. }
  205. /**
  206. * Post Redis call cleanup.
  207. *
  208. * @return void
  209. */
  210. private function postCallCleanup() {
  211. $this->lastError = $this->conn->getLastError() ?: $this->lastError;
  212. // Restore original timeout in the case of blocking calls.
  213. $this->pool->resetTimeout( $this->conn );
  214. }
  215. /**
  216. * @param string $script
  217. * @param array $params
  218. * @param int $numKeys
  219. * @return mixed
  220. * @throws RedisException
  221. */
  222. public function luaEval( $script, array $params, $numKeys ) {
  223. $sha1 = sha1( $script ); // 40 char hex
  224. $conn = $this->conn; // convenience
  225. $server = $this->server; // convenience
  226. // Try to run the server-side cached copy of the script
  227. $conn->clearLastError();
  228. $res = $conn->evalSha( $sha1, $params, $numKeys );
  229. // If we got a permission error reply that means that (a) we are not in
  230. // multi()/pipeline() and (b) some connection problem likely occurred. If
  231. // the password the client gave was just wrong, an exception should have
  232. // been thrown back in getConnection() previously.
  233. if ( preg_match( '/^ERR operation not permitted\b/', $conn->getLastError() ) ) {
  234. $this->pool->reauthenticateConnection( $server, $conn );
  235. $conn->clearLastError();
  236. $res = $conn->eval( $script, $params, $numKeys );
  237. $this->logger->info(
  238. "Used automatic re-authentication for Lua script '$sha1'.",
  239. [ 'redis_server' => $server ]
  240. );
  241. }
  242. // If the script is not in cache, use eval() to retry and cache it
  243. if ( preg_match( '/^NOSCRIPT/', $conn->getLastError() ) ) {
  244. $conn->clearLastError();
  245. $res = $conn->eval( $script, $params, $numKeys );
  246. $this->logger->info(
  247. "Used eval() for Lua script '$sha1'.",
  248. [ 'redis_server' => $server ]
  249. );
  250. }
  251. if ( $conn->getLastError() ) { // script bug?
  252. $this->logger->error(
  253. 'Lua script error on server "{redis_server}": {lua_error}',
  254. [
  255. 'redis_server' => $server,
  256. 'lua_error' => $conn->getLastError()
  257. ]
  258. );
  259. }
  260. $this->lastError = $conn->getLastError() ?: $this->lastError;
  261. return $res;
  262. }
  263. /**
  264. * @param Redis $conn
  265. * @return bool
  266. */
  267. public function isConnIdentical( Redis $conn ) {
  268. return $this->conn === $conn;
  269. }
  270. function __destruct() {
  271. $this->pool->freeConnection( $this->server, $this->conn );
  272. }
  273. }