Skip to content

Commit 39df0b7

Browse files
fixing issue with re-connect logic
There was an issue with the re-connect logic such that it would sometimes only partially apply a transaction. The redis_check_eof() function in library.c was automatically and invisibly re-connecting to the redis server when php_stream_eof() returned non-zero. If this happened to happen after a transaction had been partially stored, the server was rolling back the transaction but the phpredis library was continuing as if it was still applying the transaction so the remaining commands were being applied immediately. The result was that the portion of the transaction after the re-connect was applied (although not atomically) but the portion before the reconnect was thrown away by the server. This change causes the phpredis library to fail (by throwing a RedisException) instead of reconnecting if the client session is in multi mode or it is watching any keys. Either of these conditions indicate that the server has lost the state of the session and the client needs to rebuild it. Because transactions can fail due to watched keys having been changed, clients that use transactions should already be coded to handle transaction failures using retry logic. With this change, clients that use transactions when talking to redis servers that use connection timeout, they will have to add a try...catch within the transaction retry loop to handle this type of failure. Clients that do not use transactions and clients that use transactions with redis servers that do not use connection timeout will be unaffected. Perhaps not coincidentally, this change also fixes a well known but previously not well understood bug that causes RedisExceptions to be thrown with a message like "protocol error, got '*' as reply type byte". My company began integrating redis into our service that handles 500,000 daily active users in April of this year. Prior to this fix, when we had our redis server connection timeout set to 5 seconds we would get hundreds of those error messages showing up in our php error logs per day. This wasn't a huge problem but it was concerning enough to raise the connection timeout to 30 seconds which reduced the rate of the errors but didn't eliminate them. At one point we had a process that was failing repeatedly with this error with a 30 second timeout so we had to raise the timeout all the way up to 300 seconds. A long timeout is not idea for a high throughput website like ours since it can cause failures to cascade from one system to another due to resource limits and requests holding on to connections for a long time. After introducing this fix roughly a month ago we have not had a single instance of RedisException show up in our php error logs (except for legitimate errors related to the server being down) even after lowering the server timeout back down to 5 seconds. To reproduce the problem without this fix, set the redis server timeout configuration to 3 seconds and use the following test php script: <?php define('MAX_FAILURES', 1); function get_redis() { $r = new Redis(); $r->connect('localhost'); return $r; } $r = get_redis(); $r->set('foo', '123'); $r->set('bar', 'abc'); if (isset($_GET['trans']) && $_GET['trans']) { $completed = false; $failures = 0; while (!$completed && ($failures < MAX_FAILURES)) { try { $trans = $r->multi(); $trans->set('foo', $_GET['foo']); if (isset($_GET['sleep']) && $_GET['sleep']) { sleep($_GET['sleep']); } $trans->set('bar', $_GET['bar']); var_export($trans->exec()); echo '<br/>'; $completed = true; } catch (RedisException $e) { echo 'transaction failed<br/>'; $failures++; $r = get_redis(); } } } else { $r->set('foo', $_GET['foo']); if (isset($_GET['sleep']) && $_GET['sleep']) { sleep($_GET['sleep']); } $r->set('bar', $_GET['bar']); } echo $r->get('foo'); echo '<br/>'; echo $r->get('bar'); ?> **************************** *** Results without this fix **************************** foo=bar&bar=baz&trans=0&sleep=0 bar baz foo=bar&bar=baz&trans=1&sleep=0 array ( 0 => true, 1 => true, ) bar baz foo=bar&bar=baz&trans=0&sleep=30 bar baz foo=bar&bar=baz&trans=1&sleep=30 NULL 123 baz Notice in this last example the call to exec() did not return anything and the value of the key 'bar' was modified by the transaction but the value of the key 'foo' was not even though the calls to set() on both keys were made between a call to multi() and a call to exec(). ************************* *** Results with this fix ************************* foo=bar&bar=baz&trans=0&sleep=0 bar baz foo=bar&bar=baz&trans=1&sleep=0 array ( 0 => true, 1 => true, ) bar baz foo=bar&bar=baz&trans=0&sleep=30 bar baz foo=bar&bar=baz&trans=1&sleep=30 transaction failed 123 abc Notice in the last example where the transaction failed message is printed, it is necessary to explicitly reconnect to the redis server. Trying to reuse the same redis object after it has failed to reconnect will result in a segmentation fault. I believe this was an existing problem with the phpredis library and it is not addressed by this change.
1 parent 31663d4 commit 39df0b7

