Skip to content

Commit 8620e74

Browse files
committed
Add RedisClient::Error#final? to segregate retriable errors in middlewares.
Fix: #254 Fix: #119 Ref: #119 Middlewares witness all network errors, but currently have no way of knowing whether the error is final or is about to be retried. In many case, you do want to distinguish the two because a low number of transcient network errors can be expected.
1 parent 9770028 commit 8620e74

File tree

9 files changed

+134
-7
lines changed

9 files changed

+134
-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: 21 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

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+
refute_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)