Completed
Pull Request — 2.x (#36)
by
unknown
02:53 queued 01:04
created

PhpRedisConnector::retryOnFailure()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 19
rs 9.6333
c 0
b 0
f 0
cc 3
nc 2
nop 1
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 $retryLimit = 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 $retryWait = 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
     * Create a new Redis Sentinel connection from the provided configuration
68
     * options
69
     *
70
     * @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...
71
     * @param array $options The global client options shared by all Sentinel
72
     * connections
73
     *
74
     * @return PhpRedisConnection The Sentinel connection containing a configured
75
     * PhpRedis Client
76
     */
77
    public function connect(array $servers, array $options = [ ])
78
    {
79
        // Set the initial Sentinel servers.
80
        $this->servers = array_map(function ($server) {
81
            return $this->formatServer($server);
82
        }, $servers);
83
84
        // Set the connector options.
85
        $this->retryLimit = (int) (isset($options['connector_retry_limit']) ? $options['connector_retry_limit'] : 20);
86
        $this->retryWait = (int) (isset($options['connector_retry_wait']) ? $options['connector_retry_wait'] : 1000);
87
88
        // Merge the global options shared by all Sentinel connections with
89
        // connection-specific options
90
        $clientOpts = array_merge($options, Arr::pull($servers, 'options', [ ]));
91
92
        // Extract the array of Sentinel connection options from the rest of
93
        // the client options
94
        $sentinelOpts = array_intersect_key($clientOpts, $this->sentinelKeys);
95
96
        // Filter the Sentinel connection options elements from the client
97
        // options array
98
        $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...
99
100
        // Create a client by calling the Sentinel servers
101
        $connector = function () use ($options) {
102
            return $this->createClientWithSentinel($options);
103
        };
104
105
        // Create a connection and retry if this fails.
106
        $connection = $this->retryOnFailure(function () use ($connector) {
107
            return $connector();
108
        });
109
110
        return new PhpRedisConnection($connection, $connector, $sentinelOpts);
111
    }
112
113
    /**
114
     * Create the Redis client instance
115
     *
116
     * @param  array  $options
117
     * @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...
118
     *
119
     * @throws LogicException
120
     */
121
    protected function createClientWithSentinel(array $options)
122
    {
123
        $servers = $this->servers;
124
        $service = isset($options['service']) ? $options['service'] : 'mymaster';
125
        $timeout = isset($options['sentinel_timeout']) ? $options['sentinel_timeout'] : 0;
126
        $persistent = isset($options['sentinel_peristent']) ? $options['sentinel_peristent'] : null;
127
        $retryWait = isset($options['retry_wait']) ? $options['retry_wait'] : 0;
128
        $readTimeout = isset($options['sentinel_read_timeout']) ? $options['sentinel_read_timeout'] : 0;
129
        $parameters = isset($options['parameters']) ? $options['parameters'] : [];
130
131
        // Shuffle the servers to perform some loadbalancing.
132
        shuffle($servers);
133
134
        // Check if the redis extension is enabled.
135
        if (! extension_loaded('redis')) {
136
            throw new LogicException('Please make sure the PHP Redis extension is installed and enabled.');
137
        }
138
139
        // Check if the extension is up to date and contains RedisSentinel.
140
        if (! class_exists(RedisSentinel::class)) {
141
            throw new LogicException('Please make sure the PHP Redis extension is up to date.');
142
        }
143
144
        // Try to connect to any of the servers.
145
        foreach ($servers as $idx => $server) {
146
            $host = isset($server['host']) ? $server['host'] : 'localhost';
147
            $port = isset($server['port']) ? $server['port'] : 26739;
148
149
            // Create a connection to the Sentinel instance.
150
            $sentinel = new RedisSentinel($host, $port, $timeout, $persistent, $retryWait, $readTimeout);
151
152
            try {
153
                // Check if the Sentinel server list needs to be updated.
154
                // Put the current server and the other sentinels in the server list.
155
                $updateSentinels = isset($options['update_sentinels']) ? $options['update_sentinels'] : false;
156
                if ($updateSentinels === true) {
157
                    $this->updateSentinels($sentinel, $host, $port, $service);
158
                }
159
160
                // Lookup the master node.
161
                $master = $sentinel->getMasterAddrByName($service);
162
                if (is_array($master) && ! count($master)) {
163
                    throw new RedisException(sprintf('No master found for service "%s".', $service));
164
                }
165
166
                // Create a PhpRedis client for the selected master node.
167
                return $this->createClient(array_merge($parameters, $server, [
168
                    'host' => $master[0],
169
                    'port' => $master[1],
170
                ]));
171
            } catch (RedisException $e) {
172
                // Rethrow the expection when the last server is reached.
173
                if ($idx === count($servers) - 1) {
174
                    throw $e;
175
                }
176
            }
177
        }
178
    }
179
180
    /**
181
     * Retry the callback when a RedisException is catched.
182
     *
183
     * @param callable $callback The operation to execute.
184
     * @return mixed The result of the first successful attempt.
185
     *
186
     * @throws RedisRetryException After exhausting the allowed number of
187
     * attempts to connect.
188
     */
189
    protected function retryOnFailure(callable $callback)
190
    {
191
        $attempts = 0;
192
        $previous = null;
0 ignored issues
show
Unused Code introduced by
$previous 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...
193
194
        do {
195
            try {
196
                return $callback();
197
            } catch (RedisException $exception) {
198
                $previous = $exception;
199
200
                usleep($this->retryWait * 1000);
201
202
                $attempts++;
203
            }
204
        } while ($attempts < $this->retryLimit);
205
206
        throw new RedisRetryException(sprintf('Reached the connect limit of %d attempts', $attempts), 0, $previous);
207
    }
208
209
    /**
210
     * Update the list With sentinel servers.
211
     *
212
     * @param RedisSentinel $sentinel
213
     * @param string $currentHost
214
     * @param int $currentPort
215
     * @param string $service
216
     * @return void
217
     */
218
    protected function updateSentinels(RedisSentinel $sentinel, string $currentHost, int $currentPort, string $service)
219
    {
220
        $this->servers = array_merge(
221
            [
222
                [
223
                    'host' => $currentHost,
224
                    'port' => $currentPort,
225
                ]
226
            ], array_map(function ($sentinel) {
227
                return [
228
                    'host' => $sentinel['ip'],
229
                    'port' => $sentinel['port'],
230
                ];
231
            }, $sentinel->sentinels($service))
232
        );
233
    }
234
235
    /**
236
     * Format a server.
237
     *
238
     * @param mixed $server
239
     * @return array
240
     *
241
     * @throws RedisException
242
     */
243
    protected function formatServer($server)
244
    {
245
        if (is_string($server)) {
246
            list($host, $port) = explode(':', $server);
247
            if (! $host || ! $port) {
248
                throw new RedisException('Could not format the server definition.');
249
            }
250
251
            return ['host' => $host, 'port' => (int) $port];
252
        }
253
254
        if (! is_array($server)) {
255
            throw new RedisException('Could not format the server definition.');
256
        }
257
258
        return $server;
259
    }
260
}
261