File tree

5 files changed

+63
-13
lines changed

5 files changed

+63
-13
lines changed

common.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ typedef struct {
150150
int failed;
151151
int status;
152152
int persistent;
153+
int watching;
153154
char *persistent_id;
154155

155156
int serializer;

library.c

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,24 +31,25 @@ PHPAPI void redis_stream_close(RedisSock *redis_sock TSRMLS_DC) {
3131

3232
PHPAPI int redis_check_eof(RedisSock *redis_sock TSRMLS_DC)
3333
{
34-
35-
int eof = redis_sock->stream == NULL ? 1 : php_stream_eof(redis_sock->stream);
34+
int eof = php_stream_eof(redis_sock->stream);
3635
int count = 0;
3736
while(eof) {
38-
if(count++ == 10) { /* too many failures */
37+
if((MULTI == redis_sock->mode) || redis_sock->watching || count++ == 10) { /* too many failures */
3938
if(redis_sock->stream) { /* close stream if still here */
40-
redis_stream_close(redis_sock TSRMLS_CC);
39+
php_stream_close(redis_sock->stream);
4140
redis_sock->stream = NULL;
4241
redis_sock->mode = ATOMIC;
4342
redis_sock->status = REDIS_SOCK_STATUS_FAILED;
43+
redis_sock->watching = 0;
4444
}
4545
zend_throw_exception(redis_exception_ce, "Connection lost", 0 TSRMLS_CC);
4646
return -1;
4747
}
4848
if(redis_sock->stream) { /* close existing stream before reconnecting */
49-
redis_stream_close(redis_sock TSRMLS_CC);
49+
php_stream_close(redis_sock->stream);
5050
redis_sock->stream = NULL;
5151
redis_sock->mode = ATOMIC;
52+
redis_sock->watching = 0;
5253
}
5354
redis_sock_connect(redis_sock TSRMLS_CC); /* reconnect */
5455
if(redis_sock->stream) { /* check for EOF again. */
@@ -72,6 +73,7 @@ PHPAPI zval *redis_sock_read_multibulk_reply_zval(INTERNAL_FUNCTION_PARAMETERS,
7273
redis_sock->stream = NULL;
7374
redis_sock->status = REDIS_SOCK_STATUS_FAILED;
7475
redis_sock->mode = ATOMIC;
76+
redis_sock->watching = 0;
7577
zend_throw_exception(redis_exception_ce, "read error on connection", 0 TSRMLS_CC);
7678
return NULL;
7779
}
@@ -146,6 +148,7 @@ PHPAPI char *redis_sock_read(RedisSock *redis_sock, int *buf_len TSRMLS_DC)
146148
redis_sock->stream = NULL;
147149
redis_sock->status = REDIS_SOCK_STATUS_FAILED;
148150
redis_sock->mode = ATOMIC;
151+
redis_sock->watching = 0;
149152
zend_throw_exception(redis_exception_ce, "read error on connection", 0 TSRMLS_CC);
150153
return NULL;
151154
}
@@ -469,7 +472,7 @@ PHPAPI void redis_info_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_s
469472
}
470473
}
471474

472-
PHPAPI void redis_boolean_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx) {
475+
PHPAPI void redis_boolean_response_impl(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx, SuccessCallback success_callback) {
473476

474477
char *response;
475478
int response_len;
@@ -487,19 +490,29 @@ PHPAPI void redis_boolean_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redi
487490

488491
IF_MULTI_OR_PIPELINE() {
489492
if (ret == '+') {
493+
if (success_callback != NULL) {
494+
success_callback(redis_sock);
495+
}
490496
add_next_index_bool(z_tab, 1);
491497
} else {
492498
add_next_index_bool(z_tab, 0);
493499
}
494500
} else {
495501
if (ret == '+') {
502+
if (success_callback != NULL) {
503+
success_callback(redis_sock);
504+
}
496505
RETURN_TRUE;
497506
} else {
498507
RETURN_FALSE;
499508
}
500509
}
501510
}
502511

512+
PHPAPI void redis_boolean_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx) {
513+
redis_boolean_response_impl(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, z_tab, ctx, NULL);
514+
}
515+
503516
PHPAPI void redis_long_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval * z_tab, void *ctx) {
504517

505518
char *response;
@@ -561,6 +574,7 @@ PHPAPI int redis_sock_read_multibulk_reply_zipped_with_flag(INTERNAL_FUNCTION_PA
561574
redis_sock->stream = NULL;
562575
redis_sock->status = REDIS_SOCK_STATUS_FAILED;
563576
redis_sock->mode = ATOMIC;
577+
redis_sock->watching = 0;
564578
zend_throw_exception(redis_exception_ce, "read error on connection", 0 TSRMLS_CC);
565579
return -1;
566580
}
@@ -692,6 +706,7 @@ PHPAPI RedisSock* redis_sock_create(char *host, int host_len, unsigned short por
692706
redis_sock->host = estrndup(host, host_len);
693707
redis_sock->stream = NULL;
694708
redis_sock->status = REDIS_SOCK_STATUS_DISCONNECTED;
709+
redis_sock->watching = 0;
695710

696711
redis_sock->persistent = persistent;
697712

@@ -832,6 +847,7 @@ PHPAPI int redis_sock_disconnect(RedisSock *redis_sock TSRMLS_DC)
832847
}
833848

834849
redis_sock->status = REDIS_SOCK_STATUS_DISCONNECTED;
850+
redis_sock->watching = 0;
835851
if(redis_sock->stream && !redis_sock->persistent) { /* still valid after the write? */
836852
php_stream_close(redis_sock->stream);
837853
}
@@ -884,6 +900,7 @@ PHPAPI int redis_sock_read_multibulk_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSo
884900
redis_sock->stream = NULL;
885901
redis_sock->status = REDIS_SOCK_STATUS_FAILED;
886902
redis_sock->mode = ATOMIC;
903+
redis_sock->watching = 0;
887904
zend_throw_exception(redis_exception_ce, "read error on connection", 0 TSRMLS_CC);
888905
return -1;
889906
}
@@ -925,6 +942,7 @@ PHPAPI int redis_sock_read_multibulk_reply_raw(INTERNAL_FUNCTION_PARAMETERS, Red
925942
redis_sock->stream = NULL;
926943
redis_sock->status = REDIS_SOCK_STATUS_FAILED;
927944
redis_sock->mode = ATOMIC;
945+
redis_sock->watching = 0;
928946
zend_throw_exception(redis_exception_ce, "read error on connection", 0 TSRMLS_CC);
929947
return -1;
930948
}
@@ -998,6 +1016,7 @@ PHPAPI int redis_sock_read_multibulk_reply_assoc(INTERNAL_FUNCTION_PARAMETERS, R
9981016
redis_sock->stream = NULL;
9991017
redis_sock->status = REDIS_SOCK_STATUS_FAILED;
10001018
redis_sock->mode = ATOMIC;
1019+
redis_sock->watching = 0;
10011020
zend_throw_exception(redis_exception_ce, "read error on connection", 0 TSRMLS_CC);
10021021
return -1;
10031022
}

library.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ PHPAPI char * redis_sock_read(RedisSock *redis_sock, int *buf_len TSRMLS_DC);
77

88
PHPAPI void redis_1_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx);
99
PHPAPI void redis_long_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval* z_tab, void *ctx);
10+
typedef void (*SuccessCallback)(RedisSock *redis_sock);
11+
PHPAPI void redis_boolean_response_impl(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx, SuccessCallback success_callback);
1012
PHPAPI void redis_boolean_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx);
1113
PHPAPI void redis_bulk_double_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx);
1214
PHPAPI void redis_string_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx);

