Skip to content

Commit a255066

Browse files
authored
Merge pull request #255 from redis-rb/middleware-final-errors
Add `RedisClient::Error#final?` to segregate retriable errors in middlewares
2 parents 9770028 + 1befb5a commit a255066

File tree

9 files changed

+135
-7
lines changed

9 files changed

+135
-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+
- Add `RedisClient::Error#final?` and `#retriable?` to allow middleware to filter out non-final errors.
34
- Fix precedence of `db: nil` initialization parameter.
45

56
```ruby

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,29 @@ RedisClient.register(MyGlobalRedisInstrumentation)
459459

460460
redis_config = RedisClient.config(custom: { tags: { "environment": Rails.env }})
461461
```
462+
463+
### Instrumenting Errors
464+
465+
It is important to note that when `reconnect_attempts` is enabled, all network errors are reported to the middlewares,
466+
even the ones that will be retried.
467+
468+
In many cases you may want to ignore retriable errors, or report them differently:
469+
470+
```ruby
471+
module MyGlobalRedisInstrumentation
472+
def call(command, redis_config)
473+
super
474+
rescue RedisClient::Error => error
475+
if error.final?
476+
# Error won't be retried.
477+
else
478+
# Error will be retried.
479+
end
480+
raise
481+
end
482+
end
483+
```
484+
462485
### Timeouts
463486

464487
The client allows you to configure connect, read, and write timeouts.

hiredis-client/lib/redis_client/hiredis_connection.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,19 +100,21 @@ def read(timeout = nil)
100100
end
101101
end
102102
rescue SystemCallError, IOError => error
103-
raise ConnectionError.with_config(error.message, config)
103+
raise connection_error(error.message)
104104
rescue Error => error
105105
error._set_config(config)
106+
error._set_retry_attempt(@retry_attempt)
106107
raise error
107108
end
108109

109110
def write(command)
110111
_write(command)
111112
flush
112113
rescue SystemCallError, IOError => error
113-
raise ConnectionError.with_config(error.message, config)
114+
raise connection_error(error.message)
114115
rescue Error => error
115116
error._set_config(config)
117+
error._set_retry_attempt(@retry_attempt)
116118
raise error
117119
end
118120

@@ -122,9 +124,10 @@ def write_multi(commands)
122124
end
123125
flush
124126
rescue SystemCallError, IOError => error
125-
raise ConnectionError.with_config(error.message, config)
127+
raise connection_error(error.message)
126128
rescue Error => error
127129
error._set_config(config)
130+
error._set_retry_attempt(@retry_attempt)
128131
raise error
129132
end
130133

lib/redis_client.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,27 @@ def message
9999
end
100100
end
101101

102+
module Retriable
103+
def _set_retry_attempt(retry_attempt)
104+
@retry_attempt = retry_attempt
105+
end
106+
107+
def retry_attempt
108+
@retry_attempt || 0
109+
end
110+
111+
def retriable?
112+
!!@retry_attempt
113+
end
114+
115+
def final?
116+
!@retry_attempt
117+
end
118+
end
119+
102120
class Error < StandardError
103121
include HasConfig
122+
include Retriable
104123

105124
def self.with_config(message, config = nil)
106125
error = new(message)
@@ -706,6 +725,7 @@ def ensure_connected(retryable: true)
706725
close if !config.inherit_socket && @pid != PIDCache.pid
707726

708727
if @disable_reconnection
728+
@raw_connection.retry_attempt = nil
709729
if block_given?
710730
yield @raw_connection
711731
else
@@ -717,6 +737,7 @@ def ensure_connected(retryable: true)
717737
preferred_error = nil
718738
begin
719739
connection = raw_connection
740+
connection.retry_attempt = config.retriable?(tries) ? tries : nil
720741
if block_given?
721742
yield connection
722743
else
@@ -744,6 +765,7 @@ def ensure_connected(retryable: true)
744765
connection = ensure_connected
745766
begin
746767
@disable_reconnection = true
768+
@raw_connection.retry_attempt = nil
747769
yield connection
748770
rescue ConnectionError, ProtocolError
749771
close

lib/redis_client/config.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ def new_client(**kwargs)
140140
@client_implementation.new(self, **kwargs)
141141
end
142142

143+
def retriable?(attempt)
144+
@reconnect_attempts && @reconnect_attempts[attempt]
145+
end
146+
143147
def retry_connecting?(attempt, _error)
144148
if @reconnect_attempts
145149
if (pause = @reconnect_attempts[attempt])

lib/redis_client/connection_mixin.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
class RedisClient
44
module ConnectionMixin
5+
attr_accessor :retry_attempt
6+
57
def initialize
68
@pending_reads = 0
9+
@retry_attempt = nil
710
end
811

