Skip to content

Commit 7500ee7

Browse files
underwoo16byroot
andcommitted
Fix Error#final? accuracy.
- During reconnection attempt - With Protocol errors - With Command errors (always final). Co-Authored-By: Jean Boussier <[email protected]>
1 parent 72c72d4 commit 7500ee7

File tree

5 files changed

+126
-7
lines changed

5 files changed

+126
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Unreleased
22

3+
- Fix a few corner cases where `RedisClient::Error#final?` was innacurate.
34
- hiredis-client: Properly reconnect to the new leader after a sentinel failover.
45

56
# 0.26.0

lib/redis_client.rb

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,23 @@ def final?
117117
end
118118
end
119119

120+
module Final
121+
def _set_retry_attempt(_retry_attempt)
122+
end
123+
124+
def retry_attempt
125+
0
126+
end
127+
128+
def retriable?
129+
false
130+
end
131+
132+
def final?
133+
true
134+
end
135+
end
136+
120137
class Error < StandardError
121138
include HasConfig
122139
include Retriable
@@ -132,6 +149,7 @@ def self.with_config(message, config = nil)
132149
UnsupportedServer = Class.new(Error)
133150

134151
ConnectionError = Class.new(Error)
152+
135153
CannotConnectError = Class.new(ConnectionError)
136154

137155
FailoverError = Class.new(ConnectionError)
@@ -161,6 +179,7 @@ def initialize(message = nil, code = nil)
161179
class CommandError < Error
162180
include HasCommand
163181
include HasCode
182+
include Final
164183

165184
class << self
166185
def parse(error_message)
@@ -736,8 +755,7 @@ def ensure_connected(retryable: true)
736755
connection = nil
737756
preferred_error = nil
738757
begin
739-
connection = raw_connection
740-
connection.retry_attempt = config.retriable?(tries) ? tries : nil
758+
connection = raw_connection(config.retriable?(tries) ? tries : nil)
741759
if block_given?
742760
yield connection
743761
else
@@ -776,14 +794,15 @@ def ensure_connected(retryable: true)
776794
end
777795
end
778796

779-
def raw_connection
797+
def raw_connection(retry_attempt = nil)
780798
if @raw_connection.nil? || !@raw_connection.revalidate
781-
connect
799+
connect(retry_attempt)
782800
end
801+
@raw_connection.retry_attempt = retry_attempt
783802
@raw_connection
784803
end
785804

786-
def connect
805+
def connect(retry_attempt = nil)
787806
@pid = PIDCache.pid
788807

789808
if @raw_connection&.revalidate
@@ -799,6 +818,7 @@ def connect
799818
write_timeout: write_timeout,
800819
)
801820
end
821+
@raw_connection.retry_attempt = retry_attempt
802822
end
803823

804824
prelude = config.connection_prelude.dup

lib/redis_client/connection_mixin.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@ def connection_timeout(timeout)
9292
end
9393

9494
def protocol_error(message)
95-
ProtocolError.with_config(message, config)
95+
error = ProtocolError.with_config(message, config)
96+
error._set_retry_attempt(@retry_attempt)
97+
error
9698
end
9799

98100
def connection_error(message)

lib/redis_client/ruby_connection.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ def write(command)
7373
@io.write(buffer)
7474
rescue SystemCallError, IOError, OpenSSL::SSL::SSLError => error
7575
raise connection_error(error.message)
76+
rescue Error => error
77+
error._set_config(config)
78+
error._set_retry_attempt(@retry_attempt)
79+
raise error
7680
end
7781
end
7882

@@ -85,6 +89,10 @@ def write_multi(commands)
8589
@io.write(buffer)
8690
rescue SystemCallError, IOError, OpenSSL::SSL::SSLError => error
8791
raise connection_error(error.message)
92+
rescue Error => error
93+
error._set_config(config)
94+
error._set_retry_attempt(@retry_attempt)
95+
raise error
8896
end
8997
end
9098

@@ -95,9 +103,13 @@ def read(timeout = nil)
95103
@io.with_timeout(timeout) { RESP3.load(@io) }
96104
end
97105
rescue RedisClient::RESP3::UnknownType => error
98-
raise RedisClient::ProtocolError.with_config(error.message, config)
106+
raise protocol_error(error.message)
99107
rescue SystemCallError, IOError, OpenSSL::SSL::SSLError => error
100108
raise connection_error(error.message)
109+
rescue Error => error
110+
error._set_config(config)
111+
error._set_retry_attempt(@retry_attempt)
112+
raise error
101113
end
102114

103115
def measure_round_trip_delay

test/redis_client/middlewares_test.rb

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,90 @@ def test_final_errors
129129
TestMiddleware.calls.clear
130130
end
131131

132+
def test_final_errors_during_reconnect
133+
client = new_client(reconnect_attempts: 1)
134+
simulate_network_errors(client, ["PING", "HELLO"]) do
135+
assert_raises ConnectionError do
136+
client.call("PING")
137+
end
138+
end
139+
140+
calls = TestMiddleware.calls.select { |type, _| type == :call }
141+
assert_equal 1, calls.size
142+
143+
call = calls[0]
144+
assert_equal :error, call[1]
145+
assert_equal ["PING"], call[2]
146+
refute_predicate call[3], :final?
147+
148+
pipeline_calls = TestMiddleware.calls.select { |type, _| type == :pipeline }
149+
assert_equal 2, pipeline_calls.size
150+
151+
failing_pipeline = pipeline_calls[1]
152+
assert_equal :error, failing_pipeline[1]
153+
assert_equal [["HELLO", "3"]], failing_pipeline[2]
154+
assert_predicate failing_pipeline[3], :final?
155+
end
156+
157+
def test_command_error_final
158+
tcp_server = TCPServer.new("127.0.0.1", 0)
159+
tcp_server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
160+
port = tcp_server.addr[1]
161+
162+
server_thread = Thread.new do
163+
session = tcp_server.accept
164+
session.write("-Whoops\r\n")
165+
session.close
166+
end
167+
168+
assert_raises CommandError do
169+
new_client(host: "127.0.0.1", port: port, reconnect_attempts: 1, protocol: 2).call("PING")
170+
end
171+
172+
calls = TestMiddleware.calls.select { |type, _| type == :call }
173+
assert_equal 1, calls.size
174+
call = calls[0]
175+
assert_equal :error, call[1]
176+
assert_equal ["PING"], call[2]
177+
assert_predicate call[3], :final?
178+
ensure
179+
server_thread&.kill
180+
end
181+
182+
def test_protocol_error
183+
tcp_server = TCPServer.new("127.0.0.1", 0)
184+
tcp_server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
185+
port = tcp_server.addr[1]
186+
187+
server_thread = Thread.new do
188+
2.times do
189+
session = tcp_server.accept
190+
session.write("garbage\r\n")
191+
session.flush
192+
session.close
193+
end
194+
end
195+
196+
assert_raises ProtocolError do
197+
new_client(host: "127.0.0.1", port: port, reconnect_attempts: 1, protocol: 2).call("PING")
198+
end
199+
200+
calls = TestMiddleware.calls.select { |type, _| type == :call }
201+
assert_equal 2, calls.size
202+
203+
call = calls[0]
204+
assert_equal :error, call[1]
205+
assert_equal ["PING"], call[2]
206+
refute_predicate call[3], :final?
207+
208+
call = calls[1]
209+
assert_equal :error, call[1]
210+
assert_equal ["PING"], call[2]
211+
assert_predicate call[3], :final?
212+
ensure
213+
server_thread&.kill
214+
end
215+
132216
module DummyMiddleware
133217
def call(command, _config, &_)
134218
command

0 commit comments

Comments
 (0)