#2931·phpredis

Reconnect inside a pipeline drops its replies and desyncs persistent connections

Author: rechlCreated Sep 18, 2026Updated Sep 18, 2026

On a persistent connection, a pipeline that reconnects inside exec() writes its buffered commands to the new socket but has already freed their reply callbacks. exec() returns an empty array, the replies arrive with nobody waiting for them, and every later reply on that connection is one command behind. Nothing raises.

Reproduce

php
<?php
$redis = new Redis(['host' => '127.0.0.1', 'persistent' => true]);
$redis->set('key', 'value');

$id   = $redis->client('id');
$pipe = $redis->pipeline()->get('key');

/* The server closes the connection while GET is still buffered client side. */
(new Redis(['host' => '127.0.0.1']))->rawCommand('CLIENT', 'KILL', 'ID', (string) $id);

var_dump($pipe->exec());
var_dump($redis->ping());

prints

array(0) {
}
string(5) "value"

With 'persistent' => false the same script prints array(1) { [0]=> string(5) "value" } and bool(true). What happens: exec() reconnects and sends GET, but its reply callback was freed during the reconnect, so nobody consumes the reply. ping() reads it instead of +PONG, and from there on every reply is one command behind.

Cause

redis_check_eof() reconnects in place and keeps the socket mode so that a buffered pipeline survives — that is what is_reset_mode = 0 from #2358 is for. redis_sock_disconnect() frees the reply callbacks anyway, but only in the persistent branch, while pipeline_cmd survives:

c
if (redis_sock->persistent) {
    if (force || !redis_sock_is_atomic(redis_sock)) {
        php_stream_pclose(redis_sock->stream);
        redis_free_reply_callbacks(redis_sock);   /* also on an in-place reconnect */

The non-persistent branch keeps its callbacks, which is why persistent => false is correct.

Fix

diff
             if (force || !redis_sock_is_atomic(redis_sock)) {
                 php_stream_pclose(redis_sock->stream);
-                redis_free_reply_callbacks(redis_sock);
+                /* An in-place reconnect (is_reset_mode == 0) keeps the buffered
+                 * pipeline, so it must keep its reply callbacks too. */
+                if (is_reset_mode) {
+                    redis_free_reply_callbacks(redis_sock);
+                }
                 if (p) p->nb_active--;

is_reset_mode is 0 at exactly one of the thirteen call sites, the reconnect loop in redis_check_eof(), and replaying is safe there because the commands are still entirely client side.

I have tests for both shapes — a plain PIPELINE and a MULTI inside a PIPELINE, which takes the same path because redis_check_eof() tests mode == MULTI. Both fail without the patch and pass with it, and the rest of --class Redis is unchanged. Happy to open a PR.

Affected

6.1.0, 6.2.0, 6.3.0 and develop @ 48a46c71, all reproduced; PHP 8.4.8, Redis 8.0.2.

#2357 / #2358 fixed the other half of this: the reconnect no longer resets the socket mode, but the reply callbacks were not considered. The symptom has come up before without a reproducible case — #1655 (closed into #1668), #1652, #429. #2829 looks similar but has a different cause.

(PR is currently being prepared)