Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 76 |
|
0.00% |
0 / 16 |
CRAP | |
0.00% |
0 / 1 |
| RedisConnRef | |
0.00% |
0 / 75 |
|
0.00% |
0 / 16 |
1056 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
| setLogger | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getServer | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getLastError | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| clearLastError | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| __call | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
42 | |||
| tryCall | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
| scan | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| sScan | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| hScan | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| zScan | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| checkAuthentication | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
| postCallCleanup | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
| luaEval | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
42 | |||
| isConnIdentical | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| __destruct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * @license GPL-2.0-or-later |
| 4 | * @file |
| 5 | */ |
| 6 | |
| 7 | namespace Wikimedia\ObjectCache; |
| 8 | |
| 9 | use Psr\Log\LoggerAwareInterface; |
| 10 | use Psr\Log\LoggerInterface; |
| 11 | use Redis; |
| 12 | use RedisException; |
| 13 | |
| 14 | /** |
| 15 | * Wrapper class for Redis connections that automatically reuses connections (via RAII pattern) |
| 16 | * |
| 17 | * This class proxies a Redis class instance from the php-redis PECL extension. |
| 18 | * All its methods can be called the same way. |
| 19 | * |
| 20 | * @see <https://github.com/phpredis/phpredis#table-of-contents> |
| 21 | * |
| 22 | * @ingroup Cache |
| 23 | * @since 1.21 |
| 24 | */ |
| 25 | class RedisConnRef implements LoggerAwareInterface { |
| 26 | /** @var RedisConnectionPool */ |
| 27 | protected $pool; |
| 28 | /** @var Redis */ |
| 29 | protected $conn; |
| 30 | /** @var string */ |
| 31 | protected $server; |
| 32 | /** @var string|null */ |
| 33 | protected $lastError; |
| 34 | |
| 35 | /** |
| 36 | * @var LoggerInterface |
| 37 | */ |
| 38 | protected $logger; |
| 39 | |
| 40 | /** |
| 41 | * No authentication errors. |
| 42 | */ |
| 43 | private const AUTH_NO_ERROR = 200; |
| 44 | |
| 45 | /** |
| 46 | * Temporary authentication error; recovered by reauthenticating. |
| 47 | */ |
| 48 | private const AUTH_ERROR_TEMPORARY = 201; |
| 49 | |
| 50 | /** |
| 51 | * Authentication error was permanent and could not be recovered. |
| 52 | */ |
| 53 | private const AUTH_ERROR_PERMANENT = 202; |
| 54 | |
| 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 | |
| 70 | public function setLogger( LoggerInterface $logger ): void { |
| 71 | $this->logger = $logger; |
| 72 | } |
| 73 | |
| 74 | /** |
| 75 | * @return string |
| 76 | * @since 1.23 |
| 77 | */ |
| 78 | public function getServer() { |
| 79 | return $this->server; |
| 80 | } |
| 81 | |
| 82 | /** |
| 83 | * @return string|null |
| 84 | */ |
| 85 | public function getLastError() { |
| 86 | return $this->lastError; |
| 87 | } |
| 88 | |
| 89 | public function clearLastError() { |
| 90 | $this->lastError = null; |
| 91 | } |
| 92 | |
| 93 | /** |
| 94 | * Magic __call handler for most Redis functions. |
| 95 | * |
| 96 | * @param string $name |
| 97 | * @param array $arguments |
| 98 | * @return mixed |
| 99 | * @throws RedisException |
| 100 | */ |
| 101 | public function __call( $name, $arguments ) { |
| 102 | // Work around https://github.com/nicolasff/phpredis/issues/70 |
| 103 | $lname = strtolower( $name ); |
| 104 | if ( |
| 105 | ( $lname === 'blpop' || $lname === 'brpop' || $lname === 'brpoplpush' ) |
| 106 | && count( $arguments ) > 1 |
| 107 | ) { |
| 108 | // Get timeout off the end since it is always required and argument length can vary |
| 109 | $timeout = end( $arguments ); |
| 110 | // Only give the additional one second buffer if not requesting an infinite timeout |
| 111 | $this->pool->resetTimeout( $this->conn, ( $timeout > 0 ? $timeout + 1 : $timeout ) ); |
| 112 | } |
| 113 | |
| 114 | return $this->tryCall( $name, $arguments ); |
| 115 | } |
| 116 | |
| 117 | /** |
| 118 | * Do the method call in the common try catch handler. |
| 119 | * |
| 120 | * @param string $method |
| 121 | * @param array $arguments |
| 122 | * @return mixed |
| 123 | * @throws RedisException |
| 124 | */ |
| 125 | private function tryCall( $method, $arguments ) { |
| 126 | $this->conn->clearLastError(); |
| 127 | try { |
| 128 | $res = $this->conn->$method( ...$arguments ); |
| 129 | $authError = $this->checkAuthentication(); |
| 130 | if ( $authError === self::AUTH_ERROR_TEMPORARY ) { |
| 131 | $res = $this->conn->$method( ...$arguments ); |
| 132 | } |
| 133 | if ( $authError === self::AUTH_ERROR_PERMANENT ) { |
| 134 | throw new RedisException( "Failure reauthenticating to Redis." ); |
| 135 | } |
| 136 | return $res; |
| 137 | } finally { |
| 138 | $this->postCallCleanup(); |
| 139 | } |
| 140 | } |
| 141 | |
| 142 | /** |
| 143 | * Key Scan |
| 144 | * Handle this explicitly due to needing the iterator passed by reference. |
| 145 | * See: https://github.com/phpredis/phpredis#scan |
| 146 | * |
| 147 | * @param int &$iterator |
| 148 | * @param string|null $pattern |
| 149 | * @param int|null $count |
| 150 | * @return array |
| 151 | */ |
| 152 | public function scan( &$iterator, $pattern = null, $count = null ) { |
| 153 | return $this->tryCall( 'scan', [ &$iterator, $pattern, $count ] ); |
| 154 | } |
| 155 | |
| 156 | /** |
| 157 | * Set Scan |
| 158 | * Handle this explicitly due to needing the iterator passed by reference. |
| 159 | * See: https://github.com/phpredis/phpredis#sScan |
| 160 | * |
| 161 | * @param string $key |
| 162 | * @param int &$iterator |
| 163 | * @param string|null $pattern |
| 164 | * @param int|null $count |
| 165 | * @return array |
| 166 | */ |
| 167 | public function sScan( $key, &$iterator, $pattern = null, $count = null ) { |
| 168 | return $this->tryCall( 'sScan', [ $key, &$iterator, $pattern, $count ] ); |
| 169 | } |
| 170 | |
| 171 | /** |
| 172 | * Hash Scan |
| 173 | * Handle this explicitly due to needing the iterator passed by reference. |
| 174 | * See: https://github.com/phpredis/phpredis#hScan |
| 175 | * |
| 176 | * @param string $key |
| 177 | * @param int &$iterator |
| 178 | * @param string|null $pattern |
| 179 | * @param int|null $count |
| 180 | * @return array |
| 181 | */ |
| 182 | public function hScan( $key, &$iterator, $pattern = null, $count = null ) { |
| 183 | return $this->tryCall( 'hScan', [ $key, &$iterator, $pattern, $count ] ); |
| 184 | } |
| 185 | |
| 186 | /** |
| 187 | * Sorted Set Scan |
| 188 | * Handle this explicitly due to needing the iterator passed by reference. |
| 189 | * See: https://github.com/phpredis/phpredis#hScan |
| 190 | * |
| 191 | * @param string $key |
| 192 | * @param int &$iterator |
| 193 | * @param string|null $pattern |
| 194 | * @param int|null $count |
| 195 | * @return array |
| 196 | */ |
| 197 | public function zScan( $key, &$iterator, $pattern = null, $count = null ) { |
| 198 | return $this->tryCall( 'zScan', [ $key, &$iterator, $pattern, $count ] ); |
| 199 | } |
| 200 | |
| 201 | /** |
| 202 | * Handle authentication errors and automatically reauthenticate. |
| 203 | * |
| 204 | * @return int self::AUTH_NO_ERROR, self::AUTH_ERROR_TEMPORARY, or self::AUTH_ERROR_PERMANENT |
| 205 | */ |
| 206 | private function checkAuthentication() { |
| 207 | $lastError = $this->conn->getLastError(); |
| 208 | if ( $lastError && preg_match( '/^ERR operation not permitted\b/', $lastError ) ) { |
| 209 | if ( !$this->pool->reauthenticateConnection( $this->server, $this->conn ) ) { |
| 210 | return self::AUTH_ERROR_PERMANENT; |
| 211 | } |
| 212 | $this->conn->clearLastError(); |
| 213 | $this->logger->info( |
| 214 | "Used automatic re-authentication for Redis.", |
| 215 | [ 'redis_server' => $this->server ] |
| 216 | ); |
| 217 | return self::AUTH_ERROR_TEMPORARY; |
| 218 | } |
| 219 | return self::AUTH_NO_ERROR; |
| 220 | } |
| 221 | |
| 222 | /** |
| 223 | * Post Redis call cleanup. |
| 224 | * |
| 225 | * @return void |
| 226 | */ |
| 227 | private function postCallCleanup() { |
| 228 | $this->lastError = $this->conn->getLastError() ?: $this->lastError; |
| 229 | |
| 230 | // Restore original timeout in the case of blocking calls. |
| 231 | $this->pool->resetTimeout( $this->conn ); |
| 232 | } |
| 233 | |
| 234 | /** |
| 235 | * @param string $script |
| 236 | * @param array $params |
| 237 | * @param int $numKeys |
| 238 | * @return mixed |
| 239 | * @throws RedisException |
| 240 | */ |
| 241 | public function luaEval( $script, array $params, $numKeys ) { |
| 242 | $sha1 = sha1( $script ); // 40 char hex |
| 243 | $conn = $this->conn; // convenience |
| 244 | $server = $this->server; // convenience |
| 245 | |
| 246 | // Try to run the server-side cached copy of the script |
| 247 | $conn->clearLastError(); |
| 248 | $res = $conn->evalSha( $sha1, $params, $numKeys ); |
| 249 | // If we got a permission error reply that means that (a) we are not in |
| 250 | // multi()/pipeline() and (b) some connection problem likely occurred. If |
| 251 | // the password the client gave was just wrong, an exception should have |
| 252 | // been thrown back in getConnection() previously. |
| 253 | $lastError = $conn->getLastError(); |
| 254 | if ( $lastError && preg_match( '/^ERR operation not permitted\b/', $lastError ) ) { |
| 255 | $this->pool->reauthenticateConnection( $server, $conn ); |
| 256 | $conn->clearLastError(); |
| 257 | $res = $conn->eval( $script, $params, $numKeys ); |
| 258 | $this->logger->info( |
| 259 | "Used automatic re-authentication for Lua script '$sha1'.", |
| 260 | [ 'redis_server' => $server ] |
| 261 | ); |
| 262 | } |
| 263 | // If the script is not in cache, use eval() to retry and cache it |
| 264 | $lastError = $conn->getLastError(); |
| 265 | if ( $lastError && preg_match( '/^NOSCRIPT/', $lastError ) ) { |
| 266 | $conn->clearLastError(); |
| 267 | $res = $conn->eval( $script, $params, $numKeys ); |
| 268 | $this->logger->info( |
| 269 | "Used eval() for Lua script '$sha1'.", |
| 270 | [ 'redis_server' => $server ] |
| 271 | ); |
| 272 | } |
| 273 | |
| 274 | $lastError = $conn->getLastError(); |
| 275 | if ( $lastError ) { // script bug? |
| 276 | $this->logger->error( |
| 277 | 'Lua script error on server "{redis_server}": {lua_error}', |
| 278 | [ |
| 279 | 'redis_server' => $server, |
| 280 | 'lua_error' => $lastError |
| 281 | ] |
| 282 | ); |
| 283 | $this->lastError = $lastError; |
| 284 | } |
| 285 | |
| 286 | return $res; |
| 287 | } |
| 288 | |
| 289 | /** |
| 290 | * @param Redis $conn |
| 291 | * @return bool |
| 292 | */ |
| 293 | public function isConnIdentical( Redis $conn ) { |
| 294 | return $this->conn === $conn; |
| 295 | } |
| 296 | |
| 297 | public function __destruct() { |
| 298 | $this->pool->freeConnection( $this->server, $this->conn ); |
| 299 | } |
| 300 | } |
| 301 | |
| 302 | /** @deprecated class alias since 1.43 */ |
| 303 | class_alias( RedisConnRef::class, 'RedisConnRef' ); |