Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 76
0.00% covered (danger)
0.00%
0 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
RedisConnRef
0.00% covered (danger)
0.00%
0 / 75
0.00% covered (danger)
0.00%
0 / 16
1056
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 setLogger
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getServer
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLastError
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 clearLastError
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __call
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
42
 tryCall
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 scan
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sScan
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 hScan
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 zScan
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 checkAuthentication
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 postCallCleanup
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 luaEval
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
42
 isConnIdentical
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __destruct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace Wikimedia\ObjectCache;
8
9use Psr\Log\LoggerAwareInterface;
10use Psr\Log\LoggerInterface;
11use Redis;
12use 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 */
25class 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 */
303class_alias( RedisConnRef::class, 'RedisConnRef' );