• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

predis / predis / 22723904332

05 Mar 2026 03:01PM UTC coverage: 92.905% (+0.01%) from 92.892%
22723904332

push

github

web-flow
Fix sentinel discovery methods not catching StreamInitException (#1650)

* Fix sentinel discovery methods not catching StreamInitException

The getMaster(), getSlaves(), and updateSentinels() methods only catch
ConnectionException when connecting to Sentinel nodes. StreamInitException
(thrown by stream_socket_client() failures) extends PredisException directly,
not ConnectionException, so it propagates uncaught — preventing fallback to
the next Sentinel node.

This causes complete application failure when any single Sentinel is
unreachable, even if other Sentinels are healthy.

Add StreamInitException to the catch clause using a union type in all three
methods. This is more precise than catching PredisException, which would
also swallow ServerException (e.g. "ERR No such master with that name")
and mask configuration errors.

See also #1577 which applied a similar fix to retryCommandOnFailure().

* Fix coding standards and add changelog entry

- Remove spaces around | in union catch (php-cs-fixer)
- Add CHANGELOG.md entry for #1650

* Codestlye fixes

---------

Co-authored-by: Wolfgang Kerschbaumer <wolfgang.kerschbaumer@bergfex.at>
Co-authored-by: vladvildanov <vladyslav.vildanov@redis.com>

3 of 3 new or added lines in 1 file covered. (100.0%)

20 existing lines in 1 file now uncovered.

8328 of 8964 relevant lines covered (92.9%)

113.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.76
/src/Connection/Replication/SentinelReplication.php
1
<?php
2

3
/*
4
 * This file is part of the Predis package.
5
 *
6
 * (c) 2009-2020 Daniele Alessandri
7
 * (c) 2021-2026 Till Krüss
8
 *
9
 * For the full copyright and license information, please view the LICENSE
10
 * file that was distributed with this source code.
11
 */
12

13
namespace Predis\Connection\Replication;
14

15
use InvalidArgumentException;
16
use Predis\Command\Command;
17
use Predis\Command\CommandInterface;
18
use Predis\Command\RawCommand;
19
use Predis\CommunicationException;
20
use Predis\Connection\AbstractAggregateConnection;
21
use Predis\Connection\ConnectionException;
22
use Predis\Connection\FactoryInterface as ConnectionFactoryInterface;
23
use Predis\Connection\NodeConnectionInterface;
24
use Predis\Connection\Parameters;
25
use Predis\Connection\ParametersInterface;
26
use Predis\Connection\RelayFactory;
27
use Predis\Connection\Resource\Exception\StreamInitException;
28
use Predis\Replication\ReplicationStrategy;
29
use Predis\Replication\RoleException;
30
use Predis\Response\Error;
31
use Predis\Response\ErrorInterface as ErrorResponseInterface;
32
use Predis\Response\ServerException;
33
use Predis\Retry\Retry;
34
use Predis\Retry\Strategy\ExponentialBackoff;
35
use Throwable;
36

37
/**
38
 * @author Daniele Alessandri <suppakilla@gmail.com>
39
 * @author Ville Mattila <ville@eventio.fi>
40
 */
41
class SentinelReplication extends AbstractAggregateConnection implements ReplicationInterface
42
{
43
    /**
44
     * @var NodeConnectionInterface
45
     */
46
    protected $master;
47

48
    /**
49
     * @var NodeConnectionInterface[]
50
     */
51
    protected $slaves = [];
52

53
    /**
54
     * @var NodeConnectionInterface[]
55
     */
56
    protected $pool = [];
57

58
    /**
59
     * @var NodeConnectionInterface
60
     */
61
    protected $current;
62

63
    /**
64
     * @var string
65
     */
66
    protected $service;
67

68
    /**
69
     * @var ConnectionFactoryInterface
70
     */
71
    protected $connectionFactory;
72

73
    /**
74
     * @var ReplicationStrategy
75
     */
76
    protected $strategy;
77

78
    /**
79
     * Sentinel connection parameters.
80
     *
81
     * Can contain:
82
     * - String URIs (e.g., "tcp://127.0.0.1:26379")
83
     * - Arrays of connection parameters (e.g., ['host' => '127.0.0.1', 'port' => 26379])
84
     * - ParametersInterface objects
85
     * - NodeConnectionInterface objects
86
     *
87
     * @var array<string|array|ParametersInterface|NodeConnectionInterface>
88
     */
89
    protected $sentinels = [];
90

91
    /**
92
     * @var int
93
     */
94
    protected $sentinelIndex = 0;
95

96
    /**
97
     * @var NodeConnectionInterface
98
     */
99
    protected $sentinelConnection;
100

101
    /**
102
     * @var float
103
     */
104
    protected $sentinelTimeout = 0.100;
105

106
    /**
107
     * Max number of automatic retries of commands upon server failure.
108
     *
109
     * -1 = unlimited retry attempts
110
     *  0 = no retry attempts (fails immediately)
111
     *  n = fail only after n retry attempts
112
     *
113
     * @var int
114
     */
115
    protected $retryLimit = 20;
116

117
    /**
118
     * Time to wait in milliseconds before fetching a new configuration from one
119
     * of the sentinel servers.
120
     *
121
     * @var int
122
     */
123
    protected $retryWait = 1000;
124

125
    /**
126
     * Flag for automatic fetching of available sentinels.
127
     *
128
     * @var bool
129
     */
130
    protected $updateSentinels = false;
131

132
    /**
133
     * @param string                     $service           Name of the service for autodiscovery.
134
     * @param array                      $sentinels         Sentinel servers connection parameters.
135
     * @param ConnectionFactoryInterface $connectionFactory Connection factory instance.
136
     * @param ReplicationStrategy|null   $strategy          Replication strategy instance.
137
     */
138
    public function __construct(
61✔
139
        $service,
140
        array $sentinels,
141
        ConnectionFactoryInterface $connectionFactory,
142
        ?ReplicationStrategy $strategy = null
143
    ) {
144
        $this->sentinels = $sentinels;
61✔
145
        $this->service = $service;
61✔
146
        $this->connectionFactory = $connectionFactory;
61✔
147
        $this->strategy = $strategy ?: new ReplicationStrategy();
61✔
148
    }
149

150
    /**
151
     * Sets a default timeout for connections to sentinels.
152
     *
153
     * When "timeout" is present in the connection parameters of sentinels, its
154
     * value overrides the default sentinel timeout.
155
     *
156
     * @param float $timeout Timeout value.
157
     */
UNCOV
158
    public function setSentinelTimeout($timeout)
×
159
    {
UNCOV
160
        $this->sentinelTimeout = (float) $timeout;
×
161
    }
162

163
    /**
164
     * Sets the maximum number of retries for commands upon server failure.
165
     *
166
     * -1 = unlimited retry attempts
167
     *  0 = no retry attempts (fails immediately)
168
     *  n = fail only after n retry attempts
169
     *
170
     * @param int $retry Number of retry attempts.
171
     */
UNCOV
172
    public function setRetryLimit($retry)
×
173
    {
UNCOV
174
        $this->retryLimit = (int) $retry;
×
175
    }
176

177
    /**
178
     * Sets the time to wait (in milliseconds) before fetching a new configuration
179
     * from one of the sentinels.
180
     *
181
     * @param float $milliseconds Time to wait before the next attempt.
182
     */
183
    public function setRetryWait($milliseconds)
54✔
184
    {
185
        $this->retryWait = (float) $milliseconds;
54✔
186
    }
187

188
    /**
189
     * Set automatic fetching of available sentinels.
190
     *
191
     * @param bool $update Enable or disable automatic updates.
192
     */
UNCOV
193
    public function setUpdateSentinels($update)
×
194
    {
UNCOV
195
        $this->updateSentinels = (bool) $update;
×
196
    }
197

198
    /**
199
     * Resets the current connection.
200
     */
201
    protected function reset()
38✔
202
    {
203
        $this->current = null;
38✔
204
    }
205

206
    /**
207
     * Wipes the current list of master and slaves nodes.
208
     */
209
    protected function wipeServerList()
9✔
210
    {
211
        $this->reset();
9✔
212

213
        $this->master = null;
9✔
214
        $this->slaves = [];
9✔
215
        $this->pool = [];
9✔
216
    }
217

218
    /**
219
     * {@inheritdoc}
220
     */
221
    public function add(NodeConnectionInterface $connection)
38✔
222
    {
223
        $parameters = $connection->getParameters();
38✔
224
        $role = $parameters->role;
38✔
225

226
        if ('master' === $role) {
38✔
227
            $this->master = $connection;
35✔
228
        } elseif ('sentinel' === $role) {
26✔
229
            $this->sentinels[] = $connection;
2✔
230

231
            // sentinels are not considered part of the pool.
232
            return;
2✔
233
        } else {
234
            // everything else is considered a slave.
235
            $this->slaves[] = $connection;
26✔
236
        }
237

238
        $this->pool[(string) $connection] = $connection;
38✔
239

240
        $this->reset();
38✔
241
    }
242

243
    /**
244
     * {@inheritdoc}
245
     */
246
    public function remove(NodeConnectionInterface $connection)
1✔
247
    {
248
        if ($connection === $this->master) {
1✔
249
            $this->master = null;
×
250
        } elseif (false !== $id = array_search($connection, $this->slaves, true)) {
1✔
251
            unset($this->slaves[$id]);
1✔
252
        } elseif (false !== $id = array_search($connection, $this->sentinels, true)) {
1✔
253
            unset($this->sentinels[$id]);
1✔
254

255
            return true;
1✔
256
        } else {
UNCOV
257
            return false;
×
258
        }
259

260
        unset($this->pool[(string) $connection]);
1✔
261

262
        $this->reset();
1✔
263

264
        return true;
1✔
265
    }
266

267
    /**
268
     * Creates a new connection to a sentinel server.
269
     *
270
     * @return NodeConnectionInterface
271
     */
272
    protected function createSentinelConnection($parameters)
33✔
273
    {
274
        if ($parameters instanceof NodeConnectionInterface) {
33✔
275
            return $parameters;
25✔
276
        }
277

278
        if (is_string($parameters)) {
8✔
279
            $parameters = Parameters::parse($parameters);
6✔
280
        }
281

282
        if (is_array($parameters)) {
8✔
283
            // NOTE: sentinels do not accept SELECT command so we must
284
            // explicitly set it to NULL to avoid problems when using default
285
            // parameters set via client options.
286
            $parameters['database'] = null;
6✔
287

288
            // don't leak password from between configurations
289
            // https://github.com/predis/predis/pull/807/#discussion_r985764770
290
            if (!isset($parameters['password'])) {
6✔
291
                $parameters['password'] = null;
4✔
292
            }
293

294
            if (!isset($parameters['timeout'])) {
6✔
295
                $parameters['timeout'] = $this->sentinelTimeout;
5✔
296
            }
297
        }
298

299
        return $this->connectionFactory->create($parameters);
8✔
300
    }
301

302
    /**
303
     * Returns the current sentinel connection.
304
     *
305
     * If there is no active sentinel connection, a new connection is created.
306
     *
307
     * @return NodeConnectionInterface
308
     */
309
    public function getSentinelConnection()
35✔
310
    {
311
        if (!$this->sentinelConnection) {
35✔
312
            if ($this->sentinelIndex >= count($this->sentinels)) {
35✔
313
                $this->sentinelIndex = 0;
7✔
314
                throw new \Predis\ClientException('No sentinel server available for autodiscovery.');
7✔
315
            }
316

317
            $sentinel = $this->sentinels[$this->sentinelIndex];
33✔
318
            ++$this->sentinelIndex;
33✔
319
            $this->sentinelConnection = $this->createSentinelConnection($sentinel);
33✔
320
        }
321

322
        return $this->sentinelConnection;
33✔
323
    }
324

325
    /**
326
     * Fetches an updated list of sentinels from a sentinel.
327
     */
328
    public function updateSentinels()
7✔
329
    {
330
        SENTINEL_QUERY: {
7✔
331
            $sentinel = $this->getSentinelConnection();
7✔
332

333
            try {
7✔
334
                $payload = $sentinel->executeCommand(
7✔
335
                    RawCommand::create('SENTINEL', 'sentinels', $this->service)
7✔
336
                );
7✔
337

338
                $this->sentinels = [];
7✔
339
                $this->sentinelIndex = 0;
7✔
340
                // NOTE: sentinel server does not return itself, so we add it back.
341
                $this->sentinels[] = $sentinel->getParameters()->toArray();
7✔
342

343
                foreach ($payload as $sentinel) {
6✔
344
                    $this->sentinels[] = [
7✔
345
                        'host' => $sentinel[3],
7✔
346
                        'port' => $sentinel[5],
7✔
347
                        'role' => 'sentinel',
7✔
348
                    ];
7✔
349
                }
7✔
350
            } catch (ConnectionException|StreamInitException $exception) {
3✔
351
                $this->sentinelConnection = null;
7✔
352

353
                goto SENTINEL_QUERY;
7✔
354
            }
7✔
355
        }
7✔
356
    }
357

358
    /**
359
     * Fetches the details for the master and slave servers from a sentinel.
360
     */
361
    public function querySentinel()
2✔
362
    {
363
        $this->wipeServerList();
2✔
364

365
        $this->updateSentinels();
2✔
366
        $this->getMaster();
2✔
367
        $this->getSlaves();
2✔
368
    }
369

370
    /**
371
     * Handles error responses returned by redis-sentinel.
372
     *
373
     * @param NodeConnectionInterface $sentinel Connection to a sentinel server.
374
     * @param ErrorResponseInterface  $error    Error response.
375
     */
UNCOV
376
    private function handleSentinelErrorResponse(NodeConnectionInterface $sentinel, ErrorResponseInterface $error)
×
377
    {
UNCOV
378
        if ($error->getErrorType() === 'IDONTKNOW') {
×
UNCOV
379
            throw new ConnectionException($sentinel, $error->getMessage());
×
380
        }
UNCOV
381
        throw new ServerException($error->getMessage());
×
382
    }
383

384
    /**
385
     * Fetches the details for the master server from a sentinel.
386
     *
387
     * @param NodeConnectionInterface $sentinel Connection to a sentinel server.
388
     * @param string                  $service  Name of the service.
389
     *
390
     * @return array
391
     */
392
    protected function querySentinelForMaster(NodeConnectionInterface $sentinel, $service)
12✔
393
    {
394
        $payload = $sentinel->executeCommand(
12✔
395
            RawCommand::create('SENTINEL', 'get-master-addr-by-name', $service)
12✔
396
        );
12✔
397

398
        if ($payload === null) {
10✔
399
            throw new ServerException('ERR No such master with that name');
1✔
400
        }
401

402
        if ($payload instanceof ErrorResponseInterface) {
9✔
UNCOV
403
            $this->handleSentinelErrorResponse($sentinel, $payload);
×
404
        }
405

406
        return [
9✔
407
            'host' => $payload[0],
9✔
408
            'port' => $payload[1],
9✔
409
            'role' => 'master',
9✔
410
        ];
9✔
411
    }
412

413
    /**
414
     * Fetches the details for the slave servers from a sentinel.
415
     *
416
     * @param NodeConnectionInterface $sentinel Connection to a sentinel server.
417
     * @param string                  $service  Name of the service.
418
     *
419
     * @return array
420
     */
421
    protected function querySentinelForSlaves(NodeConnectionInterface $sentinel, $service)
13✔
422
    {
423
        $slaves = [];
13✔
424

425
        $payload = $sentinel->executeCommand(
13✔
426
            RawCommand::create('SENTINEL', 'slaves', $service)
13✔
427
        );
13✔
428

429
        if ($payload instanceof ErrorResponseInterface) {
12✔
UNCOV
430
            $this->handleSentinelErrorResponse($sentinel, $payload);
×
431
        }
432

433
        foreach ($payload as $slave) {
12✔
434
            $flags = explode(',', $slave[9]);
8✔
435

436
            if (array_intersect($flags, ['s_down', 'o_down', 'disconnected'])) {
8✔
437
                continue;
1✔
438
            }
439

440
            // ensure `master-link-status` is ok
441
            if (isset($slave[31]) && $slave[31] === 'err') {
7✔
UNCOV
442
                continue;
×
443
            }
444

445
            $slaves[] = [
7✔
446
                'host' => $slave[3],
7✔
447
                'port' => $slave[5],
7✔
448
                'role' => 'slave',
7✔
449
            ];
7✔
450
        }
451

452
        return $slaves;
12✔
453
    }
454

455
    /**
456
     * {@inheritdoc}
457
     */
458
    public function getCurrent()
4✔
459
    {
460
        return $this->current;
4✔
461
    }
462

463
    /**
464
     * {@inheritdoc}
465
     */
466
    public function getMaster()
22✔
467
    {
468
        if ($this->master) {
22✔
469
            return $this->master;
16✔
470
        }
471

472
        if ($this->updateSentinels) {
12✔
UNCOV
473
            $this->updateSentinels();
×
474
        }
475

476
        SENTINEL_QUERY: {
12✔
477
            $sentinel = $this->getSentinelConnection();
12✔
478

479
            try {
12✔
480
                $masterParameters = $this->querySentinelForMaster($sentinel, $this->service);
12✔
481
                $masterConnection = $this->connectionFactory->create($masterParameters);
12✔
482

483
                $this->add($masterConnection);
12✔
484
            } catch (ConnectionException|StreamInitException $exception) {
3✔
485
                $this->sentinelConnection = null;
12✔
486

487
                goto SENTINEL_QUERY;
12✔
488
            }
12✔
489
        }
12✔
490

491
        return $masterConnection;
9✔
492
    }
493

494
    /**
495
     * {@inheritdoc}
496
     */
497
    public function getSlaves()
23✔
498
    {
499
        if ($this->slaves) {
23✔
500
            return array_values($this->slaves);
13✔
501
        }
502

503
        if ($this->updateSentinels) {
14✔
UNCOV
504
            $this->updateSentinels();
×
505
        }
506

507
        SENTINEL_QUERY: {
14✔
508
            $sentinel = $this->getSentinelConnection();
14✔
509

510
            try {
14✔
511
                $slavesParameters = $this->querySentinelForSlaves($sentinel, $this->service);
14✔
512

513
                foreach ($slavesParameters as $slaveParameters) {
12✔
514
                    $this->add($this->connectionFactory->create($slaveParameters));
14✔
515
                }
14✔
516
            } catch (ConnectionException|StreamInitException $exception) {
1✔
517
                $this->sentinelConnection = null;
14✔
518

519
                goto SENTINEL_QUERY;
14✔
520
            }
14✔
521
        }
14✔
522

523
        return array_values($this->slaves);
12✔
524
    }
525

526
    /**
527
     * Returns a random slave.
528
     *
529
     * @return NodeConnectionInterface|null
530
     */
531
    protected function pickSlave()
17✔
532
    {
533
        $slaves = $this->getSlaves();
17✔
534

535
        return $slaves
16✔
536
            ? $slaves[rand(1, count($slaves)) - 1]
12✔
537
            : null;
16✔
538
    }
539

540
    /**
541
     * Returns the connection instance in charge for the given command.
542
     *
543
     * @param CommandInterface $command Command instance.
544
     *
545
     * @return NodeConnectionInterface
546
     */
547
    private function getConnectionInternal(CommandInterface $command)
16✔
548
    {
549
        if (!$this->current) {
16✔
550
            if ($this->strategy->isReadOperation($command) && $slave = $this->pickSlave()) {
14✔
551
                $this->current = $slave;
6✔
552
            } else {
553
                $this->current = $this->getMaster();
8✔
554
            }
555

556
            return $this->current;
14✔
557
        }
558

559
        if ($this->current === $this->master) {
7✔
560
            return $this->current;
5✔
561
        }
562

563
        if (!$this->strategy->isReadOperation($command)) {
3✔
564
            $this->current = $this->getMaster();
2✔
565
        }
566

567
        return $this->current;
3✔
568
    }
569

570
    /**
571
     * Asserts that the specified connection matches an expected role.
572
     *
573
     * @param NodeConnectionInterface $connection Connection to a redis server.
574
     * @param string                  $role       Expected role of the server ("master", "slave" or "sentinel").
575
     *
576
     * @throws RoleException|ConnectionException
577
     */
578
    protected function assertConnectionRole(NodeConnectionInterface $connection, $role)
6✔
579
    {
580
        $role = strtolower($role);
6✔
581
        $retry = $connection->getParameters()->retry;
6✔
582
        $actualRole = $retry->callWithRetry(function () use ($connection) {
6✔
583
            return $connection->executeCommand(RawCommand::create('ROLE'));
6✔
584
        });
6✔
585

586
        if ($actualRole instanceof Error) {
6✔
UNCOV
587
            throw new ConnectionException($connection, $actualRole->getMessage());
×
588
        }
589

590
        if ($role !== $actualRole[0]) {
6✔
591
            throw new RoleException($connection, "Expected $role but got $actualRole[0] [$connection]");
1✔
592
        }
593
    }
594

595
    /**
596
     * {@inheritdoc}
597
     */
598
    public function getConnectionByCommand(CommandInterface $command)
16✔
599
    {
600
        $connection = $this->getConnectionInternal($command);
16✔
601

602
        if (!$connection->isConnected()) {
16✔
603
            // When we do not have any available slave in the pool we can expect
604
            // read-only operations to hit the master server.
605
            $expectedRole = $this->strategy->isReadOperation($command) && $this->slaves ? 'slave' : 'master';
6✔
606
            $this->assertConnectionRole($connection, $expectedRole);
6✔
607
        }
608

609
        return $connection;
15✔
610
    }
611

612
    /**
613
     * {@inheritdoc}
614
     */
615
    public function getConnectionById($id)
5✔
616
    {
617
        return $this->pool[$id] ?? null;
5✔
618
    }
619

620
    /**
621
     * Returns a connection by its role.
622
     *
623
     * @param string $role Connection role (`master`, `slave` or `sentinel`)
624
     *
625
     * @return NodeConnectionInterface|null
626
     */
627
    public function getConnectionByRole($role)
5✔
628
    {
629
        if ($role === 'master') {
5✔
630
            return $this->getMaster();
3✔
631
        } elseif ($role === 'slave') {
4✔
632
            return $this->pickSlave();
3✔
633
        } elseif ($role === 'sentinel') {
3✔
634
            return $this->getSentinelConnection();
2✔
635
        }
636

637
        return null;
1✔
638
    }
639

640
    /**
641
     * Switches the internal connection in use by the backend.
642
     *
643
     * Sentinel connections are not considered as part of the pool, meaning that
644
     * trying to switch to a sentinel will throw an exception.
645
     *
646
     * @param NodeConnectionInterface $connection Connection instance in the pool.
647
     */
648
    public function switchTo(NodeConnectionInterface $connection)
4✔
649
    {
650
        if ($connection && $connection === $this->current) {
4✔
UNCOV
651
            return;
×
652
        }
653

654
        if ($connection !== $this->master && !in_array($connection, $this->slaves, true)) {
4✔
655
            throw new InvalidArgumentException('Invalid connection or connection not found.');
1✔
656
        }
657

658
        $connection->connect();
3✔
659

660
        if ($this->current) {
3✔
661
            $this->current->disconnect();
1✔
662
        }
663

664
        $this->current = $connection;
3✔
665
    }
666

667
    /**
668
     * {@inheritdoc}
669
     */
670
    public function switchToMaster()
1✔
671
    {
672
        $connection = $this->getConnectionByRole('master');
1✔
673
        $this->switchTo($connection);
1✔
674
    }
675

676
    /**
677
     * {@inheritdoc}
678
     */
679
    public function switchToSlave()
1✔
680
    {
681
        $connection = $this->getConnectionByRole('slave');
1✔
682
        $this->switchTo($connection);
1✔
683
    }
684

685
    /**
686
     * {@inheritdoc}
687
     */
688
    public function isConnected()
1✔
689
    {
690
        return $this->current ? $this->current->isConnected() : false;
1✔
691
    }
692

693
    /**
694
     * {@inheritdoc}
695
     */
696
    public function connect()
7✔
697
    {
698
        if (!$this->current) {
7✔
699
            if (!$this->current = $this->pickSlave()) {
7✔
700
                $this->current = $this->getMaster();
3✔
701
            }
702
        }
703

704
        $this->current->connect();
6✔
705
    }
706

707
    /**
708
     * {@inheritdoc}
709
     */
710
    public function disconnect()
1✔
711
    {
712
        foreach ($this->pool as $connection) {
1✔
713
            $connection->disconnect();
1✔
714
        }
715
    }
716

717
    /**
718
     * Retries the execution of a command upon server failure after asking a new
719
     * configuration to one of the sentinels.
720
     *
721
     * @param CommandInterface $command Command instance.
722
     * @param string           $method  Actual method.
723
     *
724
     * @return mixed
725
     */
726
    private function retryCommandOnFailure(CommandInterface $command, $method)
10✔
727
    {
728
        $parameters = $this->getParameters();
10✔
729

730
        if ($parameters->isDisabledRetry() || $this->connectionFactory instanceof RelayFactory) {
10✔
731
            // Override default parameters, for backward-compatibility
732
            // with current behaviour
733
            $retry = new Retry(
9✔
734
                new ExponentialBackoff($this->retryWait * 1000, -1),
9✔
735
                $this->retryLimit
9✔
736
            );
9✔
737
        } else {
738
            $retry = $parameters->retry;
1✔
739
        }
740
        $retry->updateCatchableExceptions([Throwable::class]);
10✔
741

742
        $doCallback = function () use ($method, $command) {
10✔
743
            $response = $this->getConnectionByCommand($command)->{$method}($command);
10✔
744

745
            if ($response instanceof Error && $response->getErrorType() === 'LOADING') {
8✔
746
                throw new ConnectionException($this->current, $response->getMessage());
1✔
747
            }
748

749
            return $response;
8✔
750
        };
10✔
751

752
        $failCallback = function (Throwable $exception) {
10✔
753
            $this->wipeServerList();
7✔
754

755
            if ($exception instanceof CommunicationException) {
7✔
756
                $exception->getConnection()->disconnect();
6✔
757
            }
758
        };
10✔
759

760
        return $retry->callWithRetry($doCallback, $failCallback);
10✔
761
    }
762

763
    /**
764
     * {@inheritdoc}
765
     */
766
    public function writeRequest(CommandInterface $command)
×
767
    {
768
        $this->retryCommandOnFailure($command, __FUNCTION__);
×
769
    }
770

771
    /**
772
     * {@inheritdoc}
773
     */
UNCOV
774
    public function readResponse(CommandInterface $command)
×
775
    {
UNCOV
776
        return $this->retryCommandOnFailure($command, __FUNCTION__);
×
777
    }
778

779
    /**
780
     * {@inheritdoc}
781
     */
782
    public function executeCommand(CommandInterface $command)
10✔
783
    {
784
        return $this->retryCommandOnFailure($command, __FUNCTION__);
10✔
785
    }
786

787
    /**
788
     * Returns the underlying replication strategy.
789
     *
790
     * @return ReplicationStrategy
791
     */
792
    public function getReplicationStrategy()
2✔
793
    {
794
        return $this->strategy;
2✔
795
    }
796

797
    /**
798
     * {@inheritdoc}
799
     */
800
    public function __sleep()
1✔
801
    {
802
        return [
1✔
803
            'master', 'slaves', 'pool', 'service', 'sentinels', 'connectionFactory', 'strategy',
1✔
804
        ];
1✔
805
    }
806

807
    /**
808
     * {@inheritdoc}
809
     */
810
    public function getParameters(): ?ParametersInterface
18✔
811
    {
812
        if (isset($this->master)) {
18✔
813
            return $this->master->getParameters();
11✔
814
        }
815

816
        if (!empty($this->slaves)) {
7✔
817
            return $this->slaves[0]->getParameters();
1✔
818
        }
819

820
        if (!empty($this->sentinels)) {
6✔
821
            $sentinel = $this->sentinels[0];
5✔
822

823
            // Handle string URIs (e.g., "tcp://127.0.0.1:26379")
824
            if (is_string($sentinel)) {
5✔
825
                return new Parameters(Parameters::parse($sentinel));
1✔
826
            }
827

828
            // After querySentinels(), sentinels array contains plain arrays instead of connection objects
829
            if (is_array($sentinel)) {
4✔
830
                return new Parameters($sentinel);
2✔
831
            }
832

833
            if ($sentinel instanceof ParametersInterface) {
2✔
834
                return $sentinel;
1✔
835
            }
836

837
            return $sentinel->getParameters();
1✔
838
        }
839

840
        return null;
1✔
841
    }
842
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc