@@ -31,15 +31,36 @@ class Redis implements Adapter
31
31
'password ' => null ,
32
32
'user ' => null ,
33
33
'sentinel ' => [ // sentinel options
34
- 'enable ' => false ,
35
- 'host ' => null ,
36
- 'port ' => 26379 ,
37
- 'master ' => 'mymaster ' ,
38
- 'timeout ' => 0.1 ,
39
- 'read_timeout ' => null ,
40
- ],
34
+ 'enable ' => false , // if enabled uses sentinel to get the master before connecting to redis
35
+ 'host ' => '127.0.0.1 ' , // phpredis sentinel address of the redis, default is the same as redis host if empty
36
+ 'port ' => 26379 , // phpredis sentinel port of the primary redis server, default 26379 if empty.
37
+ 'service ' => 'myprimary ' , //, phpredis sentinel primary name, default myprimary
38
+ 'timeout ' => 0 , // phpredis sentinel connection timeout
39
+ 'persistent ' => null , // phpredis sentinel persistence parameter
40
+ 'retry_interval ' => 0 , // phpredis sentinel retry interval
41
+ 'read_timeout ' => 0 , // phpredis sentinel read timeout
42
+ 'username ' => '' , // phpredis sentinel auth username
43
+ 'password ' => '' , // phpredis sentinel auth password
44
+ 'ssl ' => null ,
45
+ ]
41
46
];
42
47
48
+ // The following array contains all exception message parts which are interpreted as a connection loss or
49
+ // another unavailability of Redis.
50
+ private const ERROR_MESSAGES_INDICATING_UNAVAILABILITY = [
51
+ 'connection closed ' ,
52
+ 'connection refused ' ,
53
+ 'connection lost ' ,
54
+ 'failed while reconnecting ' ,
55
+ 'is loading the dataset in memory ' ,
56
+ 'php_network_getaddresses ' ,
57
+ 'read error on connection ' ,
58
+ 'socket ' ,
59
+ 'went away ' ,
60
+ 'loading ' ,
61
+ 'readonly ' ,
62
+ "can't write against a read only replica " ,
63
+ ];
43
64
/**
44
65
* @var string
45
66
*/
@@ -67,8 +88,6 @@ class Redis implements Adapter
67
88
public function __construct (array $ options = [])
68
89
{
69
90
$ this ->options = array_merge (self ::$ defaultOptions , $ options );
70
- // is Sentinels ?
71
- $ this ->options = $ this ->isSentinel ($ this ->options );
72
91
$ this ->redis = new \Redis ();
73
92
}
74
93
@@ -78,11 +97,12 @@ public function __construct(array $options = [])
78
97
*/
79
98
public function isSentinel (array $ options = [])
80
99
{
81
- if ($ options ['sentinel ' ] && $ options ['sentinel ' ]['enable ' ]){
82
- $ sentinel = new RedisSentinel ($ options ['sentinel ' ],$ options ['host ' ]);
83
- list ($ hostname , $ port ) = $ sentinel ->getMaster ($ options );
84
- $ options ['host ' ] = $ hostname ;
85
- $ options ['port ' ] = $ port ;
100
+ if ($ options ['sentinel ' ] && $ options ['sentinel ' ]['enable ' ]) {
101
+ $ sentinel = new RedisSentinelConnector ();
102
+ $ options ['sentinel ' ]['host ' ] = $ options ['sentinel ' ]['host ' ] ?? $ options ['host ' ];
103
+ $ master = $ sentinel ->getMaster ($ options ['sentinel ' ]);
104
+ $ options ['host ' ] = $ master ['ip ' ];
105
+ $ options ['port ' ] = $ master ['port ' ];
86
106
}
87
107
return $ options ;
88
108
}
@@ -122,8 +142,8 @@ public static function setPrefix(string $prefix): void
122
142
}
123
143
124
144
/**
125
- * @deprecated use replacement method wipeStorage from Adapter interface
126
145
* @throws StorageException
146
+ * @deprecated use replacement method wipeStorage from Adapter interface
127
147
*/
128
148
public function flushRedis (): void
129
149
{
@@ -212,6 +232,27 @@ function (array $metric): MetricFamilySamples {
212
232
);
213
233
}
214
234
235
+ /**
236
+ * Inspects the given exception and reconnects the client if the reported error indicates that the server
237
+ * went away or is in readonly mode, which may happen in case of a Redis Sentinel failover.
238
+ */
239
+ private function reconnectIfRedisIsUnavailableOrReadonly (RedisException $ exception ): bool
240
+ {
241
+ // We convert the exception message to lower-case in order to perform case-insensitive comparison.
242
+ $ exceptionMessage = strtolower ($ exception ->getMessage ());
243
+
244
+ // Because we also match only partial exception messages, we cannot use in_array() at this point.
245
+ foreach (self ::ERROR_MESSAGES_INDICATING_UNAVAILABILITY as $ errorMessage ) {
246
+ if (str_contains ($ exceptionMessage , $ errorMessage )) {
247
+ // Here we reconnect through Redis Sentinel if we lost connection to the server or if another unavailability occurred.
248
+ // We may actually reconnect to the same, broken server. But after a failover occured, we should be ok.
249
+ // It may take a moment until the Sentinel returns the new master, so this may be triggered multiple times.
250
+ return true ;
251
+ }
252
+ }
253
+ return false ;
254
+ }
255
+
215
256
/**
216
257
* @throws StorageException
217
258
*/
@@ -221,7 +262,24 @@ private function ensureOpenConnection(): void
221
262
return ;
222
263
}
223
264
224
- $ this ->connectToServer ();
265
+ while (true ) {
266
+ try {
267
+ $ this ->options = $ this ->isSentinel ($ this ->options );
268
+ $ this ->connectToServer ();
269
+ break ;
270
+ } catch (\RedisException $ e ) {
271
+ $ retry = $ this ->reconnectIfRedisIsUnavailableOrReadonly ($ e );
272
+ if (!$ retry ) {
273
+ throw new StorageException (
274
+ sprintf ("Can't connect to Redis server. %s " , $ e ->getMessage ()),
275
+ $ e ->getCode (),
276
+ $ e
277
+ );
278
+ }
279
+ }
280
+ }
281
+
282
+
225
283
$ authParams = [];
226
284
227
285
if (isset ($ this ->options ['user ' ]) && $ this ->options ['user ' ] !== '' ) {
@@ -250,28 +308,20 @@ private function ensureOpenConnection(): void
250
308
*/
251
309
private function connectToServer (): void
252
310
{
253
- try {
254
- $ connection_successful = false ;
255
- if ($ this ->options ['persistent_connections ' ] !== false ) {
256
- $ connection_successful = $ this ->redis ->pconnect (
257
- $ this ->options ['host ' ],
258
- (int ) $ this ->options ['port ' ],
259
- (float ) $ this ->options ['timeout ' ]
260
- );
261
- } else {
262
- $ connection_successful = $ this ->redis ->connect ($ this ->options ['host ' ], (int ) $ this ->options ['port ' ], (float ) $ this ->options ['timeout ' ]);
263
- }
264
- if (!$ connection_successful ) {
265
- throw new StorageException (
266
- sprintf ("Can't connect to Redis server. %s " , $ this ->redis ->getLastError ()),
267
- null
268
- );
269
- }
270
- } catch (\RedisException $ e ) {
311
+ $ connection_successful = false ;
312
+ if ($ this ->options ['persistent_connections ' ] !== false ) {
313
+ $ connection_successful = $ this ->redis ->pconnect (
314
+ $ this ->options ['host ' ],
315
+ (int ) $ this ->options ['port ' ],
316
+ (float ) $ this ->options ['timeout ' ]
317
+ );
318
+ } else {
319
+ $ connection_successful = $ this ->redis ->connect ($ this ->options ['host ' ], (int ) $ this ->options ['port ' ], (float ) $ this ->options ['timeout ' ]);
320
+ }
321
+ if (!$ connection_successful ) {
271
322
throw new StorageException (
272
- sprintf ("Can't connect to Redis server. %s " , $ e ->getMessage ()),
273
- $ e ->getCode (),
274
- $ e
323
+ sprintf ("Can't connect to Redis server. %s " , $ this ->redis ->getLastError ()),
324
+ null
275
325
);
276
326
}
277
327
}
0 commit comments