php_redis.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ PHPAPI void redis_atomic_increment(INTERNAL_FUNCTION_PARAMETERS, char *keyword,
174174
PHPAPI int generic_multiple_args_cmd(INTERNAL_FUNCTION_PARAMETERS, char *keyword, int keyword_len,
175175
int min_argc, RedisSock **redis_sock, int has_timeout, int all_keys);
176176
PHPAPI void generic_sort_cmd(INTERNAL_FUNCTION_PARAMETERS, char *sort, int use_alpha);
177+
typedef void (*ResultCallback)(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx);
178+
PHPAPI void generic_empty_cmd_impl(INTERNAL_FUNCTION_PARAMETERS, char *cmd, int cmd_len, ResultCallback result_callback);
177179
PHPAPI void generic_empty_cmd(INTERNAL_FUNCTION_PARAMETERS, char *cmd, int cmd_len, ...);
178180
PHPAPI void generic_empty_long_cmd(INTERNAL_FUNCTION_PARAMETERS, char *cmd, int cmd_len, ...);
179181

redis.c

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,6 +1119,16 @@ PHP_METHOD(Redis, delete)
11191119
}
11201120
/* }}} */
11211121

1122+
PHPAPI void redis_set_watch(RedisSock *redis_sock)
1123+
{
1124+
redis_sock->watching = 1;
1125+
}
1126+
1127+
PHPAPI void redis_watch_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx)
1128+
{
1129+
redis_boolean_response_impl(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, z_tab, ctx, redis_set_watch);
1130+
}
1131+
11221132
/* {{{ proto boolean Redis::watch(string key1, string key2...)
11231133
*/
11241134
PHP_METHOD(Redis, watch)
@@ -1128,20 +1138,31 @@ PHP_METHOD(Redis, watch)
11281138
generic_multiple_args_cmd(INTERNAL_FUNCTION_PARAM_PASSTHRU,
11291139
"WATCH", sizeof("WATCH") - 1,
11301140
1, &redis_sock, 0, 1);
1141+
redis_sock->watching = 1;
11311142
IF_ATOMIC() {
1132-
redis_boolean_response(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, NULL, NULL);
1143+
redis_watch_response(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, NULL, NULL);
11331144
}
1134-
REDIS_PROCESS_RESPONSE(redis_boolean_response);
1145+
REDIS_PROCESS_RESPONSE(redis_watch_response);
11351146

