Skip to content

Commit 3b9a125

Browse files
committed
Add support for exponential backoff on retry
1 parent c3ab4a2 commit 3b9a125

File tree

10 files changed

+238
-6
lines changed

10 files changed

+238
-6
lines changed

backoff.c

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#include "common.h"
2+
3+
#include <ext/standard/php_rand.h>
4+
#include <ext/standard/php_mt_rand.h>
5+
6+
#include "backoff.h"
7+
8+
zend_long default_backoff(struct Backoff *self, int retry_index) {
9+
zend_long backoff = retry_index ? self->base : (php_rand() % self->base);
10+
return MIN(self->cap, backoff);
11+
}
12+
13+
zend_long constant_backoff(struct Backoff *self, int retry_index) {
14+
zend_long backoff = self->base;
15+
return MIN(self->cap, backoff);
16+
}
17+
18+
zend_long uniform_backoff(struct Backoff *self, int retry_index) {
19+
zend_long backoff = php_rand() % self->base;
20+
return MIN(self->cap, backoff);
21+
}
22+
23+
zend_long exponential_backoff(struct Backoff *self, int retry_index) {
24+
zend_long pow = MIN(retry_index, 10);
25+
zend_long backoff = self->base * (1 << pow);
26+
return MIN(self->cap, backoff);
27+
}
28+
29+
zend_long full_jitter_backoff(struct Backoff *self, int retry_index) {
30+
zend_long pow = MIN(retry_index, 10);
31+
zend_long backoff = self->base * (1 << pow);
32+
zend_long cap = MIN(self->cap, backoff);
33+
return php_mt_rand_range(0, cap);
34+
}
35+
36+
zend_long equal_jitter_backoff(struct Backoff *self, int retry_index) {
37+
zend_long pow = MIN(retry_index, 10);
38+
zend_long backoff = self->base * (1 << pow);
39+
zend_long temp = MIN(self->cap, backoff) >> 1;
40+
return temp + php_mt_rand_range(0, temp);
41+
}
42+
43+
zend_long decorrelated_jitter_backoff(struct Backoff *self, int retry_index) {
44+
zend_long max_backoff = MAX(self->base, self->previous_backoff * 3);
45+
zend_long temp = php_mt_rand_range(self->base, max_backoff);
46+
self->previous_backoff = MIN(self->cap, temp);
47+
return self->previous_backoff;
48+
}
49+
50+
typedef zend_long (*backoff_algorithm)(struct Backoff *self, int retry_index);
51+
52+
static backoff_algorithm backoff_algorithms[BACKOFF_ALGORITHMS] = {
53+
default_backoff,
54+
constant_backoff,
55+
uniform_backoff,
56+
exponential_backoff,
57+
full_jitter_backoff,
58+
equal_jitter_backoff,
59+
decorrelated_jitter_backoff,
60+
};
61+
62+
struct Backoff make_default_backoff(long retry_interval) {
63+
struct Backoff self = {
64+
.algorithm = 0, // default backoff
65+
.base = retry_interval,
66+
.cap = retry_interval,
67+
.previous_backoff = 0
68+
};
69+
70+
return self;
71+
}
72+
73+
void backoff_reset(struct Backoff *self) {
74+
self->previous_backoff = 0;
75+
}
76+
77+
zend_long backoff_compute(struct Backoff *self, int retry_index) {
78+
return backoff_algorithms[self->algorithm](self, retry_index);
79+
}

backoff.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#ifndef REDIS_BACKOFF_H
2+
#define REDIS_BACKOFF_H
3+
4+
/* {{{ struct Backoff */
5+
struct Backoff {
6+
int algorithm; /* index of algorithm function, returns backoff in microseconds*/
7+
zend_long base; /* base backoff in microseconds */
8+
zend_long cap; /* max backoff in microseconds */
9+
zend_long previous_backoff; /* previous backoff in microseconds */
10+
};
11+
/* }}} */
12+
13+
zend_long default_backoff(struct Backoff *self, int retry_index);
14+
zend_long constant_backoff(struct Backoff *self, int retry_index);
15+
zend_long uniform_backoff(struct Backoff *self, int retry_index);
16+
zend_long exponential_backoff(struct Backoff *self, int retry_index);
17+
zend_long full_jitter_backoff(struct Backoff *self, int retry_index);
18+
zend_long equal_jitter_backoff(struct Backoff *self, int retry_index);
19+
zend_long decorrelated_jitter_backoff(struct Backoff *self, int retry_index);
20+
21+
struct Backoff make_default_backoff();
22+
23+
void backoff_reset(struct Backoff *self);
24+
zend_long backoff_compute(struct Backoff *self, int retry_index);
25+
26+
#endif

