fix(queue): recover a Redis connection whose client phpredis marked failed - #258
Merged
Conversation
…ailed phpredis retries a dropped socket on its own, but once the peer stays away past that budget it parks the client in a failed state for good. Connection\ Redis cached that object and handed it back for every later command, so a worker that lived through a broker failover longer than those retries failed every ack with "Redis server ... went away" until the process was restarted. Seen on Appwrite Cloud when a Dragonfly primary moved: the consume side reconnected (the broker already handles that around receive()) while the command connection behind Locking never did, and every execution job was marked failed for half an hour. Every command now goes through call(), which drops the cached client and runs the command once more on a fresh one when phpredis reports a transport error. Server-side errors still surface unchanged, and a second transport failure propagates. RedisConnectionRecoveryTest reproduces the failed state deterministically: a client with no retry budget whose socket the server closes, then the next command. Red before this change at Redis.php:126, the production stack's exact line; green after. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
loks0n
requested review from
ChiragAgg5k,
abnegate and
lohanidamodar
as code owners
September 11, 2026 01:01
Contributor
|
Benchmark resultsqueue — workload shapes across both concurrency axes (4 cores, 600 messages, median of 3)
Shared CI runners — treat absolute numbers as rough, compare modes within a run. Commit 8c4d4b2. |
…th a real outage Review: a blind retry could double-apply a push, pop or counter whose reply was lost after the server ran it. call() now always drops the failed client so the next command heals, but replays only commands that are safe to repeat (del, lrem, get, llen, lrange, set); pops, pushes and counters surface the error and leave the decision to the broker. The test no longer subclasses the adapter or touches phpredis options. It talks to the compose redis through a TCP proxy it stops and starts, which is what a broker failover looks like from a worker, and asserts on observable behaviour only: a command fails while the server is gone, the first command after it is back succeeds, and a push that failed is not replayed. Endpoint comes from REDIS_URL like the NATS tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…replayed Stopping the proxy before the push meant Redis never ran it, so a replaying implementation and a non-replaying one both left the list at one element and the test could not tell them apart. The proxy can now, on request, forward one client chunk upstream and close both sockets before the reply: the server runs the command, the client only sees a read error. With that the list holds two entries after the failed push, and a replaying implementation is caught at three. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Connection\Redisruns every command through acall()wrapper: on a phpredis transport error ("went away", "Connection lost", "Connection closed", "Connection refused", "read error on connection") it drops the cached\Redisclient and runs the command once more on a fresh connection. Any otherRedisException(WRONGTYPE, OOM, ...) still surfaces as before, and a second transport failure propagates.New e2e test
RedisConnectionRecoveryTest, registered in thee2esuite against the compose redis on 16379.Why
phpredis retries a dropped socket by itself, but once the peer stays away past its retry budget it sets the client to
REDIS_SOCK_STATUS_FAILEDfor good, after which every command throwsRedis server <host>:<port> went away.getRedis()cached that object and returned it forever.On Appwrite Cloud this hit
worker-executionswhen a Dragonfly primary moved during a rollout: the consume side recovered becauseBroker\Redis::receive()already closes and reconnects, but the command connection behindLockingnever did, so every execution job failed at ack with the trace below for 30 minutes until the pod was restarted.TDD
The test reproduces the terminal phpredis state deterministically without an outage: a client with
OPT_MAX_RETRIES0 whose own socket the server closes viaCLIENT KILL ID, then a command.RedisException: Connection lostatRedis.php:126, the production line.composer test(113 unit tests) andSwooleTestagainst a live worker pass.bin/monorepo check queue --fixapplied; the remaining PHPStan findings inBroker/Nats.phpandBroker/Pool.phpare from registry-resolved siblings in my local vendor and are not in this diff (main CI is green).Notes
RedisClustercaches its client the same way and would benefit from the same wrapper; left out to keep this to the reported path.🤖 Generated with Claude Code