Completed
Pull Request — 2.x (#36)
by
unknown
01:42
created

PhpRedisConnector::__construct()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
cc 3
nc 3
nop 0
1
<?php
2
3
namespace Monospice\LaravelRedisSentinel\Connectors;
4
5
use Illuminate\Support\Arr;
6
use Illuminate\Redis\Connectors\PhpRedisConnector as LaravelPhpRedisConnector;
7
use LogicException;
8
use Monospice\LaravelRedisSentinel\Connections\PhpRedisConnection;
9
use Monospice\LaravelRedisSentinel\Exceptions\RedisRetryException;
10
use Redis;
11
use RedisSentinel;
12
use RedisException;
13
14
/**
15
 * Initializes PhpRedis Client instances for Redis Sentinel connections
16
 *
17
 * @category Package
18
 * @package  Monospice\LaravelRedisSentinel
19
 * @author   Jeffrey Zant <[email protected]>
20
 * @license  See LICENSE file
21
 * @link     http://github.com/monospice/laravel-redis-sentinel-drivers
22
 */
23
class PhpRedisConnector extends LaravelPhpRedisConnector
24
{
25
    /**
26
     * Holds the current sentinel servers.
27
     *
28
     * @var array
29
     */
30
    protected $servers;
31
32
    /**
33
     * The number of times the client attempts to retry a command when it fails
34
     * to connect to a Redis instance behind Sentinel.
35
     *
36
     * @var int
37
     */
38
    protected $connectorRetryLimit = 20;
39
40
    /**
41
     * The time in milliseconds to wait before the client retries a failed
42
     * command.
43
     *
44
     * @var int
45
     */
46
    protected $connectorRetryWait = 1000;
47
48
    /**
49
     * Configuration options specific to Sentinel connection operation
50
     *
51
     * Some of the Sentinel configuration options can be entered in this class.
52
     * The retry_wait and retry_limit values are passed to the connection.
53
     *
54
     * @var array
55
     */
56
    protected $sentinelKeys = [
57
        'sentinel_timeout' => null,
58
        'retry_wait' => null,
59
        'retry_limit' => null,
60
        'update_sentinels' => null,
61
62
        'sentinel_persistent' => null,
63
        'sentinel_read_timeout' => null,
64
    ];
65
66
    /**
67
     * Instantiate the connector and check if the required extension is available.
68
     */
69
    public function __construct()
70
    {
71
        if (! extension_loaded('redis')) {
72
            throw new LogicException('Please make sure the PHP Redis extension is installed and enabled.');
73
        }
74
75
        if (! class_exists(RedisSentinel::class)) {
76
            throw new LogicException('Please make sure the PHP Redis extension is up to date (5.3.4 or greater).');
77
        }
78
    }
79
80
    /**
81
     * Create a new Redis Sentinel connection from the provided configuration
82
     * options
83
     *
84
     * @param array $server  The client configuration for the connection
0 ignored issues
show
Documentation introduced by
There is no parameter named $server. Did you maybe mean $servers?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
85
     * @param array $options The global client options shared by all Sentinel
86
     * connections
87
     *
88
     * @return PhpRedisConnection The Sentinel connection containing a configured
89
     * PhpRedis Client
90
     */
91
    public function connect(array $servers, array $options = [ ])
92
    {
93
        // Set the initial Sentinel servers.
94
        $this->servers = array_map(function ($server) {
95
            return $this->formatServer($server);
96
        }, $servers);
97
98
        // Set the connector retry limit.
99
        if (isset($options['connector_retry_limit']) && is_numeric($options['connector_retry_limit'])) {
100
            $this->connectorRetryLimit = (int) $options['connector_retry_limit'];
101
        }
102
103
        // Set the connector retry wait.
104
        if (isset($options['connector_retry_wait']) && is_numeric($options['connector_retry_wait'])) {
105
            $this->connectorRetryWait = (int) $options['connector_retry_wait'];
106
        }
107
108
        // Merge the global options shared by all Sentinel connections with
109
        // connection-specific options
110
        $clientOpts = array_merge($options, Arr::pull($servers, 'options', [ ]));
111
112
        // Extract the array of Sentinel connection options from the rest of
113
        // the client options
114
        $sentinelOpts = array_intersect_key($clientOpts, $this->sentinelKeys);
115
116
        // Filter the Sentinel connection options elements from the client
117
        // options array
118
        $clientOpts = array_diff_key($clientOpts, $this->sentinelKeys);
0 ignored issues
show
Unused Code introduced by
$clientOpts is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
119
120
        // Create a client by calling the Sentinel servers
121
        $connector = function () use ($options) {
122
            return $this->createClientWithSentinel($options);
123
        };
124
125
        // Create a connection and retry if this fails.
126
        $connection = self::retryOnFailure(function () use ($connector) {
127
            return $connector();
128
        }, $this->connectorRetryLimit, $this->connectorRetryWait);
129
130
        return new PhpRedisConnection($connection, $connector, $sentinelOpts);
131
    }
132
133
    /**
134
     * Create the Redis client instance
135
     *
136
     * @param  array  $options
137
     * @return Redis
0 ignored issues
show
Documentation introduced by
Should the return type not be Redis|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
138
     *
139
     * @throws LogicException
140
     */
141
    protected function createClientWithSentinel(array $options)
142
    {
143
        $serverConfigurations = $this->servers;
144
        $clientConfiguration = isset($options['parameters']) ? $options['parameters'] : [];
145
146
        $updateSentinels = isset($options['update_sentinels']) ? $options['update_sentinels'] : false;
147
        $sentinelService = isset($options['service']) ? $options['service'] : 'mymaster';
148
        $sentinelTimeout = isset($options['sentinel_timeout']) ? $options['sentinel_timeout'] : 0;
149
        $sentinelPersistent = isset($options['sentinel_persistent']) ? $options['sentinel_persistent'] : null;
150
        $sentinelReadTimeout = isset($options['sentinel_read_timeout']) ? $options['sentinel_read_timeout'] : 0;
151
152
        // Shuffle the server configurations to perform some loadbalancing.
153
        shuffle($serverConfigurations);
154
155
        // Try to connect to any of the servers.
156
        foreach ($serverConfigurations as $idx => $serverConfiguration) {
157
            $host = isset($serverConfiguration['host']) ? $serverConfiguration['host'] : 'localhost';
158
            $port = isset($serverConfiguration['port']) ? $serverConfiguration['port'] : 26379;
159
160
            // Create a connection to the Sentinel instance. Using a retry_interval of 0, retrying
161
            // is done inside the PhpRedisConnection. Cannot seem to get the retry_interval to work:
162
            // https://github.com/phpredis/phpredis/blob/37a90257d09b4efa75230769cf535484116b2b67/library.c#L343
163
            $sentinel = new RedisSentinel($host, $port, $sentinelTimeout, $sentinelPersistent, 0, $sentinelReadTimeout);
164
165
            try {
166
                // Check if the Sentinel server list needs to be updated.
167
                // Put the current server and the other sentinels in the server list.
168
                if ($updateSentinels === true) {
169
                    $this->updateSentinels($sentinel, $host, $port, $sentinelService);
170
                }
171
172
                // Lookup the master node.
173
                $master = $sentinel->getMasterAddrByName($sentinelService);
174
                if (is_array($master) && ! count($master)) {
175
                    throw new RedisException(sprintf('No master found for service "%s".', $sentinelService));
176
                }
177
178
                // Create a PhpRedis client for the selected master node.
179
                return $this->createClient(array_merge($clientConfiguration, $serverConfiguration, [
180
                    'host' => $master[0],
181
                    'port' => $master[1],
182
                ]));
183
            } catch (RedisException $e) {
184
                // Rethrow the expection when the last server is reached.
185
                if ($idx === count($serverConfigurations) - 1) {
186
                    throw $e;
187
                }
188
            }
189
        }
190
    }
191
192
    /**
193
     * Update the list With sentinel servers.
194
     *
195
     * @param RedisSentinel $sentinel
196
     * @param string $currentHost
197
     * @param int $currentPort
198
     * @param string $service
199
     * @return void
200
     */
201
    protected function updateSentinels(RedisSentinel $sentinel, string $currentHost, int $currentPort, string $service)
202
    {
203
        $this->servers = array_merge(
204
            [
205
                [
206
                    'host' => $currentHost,
207
                    'port' => $currentPort,
208
                ]
209
            ], array_map(function ($sentinel) {
210
                return [
211
                    'host' => $sentinel['ip'],
212
                    'port' => $sentinel['port'],
213
                ];
214
            }, $sentinel->sentinels($service))
215
        );
216
    }
217
218
    /**
219
     * Format a server.
220
     *
221
     * @param mixed $server
222
     * @return array
223
     *
224
     * @throws RedisException
225
     */
226
    protected function formatServer($server)
227
    {
228
        if (is_string($server)) {
229
            list($host, $port) = explode(':', $server);
230
            if (! $host || ! $port) {
231
                throw new RedisException('Could not format the server definition.');
232
            }
233
234
            return ['host' => $host, 'port' => (int) $port];
235
        }
236
237
        if (! is_array($server)) {
238
            throw new RedisException('Could not format the server definition.');
239
        }
240
241
        return $server;
242
    }
243
244
    /**
245
     * Retry the callback when a RedisException is catched.
246
     *
247
     * @param callable $callback The operation to execute.
248
     * @param int $retryLimit The number of times the retry is performed.
249
     * @param int $retryWait The time in milliseconds to wait before retrying again.
250
     * @param callable $failureCallback The operation to execute when a failure happens.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $failureCallback not be null|callable? Also, consider making the array more specific, something like array<String>, or String[].

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
251
     * @return mixed The result of the first successful attempt.
252
     *
253
     * @throws RedisRetryException After exhausting the allowed number of
254
     * attempts to connect.
255
     */
256
    public static function retryOnFailure(callable $callback, int $retryLimit, int $retryWait, callable $failureCallback = null)
257
    {
258
        $attempts = 0;
259
        $previousException = null;
260
261
        while ($attempts < $retryLimit) {
262
            try {
263
                return $callback();
264
            } catch (RedisException $exception) {
265
                $previousException = $exception;
266
267
                if ($failureCallback) {
268
                    call_user_func($failureCallback);
269
                }
270
271
                usleep($retryWait * 1000);
272
273
                $attempts++;
274
            }
275
        }
276
277
        throw new RedisRetryException(sprintf('Reached the (re)connect limit of %d attempts', $attempts), 0, $previousException);
278
    }
279
}
280