common.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
#define NULL ((void *) 0)
2222
#endif
2323

24+
#include "backoff.h"
25+
2426
typedef enum {
2527
REDIS_SOCK_STATUS_FAILED = -1,
2628
REDIS_SOCK_STATUS_DISCONNECTED,
@@ -83,6 +85,10 @@ typedef enum _PUBSUB_TYPE {
8385
#define REDIS_OPT_REPLY_LITERAL 8
8486
#define REDIS_OPT_COMPRESSION_LEVEL 9
8587
#define REDIS_OPT_NULL_MBULK_AS_NULL 10
88+
#define REDIS_OPT_MAX_RETRIES 11
89+
#define REDIS_OPT_BACKOFF_ALGORITHM 12
90+
#define REDIS_OPT_BACKOFF_BASE 13
91+
#define REDIS_OPT_BACKOFF_CAP 14
8692

8793
/* cluster options */
8894
#define REDIS_FAILOVER_NONE 0
@@ -109,6 +115,24 @@ typedef enum {
109115
#define REDIS_SCAN_PREFIX 2
110116
#define REDIS_SCAN_NOPREFIX 3
111117

118+
/* BACKOFF_ALGORITHM options */
119+
#define BACKOFF_ALGORITHMS 7
120+
#define BACKOFF_ALGORITHM_DEFAULT 0
121+
#define BACKOFF_ALGORITHM_CONSTANT 1
122+
#define BACKOFF_ALGORITHM_UNIFORM 2
123+
#define BACKOFF_ALGORITHM_EXPONENTIAL 3
124+
#define BACKOFF_ALGORITHM_FULL_JITTER 4
125+
#define BACKOFF_ALGORITHM_EQUAL_JITTER 5
126+
#define BACKOFF_ALGORITHM_DECORRELATED_JITTER 6
127+
128+
zend_long default_backoff(struct Backoff *self, int retry_index);
129+
zend_long constant_backoff(struct Backoff *self, int retry_index);
130+
zend_long uniform_backoff(struct Backoff *self, int retry_index);
131+
zend_long exponential_backoff(struct Backoff *self, int retry_index);
132+
zend_long full_jitter_backoff(struct Backoff *self, int retry_index);
133+
zend_long equal_jitter_backoff(struct Backoff *self, int retry_index);
134+
zend_long decorrelated_jitter_backoff(struct Backoff *self, int retry_index);
135+
112136
/* GETBIT/SETBIT offset range limits */
113137
#define BITOP_MIN_OFFSET 0
114138
#define BITOP_MAX_OFFSET 4294967295U
@@ -267,6 +291,8 @@ typedef struct {
267291
double timeout;
268292
double read_timeout;
269293
long retry_interval;
294+
int max_retries;
295+
struct Backoff backoff;
270296
redis_sock_status status;
271297
int persistent;
272298
int watching;

config.m4

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,5 +323,5 @@ if test "$PHP_REDIS" != "no"; then
323323
fi
324324

325325
PHP_SUBST(REDIS_SHARED_LIBADD)
326-
PHP_NEW_EXTENSION(redis, redis.c redis_commands.c library.c redis_session.c redis_array.c redis_array_impl.c redis_cluster.c cluster_library.c redis_sentinel.c sentinel_library.c $lzf_sources, $ext_shared)
326+
PHP_NEW_EXTENSION(redis, redis.c redis_commands.c library.c redis_session.c redis_array.c redis_array_impl.c redis_cluster.c cluster_library.c redis_sentinel.c sentinel_library.c backoff.c $lzf_sources, $ext_shared)
327327
fi

config.w32

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ ARG_ENABLE("redis-session", "whether to enable sessions", "yes");
55
ARG_ENABLE("redis-igbinary", "whether to enable igbinary serializer support", "no");
66

77
if (PHP_REDIS != "no") {
8-
var sources = "redis.c redis_commands.c library.c redis_session.c redis_array.c redis_array_impl.c redis_cluster.c cluster_library.c redis_sentinel.c sentinel_library.c";
8+
var sources = "redis.c redis_commands.c library.c redis_session.c redis_array.c redis_array_impl.c redis_cluster.c cluster_library.c redis_sentinel.c sentinel_library.c backoff.c";
99
if (PHP_REDIS_SESSION != "no") {
1010
ADD_EXTENSION_DEP("redis", "session");
1111
ADD_FLAG("CFLAGS_REDIS", ' /D PHP_SESSION=1 ');

library.c

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ redis_error_throw(RedisSock *redis_sock)
301301
PHP_REDIS_API int
302302
redis_check_eof(RedisSock *redis_sock, int no_throw)
303303
{
304-
int count;
304+
int retry_index;
305305
char *errmsg;
306306

307307
if (!redis_sock || !redis_sock->stream || redis_sock->status == REDIS_SOCK_STATUS_FAILED) {
@@ -333,16 +333,16 @@ redis_check_eof(RedisSock *redis_sock, int no_throw)
333333
errmsg = "Connection lost and socket is in MULTI/watching mode";
334334
} else {
335335
errmsg = "Connection lost";
336-
/* TODO: configurable max retry count */
337-
for (count = 0; count < 10; ++count) {
336+
backoff_reset(&redis_sock->backoff);
337+
for (retry_index = 0; retry_index < redis_sock->max_retries; ++retry_index) {
338338
/* close existing stream before reconnecting */
339339
if (redis_sock->stream) {
340340
redis_sock_disconnect(redis_sock, 1);
341341
}
342342
// Wait for a while before trying to reconnect
343343
if (redis_sock->retry_interval) {
344344
// Random factor to avoid having several (or many) concurrent connections trying to reconnect at the same time
345-
long retry_interval = (count ? redis_sock->retry_interval : (php_rand() % redis_sock->retry_interval));
345+
zend_long retry_interval = backoff_compute(&redis_sock->backoff, retry_index);
346346
usleep(retry_interval);
347347
}
348348
/* reconnect */
@@ -2150,6 +2150,8 @@ redis_sock_create(char *host, int host_len, int port,
21502150
redis_sock->host = zend_string_init(host, host_len, 0);
21512151
redis_sock->status = REDIS_SOCK_STATUS_DISCONNECTED;
21522152
redis_sock->retry_interval = retry_interval * 1000;
2153+
redis_sock->max_retries = 10;
2154+
redis_sock->backoff = make_default_backoff(retry_interval);
21532155
redis_sock->persistent = persistent;
21542156

21552157
if (persistent && persistent_id != NULL) {

package.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
8888
<file role='doc' name='arrays.markdown'/>
8989
<file role='doc' name='cluster.markdown'/>
9090
<file role='doc' name='sentinel.markdown'/>
91+
<file role='src' name='backoff.c'/>
92+
<file role='src' name='backoff.h'/>
9193
<file role='src' name='cluster_library.c'/>
9294
<file role='src' name='cluster_library.h'/>
9395
<file role='src' name='common.h'/>

redis.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,22 @@ static void add_class_constants(zend_class_entry *ce, int is_cluster) {
775775
zend_declare_class_constant_stringl(ce, "LEFT", 4, "left", 4);
776776
zend_declare_class_constant_stringl(ce, "RIGHT", 5, "right", 5);
777777
}
778+
779+
/* retry/backoff options*/
780+
zend_declare_class_constant_long(ce, ZEND_STRL("OPT_MAX_RETRIES"), REDIS_OPT_MAX_RETRIES);
781+
782+
zend_declare_class_constant_long(ce, ZEND_STRL("OPT_BACKOFF_ALGORITHM"), REDIS_OPT_BACKOFF_ALGORITHM);
783+
zend_declare_class_constant_long(ce, ZEND_STRL("BACKOFF_ALGORITHM_DEFAULT"), BACKOFF_ALGORITHM_DEFAULT);
784+
zend_declare_class_constant_long(ce, ZEND_STRL("BACKOFF_ALGORITHM_CONSTANT"), BACKOFF_ALGORITHM_CONSTANT);
785+
zend_declare_class_constant_long(ce, ZEND_STRL("BACKOFF_ALGORITHM_UNIFORM"), BACKOFF_ALGORITHM_UNIFORM);
786+
zend_declare_class_constant_long(ce, ZEND_STRL("BACKOFF_ALGORITHM_EXPONENTIAL"), BACKOFF_ALGORITHM_EXPONENTIAL);
787+
zend_declare_class_constant_long(ce, ZEND_STRL("BACKOFF_ALGORITHM_FULL_JITTER"), BACKOFF_ALGORITHM_FULL_JITTER);
788+
zend_declare_class_constant_long(ce, ZEND_STRL("BACKOFF_ALGORITHM_EQUAL_JITTER"), BACKOFF_ALGORITHM_EQUAL_JITTER);
789+
zend_declare_class_constant_long(ce, ZEND_STRL("BACKOFF_ALGORITHM_DECORRELATED_JITTER"), BACKOFF_ALGORITHM_DECORRELATED_JITTER);
790+
791+
zend_declare_class_constant_long(ce, ZEND_STRL("OPT_BACKOFF_BASE"), REDIS_OPT_BACKOFF_BASE);
792+
793+
zend_declare_class_constant_long(ce, ZEND_STRL("OPT_BACKOFF_CAP"), REDIS_OPT_BACKOFF_CAP);
778794
}
779795

780796
static ZEND_RSRC_DTOR_FUNC(redis_connections_pool_dtor)

redis_commands.c

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4308,6 +4308,14 @@ void redis_getoption_handler(INTERNAL_FUNCTION_PARAMETERS,
43084308
RETURN_LONG(redis_sock->null_mbulk_as_null);
43094309
case REDIS_OPT_FAILOVER:
43104310
RETURN_LONG(c->failover);
4311+
case REDIS_OPT_MAX_RETRIES:
4312+
RETURN_LONG(redis_sock->max_retries);
4313+
case REDIS_OPT_BACKOFF_ALGORITHM:
4314+
RETURN_LONG(redis_sock->backoff.algorithm);
4315+
case REDIS_OPT_BACKOFF_BASE:
4316+
RETURN_LONG(redis_sock->backoff.base / 1000);
4317+
case REDIS_OPT_BACKOFF_CAP:
4318+
RETURN_LONG(redis_sock->backoff.cap / 1000);
43114319
default:
43124320
RETURN_FALSE;
43134321
}
@@ -4441,6 +4449,35 @@ void redis_setoption_handler(INTERNAL_FUNCTION_PARAMETERS,
44414449
RETURN_TRUE;
44424450
}
44434451
break;
4452+
case REDIS_OPT_MAX_RETRIES:
4453+
val_long = zval_get_long(val);
4454+
if(val_long >= 0) {
4455+
redis_sock->max_retries = val_long;
4456+
RETURN_TRUE;
4457+
}
4458+
break;
4459+
case REDIS_OPT_BACKOFF_ALGORITHM:
4460+
val_long = zval_get_long(val);
4461+
if(val_long >= 0 &&
4462+
val_long < BACKOFF_ALGORITHMS) {
4463+
redis_sock->backoff.algorithm = val_long;
4464+
RETURN_TRUE;
4465+
}
4466+
break;
4467+
case REDIS_OPT_BACKOFF_BASE:
4468+
val_long = zval_get_long(val);
4469+
if(val_long >= 0) {
4470+
redis_sock->backoff.base = val_long * 1000;
4471+
RETURN_TRUE;
4472+
}
4473+
break;
4474+
case REDIS_OPT_BACKOFF_CAP:
4475+
val_long = zval_get_long(val);
4476+
if(val_long >= 0) {
4477+
redis_sock->backoff.cap = val_long * 1000;
4478+
RETURN_TRUE;
4479+
}
4480+
break;
44444481
EMPTY_SWITCH_DEFAULT_CASE()
44454482
}
44464483
RETURN_FALSE;

tests/RedisTest.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5398,6 +5398,50 @@ public function testScanPrefix() {
53985398
$this->assertTrue(count($arr_all_keys) == 0);
53995399
}
54005400

5401+
public function testMaxRetriesOption() {
5402+
$maxRetriesExpected = 5;
5403+
$this->redis->setOption(Redis::OPT_MAX_RETRIES, $maxRetriesExpected);
5404+
$maxRetriesActual=$this->redis->getOption(Redis::OPT_MAX_RETRIES);
5405+
$this->assertEquals($maxRetriesActual, $maxRetriesExpected);
5406+
}
5407+
5408+
public function testBackoffOptions() {
5409+
$this->redis->setOption(Redis::OPT_MAX_RETRIES, 5);
5410+
$this->assertEquals($this->redis->getOption(Redis::OPT_MAX_RETRIES), 5);
5411+
5412+
$this->$redis->setOption(Redis::OPT_BACKOFF_ALGORITHM, Redis::BACKOFF_ALGORITHM_DEFAULT);
5413+
$this->assertEquals($this->$redis->getOption(Redis::OPT_BACKOFF_ALGORITHM), Redis::BACKOFF_ALGORITHM_DEFAULT);
5414+
5415+
$this->$redis->setOption(Redis::OPT_BACKOFF_ALGORITHM, Redis::BACKOFF_ALGORITHM_CONSTANT);
5416+
$this->assertEquals($this->$redis->getOption(Redis::OPT_BACKOFF_ALGORITHM), Redis::BACKOFF_ALGORITHM_CONSTANT);
5417+
5418+
$this->$redis->setOption(Redis::OPT_BACKOFF_ALGORITHM, Redis::BACKOFF_ALGORITHM_UNIFORM);
5419+
$this->assertEquals($this->$redis->getOption(Redis::OPT_BACKOFF_ALGORITHM), Redis::BACKOFF_ALGORITHM_UNIFORM);
5420+
5421+
$this->$redis -> setOption(Redis::OPT_BACKOFF_ALGORITHM, Redis::BACKOFF_ALGORITHM_EXPONENTIAL);
5422+
$this->assertEquals($this->$redis->getOption(Redis::OPT_BACKOFF_ALGORITHM), Redis::BACKOFF_ALGORITHM_EXPONENTIAL);
5423+
5424+
$this->$redis->setOption(Redis::OPT_BACKOFF_ALGORITHM, Redis::BACKOFF_ALGORITHM_FULL_JITTER);
5425+
$this->assertEquals($this->$redis->getOption(Redis::OPT_BACKOFF_ALGORITHM), Redis::BACKOFF_ALGORITHM_FULL_JITTER);
5426+
5427+
$this->$redis->setOption(Redis::OPT_BACKOFF_ALGORITHM, Redis::BACKOFF_ALGORITHM_DECORRELATED_JITTER);
5428+
$this->assertEquals($this->$redis->getOption(Redis::OPT_BACKOFF_ALGORITHM), Redis::BACKOFF_ALGORITHM_DECORRELATED_JITTER);
5429+
5430+
$this->assertFalse($this->$redis->setOption(Redis::OPT_BACKOFF_ALGORITHM, 55555));
5431+
5432+
$this->$redis->setOption(Redis::OPT_BACKOFF_BASE, 500);
5433+
$this->assertEquals($this->$redis->getOption(Redis::OPT_BACKOFF_BASE), 500);
5434+
5435+
$this->$redis->setOption(Redis::OPT_BACKOFF_BASE, 750);
5436+
$this->assertEquals($this->$redis->getOption(Redis::OPT_BACKOFF_BASE), 750);
5437+
5438+
$this->$redis->setOption(Redis::OPT_BACKOFF_CAP, 500);
5439+
$this->assertEquals($this->$redis->getOption(Redis::OPT_BACKOFF_CAP), 500);
5440+
5441+
$this->$redis->setOption(Redis::OPT_BACKOFF_CAP, 750);
5442+
$this->assertEquals($this->$redis->getOption(Redis::OPT_BACKOFF_CAP), 750);
5443+
}
5444+
54015445
public function testHScan() {
54025446
if (version_compare($this->version, "2.8.0") < 0) {
54035447
$this->markTestSkipped();

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy