Skip to content

Commit 45a6072

Browse files
authored
Merge pull request #259 from underwoo16/retry-attempt-fixes
Set retry_attempt more consistently for `Error#final?`
2 parents 72c72d4 + 69cb23a commit 45a6072

File tree

5 files changed

+116
-4
lines changed

5 files changed

+116
-4
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: 22 additions & 1 deletion
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
@@ -161,6 +178,7 @@ def initialize(message = nil, code = nil)
161178
class CommandError < Error
162179
include HasCommand
163180
include HasCode
181+
include Final
164182

165183
class << self
166184
def parse(error_message)
@@ -231,6 +249,7 @@ def initialize(config, **)
231249
@middlewares = config.middlewares_stack.new(self)
232250
@raw_connection = nil
233251
@disable_reconnection = false
252+
@retry_attempt = nil
234253
end
235254

236255
def inspect
@@ -736,8 +755,8 @@ def ensure_connected(retryable: true)
736755
connection = nil
737756
preferred_error = nil
738757
begin
758+
@retry_attempt = config.retriable?(tries) ? tries : nil
739759
connection = raw_connection
740-
connection.retry_attempt = config.retriable?(tries) ? tries : nil
741760
if block_given?
742761
yield connection
743762
else
@@ -780,6 +799,7 @@ def raw_connection
780799
if @raw_connection.nil? || !@raw_connection.revalidate
781800
connect
782801
end
802+
@raw_connection.retry_attempt = @retry_attempt
783803
@raw_connection
784804
end
785805

@@ -800,6 +820,7 @@ def connect
800820
)
801821
end
802822
end
823+
@raw_connection.retry_attempt = @retry_attempt
803824

804825
prelude = config.connection_prelude.dup
805826

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: 6 additions & 2 deletions
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

@@ -94,8 +98,8 @@ def read(timeout = nil)
9498
else
9599
@io.with_timeout(timeout) { RESP3.load(@io) }
96100
end
97-
rescue RedisClient::RESP3::UnknownType => error
98-
raise RedisClient::ProtocolError.with_config(error.message, config)
101+
rescue RedisClient::RESP3::Error => error
102+
raise protocol_error(error.message)
99103
rescue SystemCallError, IOError, OpenSSL::SSL::SSLError => error
100104
raise connection_error(error.message)
101105
end

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)