Completed
Push — 2.x ( 341981...b9f40f )
by Cy
01:39
created

PredisConnection   A

Complexity

Total Complexity 11

Size/Duplication

Total Lines 170
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
wmc 11
lcom 1
cbo 5
dl 0
loc 170
rs 10
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 2
A setSentinelTimeout() 0 6 1
A setRetryLimit() 0 7 1
A setRetryWait() 0 7 1
A setUpdateSentinels() 0 6 1
A transaction() 0 6 1
A retryOnFailure() 0 19 3
A getMaster() 0 4 1
1
<?php
2
3
namespace Monospice\LaravelRedisSentinel\Connections;
4
5
use Illuminate\Redis\Connections\PredisConnection as LaravelPredisConnection;
6
use Monospice\SpicyIdentifiers\DynamicMethod;
7
use Predis\ClientInterface as Client;
8
use Predis\CommunicationException;
9
10
/**
11
 * Executes Redis commands using the Predis client.
12
 *
13
 * This package extends Laravel's PredisConnection class to work around issues
14
 * experienced when using the Predis client to send commands over "aggregate"
15
 * connections (in this case, Sentinel connections).
16
 *
17
 * @category Package
18
 * @package  Monospice\LaravelRedisSentinel
19
 * @author   @pdbreen, Cy Rossignol <[email protected]>
20
 * @license  See LICENSE file
21
 * @link     https://github.com/monospice/laravel-redis-sentinel-drivers
22
 */