11361147
}
11371148
/* }}} */
11381149

1150+
PHPAPI void redis_clear_watch(RedisSock *redis_sock)
1151+
{
1152+
redis_sock->watching = 0;
1153+
}
1154+
1155+
PHPAPI void redis_unwatch_response(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx)
1156+
{
1157+
redis_boolean_response_impl(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, z_tab, ctx, redis_clear_watch);
1158+
}
1159+
11391160
/* {{{ proto boolean Redis::unwatch()
11401161
*/
11411162
PHP_METHOD(Redis, unwatch)
11421163
{
11431164
char cmd[] = "*1" _NL "$7" _NL "UNWATCH" _NL;
1144-
generic_empty_cmd(INTERNAL_FUNCTION_PARAM_PASSTHRU, estrdup(cmd), sizeof(cmd)-1);
1165+
generic_empty_cmdi_impl(INTERNAL_FUNCTION_PARAM_PASSTHRU, estrdup(cmd), sizeof(cmd)-1, redis_unwatch_response);
11451166

11461167
}
11471168
/* }}} */
@@ -2760,8 +2781,7 @@ PHP_METHOD(Redis, lSet) {
27602781
}
27612782
/* }}} */
27622783

2763-
2764-
PHPAPI void generic_empty_cmd(INTERNAL_FUNCTION_PARAMETERS, char *cmd, int cmd_len, ...) {
2784+
PHPAPI void generic_empty_cmd_impl(INTERNAL_FUNCTION_PARAMETERS, char *cmd, int cmd_len, ResultCallback result_callback) {
27652785
zval *object;
27662786
RedisSock *redis_sock;
27672787

@@ -2776,9 +2796,13 @@ PHPAPI void generic_empty_cmd(INTERNAL_FUNCTION_PARAMETERS, char *cmd, int cmd_l
27762796

27772797
REDIS_PROCESS_REQUEST(redis_sock, cmd, cmd_len);
27782798
IF_ATOMIC() {
2779-
redis_boolean_response(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, NULL, NULL);
2799+
result_callback(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, NULL, NULL);
27802800
}
2781-
REDIS_PROCESS_RESPONSE(redis_boolean_response);
2801+
REDIS_PROCESS_RESPONSE(result_callback);
2802+
}
2803+
2804+
PHPAPI void generic_empty_cmd(INTERNAL_FUNCTION_PARAMETERS, char *cmd, int cmd_len, ...) {
2805+
generic_empty_cmd_impl(INTERNAL_FUNCTION_PARAM_PASSTHRU, cmd, cmd_len, redis_boolean_response);
27822806
}
27832807

27842808
/* {{{ proto string Redis::save()
@@ -4721,10 +4745,12 @@ PHP_METHOD(Redis, exec)
47214745
zval_dtor(return_value);
47224746
free_reply_callbacks(object, redis_sock);
47234747
redis_sock->mode = ATOMIC;
4748+
redis_sock->watching = 0;
47244749
RETURN_FALSE;
47254750
}
47264751
free_reply_callbacks(object, redis_sock);
47274752
redis_sock->mode = ATOMIC;
4753+
redis_sock->watching = 0;
47284754
}
47294755

47304756
IF_PIPELINE() {

0 commit comments

Comments
 (0)