912
def reconnect
@@ -33,6 +36,7 @@ def call(command, timeout)
3336
if result.is_a?(Error)
3437
result._set_command(command)
3538
result._set_config(config)
39+
result._set_retry_attempt(@retry_attempt)
3640
raise result
3741
else
3842
result
@@ -61,6 +65,7 @@ def call_pipelined(commands, timeouts, exception: true)
6165
elsif result.is_a?(Error)
6266
result._set_command(commands[index])
6367
result._set_config(config)
68+
result._set_retry_attempt(@retry_attempt)
6469
first_exception ||= result
6570
end
6671

@@ -82,5 +87,15 @@ def connection_timeout(timeout)
8287
# to account for the network delay.
8388
timeout + config.read_timeout
8489
end
90+
91+
def protocol_error(message)
92+
ProtocolError.with_config(message, config)
93+
end
94+
95+
def connection_error(message)
96+
error = ConnectionError.with_config(message, config)
97+
error._set_retry_attempt(@retry_attempt)
98+
error
99+
end
85100
end
86101
end

lib/redis_client/ruby_connection.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def write(command)
7575
begin
7676
@io.write(buffer)
7777
rescue SystemCallError, IOError, OpenSSL::SSL::SSLError => error
78-
raise ConnectionError.with_config(error.message, config)
78+
raise connection_error(error.message)
7979
end
8080
end
8181

@@ -87,7 +87,7 @@ def write_multi(commands)
8787
begin
8888
@io.write(buffer)
8989
rescue SystemCallError, IOError, OpenSSL::SSL::SSLError => error
90-
raise ConnectionError.with_config(error.message, config)
90+
raise connection_error(error.message)
9191
end
9292
end
9393

@@ -100,7 +100,7 @@ def read(timeout = nil)
100100
rescue RedisClient::RESP3::UnknownType => error
101101
raise RedisClient::ProtocolError.with_config(error.message, config)
102102
rescue SystemCallError, IOError, OpenSSL::SSL::SSLError => error
103-
raise ConnectionError.with_config(error.message, config)
103+
raise connection_error(error.message)
104104
end
105105

106106
def measure_round_trip_delay

test/redis_client/middlewares_test.rb

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,66 @@ def test_multi_instrumentation
6969
]
7070
end
7171

72+
def test_final_errors
73+
client = new_client(reconnect_attempts: 1)
74+
simulate_network_errors(client, ["PING"]) do
75+
assert_equal("PONG", client.call("PING"))
76+
end
77+
78+
calls = TestMiddleware.calls.select { |type, _| type == :call }
79+
assert_equal 2, calls.size
80+
81+
call = calls[0]
82+
assert_equal :error, call[1]
83+
assert_equal ["PING"], call[2]
84+
refute_predicate call[3], :final?
85+
86+
call = calls[1]
87+
assert_equal :success, call[1]
88+
assert_equal ["PING"], call[2]
89+
90+
TestMiddleware.calls.clear
91+
92+
client = new_client(reconnect_attempts: 1)
93+
simulate_network_errors(client, ["PING", "PING"]) do
94+
assert_raises ConnectionError do
95+
client.call("PING")
96+
end
97+
end
98+
99+
calls = TestMiddleware.calls.select { |type, _| type == :call }
100+
assert_equal 2, calls.size
101+
102+
call = calls[0]
103+
assert_equal :error, call[1]
104+
assert_equal ["PING"], call[2]
105+
refute_predicate call[3], :final?
106+
107+
call = calls[1]
108+
assert_equal :error, call[1]
109+
assert_equal ["PING"], call[2]
110+
assert_predicate call[3], :final?
111+
112+
TestMiddleware.calls.clear
113+
114+
client = new_client(reconnect_attempts: 1)
115+
simulate_network_errors(client, ["PING"]) do
116+
assert_raises ConnectionError do
117+
client.call_once("PING")
118+
end
119+
end
120+
121+
calls = TestMiddleware.calls.select { |type, _| type == :call }
122+
assert_equal 1, calls.size
123+
124+
call = calls[0]
125+
assert_equal :error, call[1]
126+
assert_equal ["PING"], call[2]
127+
assert_predicate call[3], :final?
128+
129+
TestMiddleware.calls.clear
130+
end
131+
72132
module DummyMiddleware
73133
def call(command, _config, &_)
74134
command

test/support/client_test_helper.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def write(command)
2121
def read(*)
2222
@fail_now ||= false
2323
if @fail_now
24-
raise ::RedisClient::ConnectionError, "simulated failure"
24+
raise connection_error("simulated failure")
2525
end
2626

2727
super

0 commit comments

Comments
 (0)