23
class PredisConnection extends LaravelPredisConnection
24
{
25
    /**
26
     * The number of times the client attempts to retry a command when it fails
27
     * to connect to a Redis instance behind Sentinel.
28
     *
29
     * @var int
30
     */
31
    protected $retryLimit = 20;
32
33
    /**
34
     * The time in milliseconds to wait before the client retries a failed
35
     * command.
36
     *
37
     * @var int
38
     */
39
    protected $retryWait = 1000;
40
41
    /**
42
     * Create a Redis Sentinel connection using a Predis client.
43
     *
44
     * @param Client $client          The Redis client to wrap.
45
     * @param array  $sentinelOptions Sentinel-specific connection options.
46
     */
47
    public function __construct(Client $client, array $sentinelOptions = [ ])
48
    {
49
        parent::__construct($client);
0 ignored issues
show
Compatibility introduced by
$client of type object<Predis\ClientInterface> is not a sub-type of object<Predis\Client>. It seems like you assume a concrete implementation of the interface Predis\ClientInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
50
51
        // Set the Sentinel-specific connection options on the Predis Client
52
        // connection and the current instance of this class.
53
        foreach ($sentinelOptions as $option => $value) {
54
            DynamicMethod::parseFromUnderscore($option)
55
                ->prepend('set')
56
                ->callOn($this, [ $value ]);
57
        }
58
    }
59
60
    /**
61
     * Set the default amount of time to wait before determining that a
62
     * connection attempt to a Sentinel server failed.
63
     *
64
     * @param float $seconds The timeout value in seconds.
65
     *
66
     * @return $this The current instance for method chaining.
67
     */
68
    public function setSentinelTimeout($seconds)
69
    {
70
        $this->client->getConnection()->setSentinelTimeout($seconds);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Predis\Connection\ConnectionInterface as the method setSentinelTimeout() does only exist in the following implementations of said interface: Predis\Connection\Aggregate\SentinelReplication.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
71
72
        return $this;
73
    }
74
75
    /**
76
     * Set the default number of attempts to retry a command when the client
77
     * fails to connect to a Redis instance behind Sentinel.
78
     *
79
     * @param int $attempts With a value of 0, throw an exception after the
80
     * first failed attempt. Pass a value of -1 to retry connections forever.
81
     *
82
     * @return $this The current instance for method chaining.
83
     */
84
    public function setRetryLimit($attempts)
85
    {
86
        $this->retryLimit = (int) $attempts;
87
        $this->client->getConnection()->setRetryLimit($attempts);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Predis\Connection\ConnectionInterface as the method setRetryLimit() does only exist in the following implementations of said interface: Predis\Connection\Aggregate\RedisCluster, Predis\Connection\Aggregate\SentinelReplication.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
88
89
        return $this;
90
    }
91
92
    /**
93
     * Set the time to wait before retrying a command after a connection
94
     * attempt failed.
95
     *
96
     * @param int $milliseconds The wait time in milliseconds. When 0, retry
97
     * a failed command immediately.
98
     *
99
     * @return $this The current instance for method chaining.
100
     */
101
    public function setRetryWait($milliseconds)
102
    {
103
        $this->retryWait = (int) $milliseconds;
104
        $this->client->getConnection()->setRetryWait($milliseconds);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Predis\Connection\ConnectionInterface as the method setRetryWait() does only exist in the following implementations of said interface: Predis\Connection\Aggregate\SentinelReplication.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
105
106
        return $this;
107
    }
108
109
    /**
110
     * Set whether the client should update the list of known Sentinels each
111
     * time it needs to connect to a Redis server behind Sentinel.
112
     *
113
     * @param bool $enable If TRUE, fetch the updated Sentinel list.
114
     *
115
     * @return $this The current instance for method chaining.
116
     */
117
    public function setUpdateSentinels($enable)
118
    {
119
        $this->client->getConnection()->setUpdateSentinels($enable);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Predis\Connection\ConnectionInterface as the method setUpdateSentinels() does only exist in the following implementations of said interface: Predis\Connection\Aggregate\SentinelReplication.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
120
121
        return $this;
122
    }
123
124
    /**
125
     * Execute commands in a transaction.
126
     *
127
     * This package overrides the transaction() method to work around a
128
     * limitation in the Predis API that disallows transactions on "aggregate"
129
     * connections like Sentinel. Note that transactions execute on the Redis
130
     * master instance.
131
     *
132
     * @param callable|null $callback Contains the Redis commands to execute in
133
     * the transaction. The callback receives a Predis\Transaction\MultiExec
134
     * transaction abstraction as the only argument. We use this object to
135
     * execute Redis commands by calling its methods just like we would with
136
     * the Laravel Redis service.
137
     *
138
     * @return array|Predis\Transaction\MultiExec An array containing the
139
     * result for each command executed during the transaction. If no callback
140
     * provided, returns an instance of the Predis transaction abstraction.
141
     */
142
    public function transaction(callable $callback = null)
143
    {
144
        return $this->retryOnFailure(function () use ($callback) {
145
            return $this->getMaster()->transaction($callback);
146
        });
147
    }
148
149
    /**
150
     * Attempt to retry the provided operation when the client fails to connect
151
     * to a Redis server.
152
     *
153
     * We adapt Predis' Sentinel connection failure handling logic here to
154
     * reproduce the high-availability mode provided by the actual client. To
155
     * work around "aggregate" connection limitations in Predis, this class
156
     * provides methods that don't use the high-level Sentinel connection API
157
     * of Predis directly, so it needs to handle connection failures itself.
158
     *
159
     * @param callable $callback The operation to execute.
160
     *
161
     * @return mixed The result of the first successful attempt.
162
     */
163
    protected function retryOnFailure(callable $callback)
164
    {
165
        $attempts = 0;
166
167
        do {
168
            try {
169
                return $callback();
170
            } catch (CommunicationException $exception) {
171
                $exception->getConnection()->disconnect();
172
                $this->client->getConnection()->querySentinel();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Predis\Connection\ConnectionInterface as the method querySentinel() does only exist in the following implementations of said interface: Predis\Connection\Aggregate\SentinelReplication.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
173
174
                usleep($this->retryWait * 1000);
175
176
                $attempts++;
177
            }
178
        } while ($attempts <= $this->retryLimit);
179
180
        throw $exception;
181
    }
182
183
    /**
184
     * Get a Predis client instance for the master.
185
     *
186
     * @return Client The client instance for the current master.
187
     */
188
    protected function getMaster()
189
    {
190
        return $this->client->getClientFor('master');
191
    }
192
}
193