redis-client 0.4.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/Gemfile.lock +1 -1
- data/README.md +20 -9
- data/Rakefile +2 -2
- data/lib/redis_client/command_builder.rb +12 -8
- data/lib/redis_client/config.rb +43 -16
- data/lib/redis_client/connection_mixin.rb +2 -0
- data/lib/redis_client/decorator.rb +2 -2
- data/lib/redis_client/pooled.rb +1 -1
- data/lib/redis_client/ruby_connection/resp3.rb +11 -7
- data/lib/redis_client/ruby_connection.rb +5 -5
- data/lib/redis_client/sentinel_config.rb +22 -3
- data/lib/redis_client/version.rb +1 -1
- data/lib/redis_client.rb +144 -34
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 05e24d8136cd8dc878d579c850297b1a016133447fd1f6135bd29dba55cd764e
|
4
|
+
data.tar.gz: 9f67aa59e1dfe2370f5f786ef3dea8076abe172301d263a13624b65cb2ab3888
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2e18d178c2d520073189c71f643c7da18f46f7324c75c8757fc28a8673599064c1678186b4a6833beacf40068498cb5bf19dc6175040caedc8e9286b13e079c2
|
7
|
+
data.tar.gz: d682b304d13901199f71e3c77561eb6562d7dddeae37068e9a20c0e8383a7c12bfbd5c1b250a506990425f126884a5f8946ea7df335bf31ee89713df187b3490
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,29 @@
|
|
1
1
|
# Unreleased
|
2
2
|
|
3
|
+
# 0.6.0
|
4
|
+
|
5
|
+
- Added `protocol: 2` options to talk with Redis 5 and older servers.
|
6
|
+
- Added `_v` versions of `call` methods to make it easier to pass commands as arrays without splating.
|
7
|
+
- Fix calling `blocking_call` with a block in a pipeline.
|
8
|
+
- `blocking_call` now raise `ReadTimeoutError` if the command didn't complete in time.
|
9
|
+
- Fix `blocking_call` to not respect `retry_attempts` on timeout.
|
10
|
+
- Stop parsing RESP3 sets as Ruby Set instances.
|
11
|
+
- Fix `SystemStackError` when parsing very large hashes. Fix: #30
|
12
|
+
- `hiredis` now more properly release the GVL when doing IOs.
|
13
|
+
|
14
|
+
# 0.5.1
|
15
|
+
|
16
|
+
- Fix a regression in the `scan` familly of methods, they would raise with `ArgumentError: can't issue an empty redis command`. Fix: #24
|
17
|
+
|
18
|
+
# 0.5.0
|
19
|
+
|
20
|
+
- Fix handling of connection URLs with empty passwords (`redis://:pass@example.com`).
|
21
|
+
- Handle URLs with IPv6 hosts.
|
22
|
+
- Add `RedisClient::Config#server_url` as a quick way to identify which server the client is pointing to.
|
23
|
+
- Add `CommandError#command` to expose the command that caused the error.
|
24
|
+
- Raise a more explicit error when connecting to older redises without RESP3 support (5.0 and older).
|
25
|
+
- Properly reject empty commands early.
|
26
|
+
|
3
27
|
# 0.4.0
|
4
28
|
|
5
29
|
- The `hiredis` driver have been moved to the `hiredis-client` gem.
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
`redis-client` is a simple, low-level, client for Redis 6+.
|
4
4
|
|
5
|
-
Contrary to the `redis` gem, `redis-client` doesn't try to map all
|
5
|
+
Contrary to the `redis` gem, `redis-client` doesn't try to map all Redis commands to Ruby constructs,
|
6
6
|
it merely is a thin wrapper on top of the RESP3 protocol.
|
7
7
|
|
8
8
|
## Installation
|
@@ -63,7 +63,7 @@ redis.call("GET", "mykey")
|
|
63
63
|
### Configuration
|
64
64
|
|
65
65
|
- `url`: A Redis connection URL, e.g. `redis://example.com:6379/5`, a `rediss://` scheme enable SSL, and the path is interpreted as a database number.
|
66
|
-
Note
|
66
|
+
Note that all other configurations take precedence, e.g. `RedisClient.config(url: "redis://localhost:3000" port: 6380)` will connect on port `6380`.
|
67
67
|
- `host`: The server hostname or IP address. Defaults to `"localhost"`.
|
68
68
|
- `port`: The server port. Defaults to `6379`.
|
69
69
|
- `path`: The path to a UNIX socket, if set `url`, `host` and `port` are ignored.
|
@@ -81,6 +81,7 @@ redis.call("GET", "mykey")
|
|
81
81
|
- `read_timeout`: The read timeout, takes precedence over the general timeout when reading responses from the server.
|
82
82
|
- `write_timeout`: The write timeout, takes precedence over the general timeout when sending commands to the server.
|
83
83
|
- `reconnect_attempts`: Specify how many times the client should retry to send queries. Defaults to `0`. Makes sure to read the [reconnection section](#reconnection) before enabling it.
|
84
|
+
- `protocol:` The version of the RESP protocol to use. Default to `3`.
|
84
85
|
|
85
86
|
### Sentinel support
|
86
87
|
|
@@ -142,7 +143,7 @@ is equivalent to:
|
|
142
143
|
redis.call("LPUSH", "list", "1", "2", "3", "4")
|
143
144
|
```
|
144
145
|
|
145
|
-
Hashes are
|
146
|
+
Hashes are flattened as well:
|
146
147
|
|
147
148
|
```ruby
|
148
149
|
redis.call("HMSET", "hash", { "foo" => "1", "bar" => "2" })
|
@@ -154,7 +155,7 @@ is equivalent to:
|
|
154
155
|
redis.call("HMSET", "hash", "foo", "1", "bar", "2")
|
155
156
|
```
|
156
157
|
|
157
|
-
Any other type requires the caller to
|
158
|
+
Any other type requires the caller to explicitly cast the argument as a string.
|
158
159
|
|
159
160
|
Keywords arguments are treated as Redis command flags:
|
160
161
|
|
@@ -170,7 +171,7 @@ redis.call("SET", "mykey", "value", "nx", "ex", "60")
|
|
170
171
|
redis.call("SET", "mykey", "value")
|
171
172
|
```
|
172
173
|
|
173
|
-
If flags are built dynamically, you'll have to
|
174
|
+
If flags are built dynamically, you'll have to explicitly pass them as keyword arguments with `**`:
|
174
175
|
|
175
176
|
```ruby
|
176
177
|
flags = {}
|
@@ -185,7 +186,7 @@ unclosed hash literals with string keys may be interpreted differently:
|
|
185
186
|
redis.call("HMSET", "hash", "foo" => "bar")
|
186
187
|
```
|
187
188
|
|
188
|
-
On Ruby 2 `"foo" => "bar"` will be passed as a
|
189
|
+
On Ruby 2 `"foo" => "bar"` will be passed as a positional argument, but on Ruby 3 it will be interpreted as keyword
|
189
190
|
arguments. To avoid such problem, make sure to enclose hash literals:
|
190
191
|
|
191
192
|
```ruby
|
@@ -196,7 +197,7 @@ redis.call("HMSET", "hash", { "foo" => "bar" })
|
|
196
197
|
|
197
198
|
Contrary to the `redis` gem, `redis-client` doesn't do any type casting on the return value of commands.
|
198
199
|
|
199
|
-
If you wish to cast the return value, you can pass a block to the `#call`
|
200
|
+
If you wish to cast the return value, you can pass a block to the `#call` family of methods:
|
200
201
|
|
201
202
|
```ruby
|
202
203
|
redis.call("INCR", "counter") # => 1
|
@@ -207,6 +208,16 @@ redis.call("EXISTS", "counter") # => 1
|
|
207
208
|
redis.call("EXISTS", "counter") { |c| c > 0 } # => true
|
208
209
|
```
|
209
210
|
|
211
|
+
### `*_v` methods
|
212
|
+
|
213
|
+
In some it's more convenient to pass commands as arrays, for that `_v` versions of `call` methods are available.
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
redis.call_v(["MGET"] + keys)
|
217
|
+
redis.blocking_call_v(1, ["MGET"] + keys)
|
218
|
+
redis.call_once_v(1, ["MGET"] + keys)
|
219
|
+
```
|
220
|
+
|
210
221
|
### Blocking commands
|
211
222
|
|
212
223
|
For blocking commands such as `BRPOP`, a custom timeout duration can be passed as first argument of the `#blocking_call` method:
|
@@ -215,7 +226,7 @@ For blocking commands such as `BRPOP`, a custom timeout duration can be passed a
|
|
215
226
|
redis.blocking_call(timeout, "BRPOP", "key", 0)
|
216
227
|
```
|
217
228
|
|
218
|
-
If `timeout` is reached, `#blocking_call`
|
229
|
+
If `timeout` is reached, `#blocking_call` raises `RedisClient::ReadTimeoutError` and doesn't retry regardless of the `reconnect_attempts` configuration.
|
219
230
|
|
220
231
|
`timeout` is expressed in seconds, you can pass `false` or `0` to mean no timeout.
|
221
232
|
|
@@ -297,7 +308,7 @@ end
|
|
297
308
|
|
298
309
|
If the transaction wasn't successful, `#multi` will return `nil`.
|
299
310
|
|
300
|
-
Note that transactions using optimistic locking aren't automatically retried
|
311
|
+
Note that transactions using optimistic locking aren't automatically retried upon connection errors.
|
301
312
|
|
302
313
|
### Publish / Subscribe
|
303
314
|
|
data/Rakefile
CHANGED
@@ -107,8 +107,8 @@ end
|
|
107
107
|
|
108
108
|
if hiredis_supported
|
109
109
|
task default: %i[compile test rubocop]
|
110
|
-
task ci: %i[compile test]
|
110
|
+
task ci: %i[compile test:ruby test:hiredis]
|
111
111
|
else
|
112
112
|
task default: %i[test rubocop]
|
113
|
-
task ci: %i[test]
|
113
|
+
task ci: %i[test:ruby]
|
114
114
|
end
|
@@ -5,19 +5,17 @@ class RedisClient
|
|
5
5
|
extend self
|
6
6
|
|
7
7
|
if Symbol.method_defined?(:name)
|
8
|
-
def generate
|
8
|
+
def generate(args, kwargs = nil)
|
9
9
|
command = args.flat_map do |element|
|
10
10
|
case element
|
11
11
|
when Hash
|
12
12
|
element.flatten
|
13
|
-
when Set
|
14
|
-
element.to_a
|
15
13
|
else
|
16
14
|
element
|
17
15
|
end
|
18
16
|
end
|
19
17
|
|
20
|
-
kwargs
|
18
|
+
kwargs&.each do |key, value|
|
21
19
|
if value
|
22
20
|
if value == true
|
23
21
|
command << key.name
|
@@ -40,22 +38,24 @@ class RedisClient
|
|
40
38
|
end
|
41
39
|
end
|
42
40
|
|
41
|
+
if command.empty?
|
42
|
+
raise ArgumentError, "can't issue an empty redis command"
|
43
|
+
end
|
44
|
+
|
43
45
|
command
|
44
46
|
end
|
45
47
|
else
|
46
|
-
def generate
|
48
|
+
def generate(args, kwargs = nil)
|
47
49
|
command = args.flat_map do |element|
|
48
50
|
case element
|
49
51
|
when Hash
|
50
52
|
element.flatten
|
51
|
-
when Set
|
52
|
-
element.to_a
|
53
53
|
else
|
54
54
|
element
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
58
|
-
kwargs
|
58
|
+
kwargs&.each do |key, value|
|
59
59
|
if value
|
60
60
|
if value == true
|
61
61
|
command << key.to_s
|
@@ -76,6 +76,10 @@ class RedisClient
|
|
76
76
|
end
|
77
77
|
end
|
78
78
|
|
79
|
+
if command.empty?
|
80
|
+
raise ArgumentError, "can't issue an empty redis command"
|
81
|
+
end
|
82
|
+
|
79
83
|
command
|
80
84
|
end
|
81
85
|
end
|
data/lib/redis_client/config.rb
CHANGED
@@ -12,8 +12,8 @@ class RedisClient
|
|
12
12
|
DEFAULT_DB = 0
|
13
13
|
|
14
14
|
module Common
|
15
|
-
attr_reader :db, :
|
16
|
-
:connect_timeout, :read_timeout, :write_timeout, :driver, :connection_prelude
|
15
|
+
attr_reader :db, :password, :id, :ssl, :ssl_params, :command_builder,
|
16
|
+
:connect_timeout, :read_timeout, :write_timeout, :driver, :connection_prelude, :protocol
|
17
17
|
|
18
18
|
alias_method :ssl?, :ssl
|
19
19
|
|
@@ -29,10 +29,12 @@ class RedisClient
|
|
29
29
|
ssl: nil,
|
30
30
|
ssl_params: nil,
|
31
31
|
driver: nil,
|
32
|
+
protocol: 3,
|
33
|
+
client_implementation: RedisClient,
|
32
34
|
command_builder: CommandBuilder,
|
33
35
|
reconnect_attempts: false
|
34
36
|
)
|
35
|
-
@username = username
|
37
|
+
@username = username
|
36
38
|
@password = password
|
37
39
|
@db = db || DEFAULT_DB
|
38
40
|
@id = id
|
@@ -45,14 +47,23 @@ class RedisClient
|
|
45
47
|
|
46
48
|
@driver = driver ? RedisClient.driver(driver) : RedisClient.default_driver
|
47
49
|
|
50
|
+
@client_implementation = client_implementation
|
51
|
+
@protocol = protocol
|
52
|
+
unless protocol == 2 || protocol == 3
|
53
|
+
raise ArgumentError, "Unknown protocol version #{protocol.inspect}, expected 2 or 3"
|
54
|
+
end
|
55
|
+
|
48
56
|
@command_builder = command_builder
|
49
57
|
|
50
58
|
reconnect_attempts = Array.new(reconnect_attempts, 0).freeze if reconnect_attempts.is_a?(Integer)
|
51
59
|
@reconnect_attempts = reconnect_attempts
|
52
|
-
|
53
60
|
@connection_prelude = build_connection_prelude
|
54
61
|
end
|
55
62
|
|
63
|
+
def username
|
64
|
+
@username || DEFAULT_USERNAME
|
65
|
+
end
|
66
|
+
|
56
67
|
def sentinel?
|
57
68
|
false
|
58
69
|
end
|
@@ -63,7 +74,7 @@ class RedisClient
|
|
63
74
|
end
|
64
75
|
|
65
76
|
def new_client(**kwargs)
|
66
|
-
|
77
|
+
@client_implementation.new(self, **kwargs)
|
67
78
|
end
|
68
79
|
|
69
80
|
def retry_connecting?(attempt, _error)
|
@@ -79,17 +90,33 @@ class RedisClient
|
|
79
90
|
end
|
80
91
|
|
81
92
|
def ssl_context
|
82
|
-
@ssl_context ||= @driver.ssl_context(@ssl_params)
|
93
|
+
@ssl_context ||= @driver.ssl_context(@ssl_params || {})
|
94
|
+
end
|
95
|
+
|
96
|
+
def server_url
|
97
|
+
if path
|
98
|
+
"#{path}/#{db}"
|
99
|
+
else
|
100
|
+
"redis#{'s' if ssl?}://#{host}:#{port}/#{db}"
|
101
|
+
end
|
83
102
|
end
|
84
103
|
|
85
104
|
private
|
86
105
|
|
87
106
|
def build_connection_prelude
|
88
107
|
prelude = []
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
108
|
+
if protocol == 3
|
109
|
+
prelude << if @password
|
110
|
+
["HELLO", "3", "AUTH", @username || DEFAULT_USERNAME, @password]
|
111
|
+
else
|
112
|
+
["HELLO", "3"]
|
113
|
+
end
|
114
|
+
elsif @password
|
115
|
+
prelude << if @username && !@username.empty?
|
116
|
+
["AUTH", @username, @password]
|
117
|
+
else
|
118
|
+
["AUTH", @password]
|
119
|
+
end
|
93
120
|
end
|
94
121
|
|
95
122
|
if @db && @db != 0
|
@@ -110,15 +137,15 @@ class RedisClient
|
|
110
137
|
path: nil,
|
111
138
|
**kwargs
|
112
139
|
)
|
113
|
-
|
114
|
-
|
140
|
+
if url
|
141
|
+
uri = URI.parse(url)
|
115
142
|
kwargs[:ssl] = uri.scheme == "rediss" unless kwargs.key?(:ssl)
|
116
143
|
|
117
|
-
kwargs[:username] ||= uri.user && uri.
|
144
|
+
kwargs[:username] ||= uri.user if uri.password && !uri.user.empty?
|
118
145
|
|
119
146
|
kwargs[:password] ||= if uri.user && !uri.password
|
120
147
|
URI.decode_www_form_component(uri.user)
|
121
|
-
elsif uri
|
148
|
+
elsif uri.user && uri.password
|
122
149
|
URI.decode_www_form_component(uri.password)
|
123
150
|
end
|
124
151
|
|
@@ -127,8 +154,8 @@ class RedisClient
|
|
127
154
|
|
128
155
|
super(**kwargs)
|
129
156
|
|
130
|
-
@host = host || uri&.host || DEFAULT_HOST
|
131
|
-
@port = port || uri&.port || DEFAULT_PORT
|
157
|
+
@host = host || uri&.host&.sub(/\A\[(.*)\]\z/, '\1') || DEFAULT_HOST
|
158
|
+
@port = Integer(port || uri&.port || DEFAULT_PORT)
|
132
159
|
@path = path
|
133
160
|
end
|
134
161
|
end
|
@@ -6,6 +6,7 @@ class RedisClient
|
|
6
6
|
write(command)
|
7
7
|
result = read(timeout)
|
8
8
|
if result.is_a?(CommandError)
|
9
|
+
result._set_command(command)
|
9
10
|
raise result
|
10
11
|
else
|
11
12
|
result
|
@@ -23,6 +24,7 @@ class RedisClient
|
|
23
24
|
timeout = timeouts && timeouts[index]
|
24
25
|
result = read(timeout)
|
25
26
|
if result.is_a?(CommandError)
|
27
|
+
result._set_command(commands[index])
|
26
28
|
exception ||= result
|
27
29
|
end
|
28
30
|
results[index] = result
|
@@ -20,7 +20,7 @@ class RedisClient
|
|
20
20
|
@client = client
|
21
21
|
end
|
22
22
|
|
23
|
-
%i(call call_once blocking_call).each do |method|
|
23
|
+
%i(call call_v call_once call_once_v blocking_call blocking_call_v).each do |method|
|
24
24
|
class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
|
25
25
|
def #{method}(*args, &block)
|
26
26
|
@client.#{method}(*args, &block)
|
@@ -64,7 +64,7 @@ class RedisClient
|
|
64
64
|
RUBY
|
65
65
|
end
|
66
66
|
|
67
|
-
%i(id config size connect_timeout read_timeout write_timeout).each do |reader|
|
67
|
+
%i(id config size connect_timeout read_timeout write_timeout pubsub).each do |reader|
|
68
68
|
class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
|
69
69
|
def #{reader}
|
70
70
|
@client.#{reader}
|
data/lib/redis_client/pooled.rb
CHANGED
@@ -49,7 +49,7 @@ class RedisClient
|
|
49
49
|
pool.size
|
50
50
|
end
|
51
51
|
|
52
|
-
methods = %w(pipelined multi pubsub call call_once blocking_call)
|
52
|
+
methods = %w(pipelined multi pubsub call call_v call_once call_once_v blocking_call blocking_call_v)
|
53
53
|
iterable_methods = %w(scan sscan hscan zscan)
|
54
54
|
begin
|
55
55
|
methods.each do |method|
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "set"
|
4
|
-
|
5
3
|
class RedisClient
|
6
4
|
module RESP3
|
7
5
|
module_function
|
@@ -41,8 +39,6 @@ class RedisClient
|
|
41
39
|
case element
|
42
40
|
when Hash
|
43
41
|
element.flatten
|
44
|
-
when Set
|
45
|
-
element.to_a
|
46
42
|
else
|
47
43
|
element
|
48
44
|
end
|
@@ -55,7 +51,7 @@ class RedisClient
|
|
55
51
|
end
|
56
52
|
|
57
53
|
def new_buffer
|
58
|
-
String.new(encoding: Encoding::BINARY, capacity:
|
54
|
+
String.new(encoding: Encoding::BINARY, capacity: 127)
|
59
55
|
end
|
60
56
|
|
61
57
|
def dump_any(object, buffer)
|
@@ -144,11 +140,15 @@ class RedisClient
|
|
144
140
|
end
|
145
141
|
|
146
142
|
def parse_set(io)
|
147
|
-
parse_sequence(io, parse_integer(io))
|
143
|
+
parse_sequence(io, parse_integer(io))
|
148
144
|
end
|
149
145
|
|
150
146
|
def parse_map(io)
|
151
|
-
|
147
|
+
hash = {}
|
148
|
+
parse_integer(io).times do
|
149
|
+
hash[parse(io)] = parse(io)
|
150
|
+
end
|
151
|
+
hash
|
152
152
|
end
|
153
153
|
|
154
154
|
def parse_push(io)
|
@@ -156,6 +156,8 @@ class RedisClient
|
|
156
156
|
end
|
157
157
|
|
158
158
|
def parse_sequence(io, size)
|
159
|
+
return if size < 0 # RESP2 nil
|
160
|
+
|
159
161
|
array = Array.new(size)
|
160
162
|
size.times do |index|
|
161
163
|
array[index] = parse(io)
|
@@ -185,6 +187,8 @@ class RedisClient
|
|
185
187
|
|
186
188
|
def parse_blob(io)
|
187
189
|
bytesize = parse_integer(io)
|
190
|
+
return if bytesize < 0 # RESP2 nil type
|
191
|
+
|
188
192
|
str = io.read_chomp(bytesize)
|
189
193
|
str.force_encoding(Encoding.default_external)
|
190
194
|
str.force_encoding(Encoding::BINARY) unless str.valid_encoding?
|
@@ -60,9 +60,9 @@ class RedisClient
|
|
60
60
|
loop do
|
61
61
|
case status = socket.connect_nonblock(exception: false)
|
62
62
|
when :wait_readable
|
63
|
-
socket.to_io.wait_readable(connect_timeout) or raise
|
63
|
+
socket.to_io.wait_readable(connect_timeout) or raise CannotConnectError
|
64
64
|
when :wait_writable
|
65
|
-
socket.to_io.wait_writable(connect_timeout) or raise
|
65
|
+
socket.to_io.wait_writable(connect_timeout) or raise CannotConnectError
|
66
66
|
when socket
|
67
67
|
break
|
68
68
|
else
|
@@ -76,10 +76,8 @@ class RedisClient
|
|
76
76
|
read_timeout: read_timeout,
|
77
77
|
write_timeout: write_timeout,
|
78
78
|
)
|
79
|
-
rescue Errno::ETIMEDOUT => error
|
80
|
-
raise ConnectTimeoutError, error.message
|
81
79
|
rescue SystemCallError, OpenSSL::SSL::SSLError, SocketError => error
|
82
|
-
raise
|
80
|
+
raise CannotConnectError, error.message, error.backtrace
|
83
81
|
end
|
84
82
|
|
85
83
|
def connected?
|
@@ -125,6 +123,8 @@ class RedisClient
|
|
125
123
|
else
|
126
124
|
@io.with_timeout(timeout) { RESP3.load(@io) }
|
127
125
|
end
|
126
|
+
rescue RedisClient::RESP3::UnknownType => error
|
127
|
+
raise RedisClient::ProtocolError, error.message
|
128
128
|
rescue SystemCallError, IOError, OpenSSL::SSL::SSLError => error
|
129
129
|
raise ConnectionError, error.message
|
130
130
|
end
|
@@ -12,8 +12,21 @@ class RedisClient
|
|
12
12
|
raise ArgumentError, "Expected role to be either :master or :replica, got: #{role.inspect}"
|
13
13
|
end
|
14
14
|
|
15
|
+
@to_list_of_hash = @to_hash = nil
|
16
|
+
extra_config = {}
|
17
|
+
if client_config[:protocol] == 2
|
18
|
+
extra_config[:protocol] = client_config[:protocol]
|
19
|
+
@to_list_of_hash = lambda do |may_be_a_list|
|
20
|
+
if may_be_a_list.is_a?(Array)
|
21
|
+
may_be_a_list.map { |l| l.each_slice(2).to_h }
|
22
|
+
else
|
23
|
+
may_be_a_list
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
15
28
|
@name = name
|
16
|
-
@sentinel_configs = sentinels.map { |s| Config.new(**s) }
|
29
|
+
@sentinel_configs = sentinels.map { |s| Config.new(**extra_config, **s) }
|
17
30
|
@sentinels = {}.compare_by_identity
|
18
31
|
@role = role
|
19
32
|
@mutex = Mutex.new
|
@@ -90,7 +103,10 @@ class RedisClient
|
|
90
103
|
return Config.new(host: host, port: Integer(port), **@client_config)
|
91
104
|
end
|
92
105
|
end
|
93
|
-
|
106
|
+
rescue ConnectionError
|
107
|
+
raise ConnectionError, "No sentinels available"
|
108
|
+
else
|
109
|
+
raise ConnectionError, "Couldn't locate a replica for role: #{@name}"
|
94
110
|
end
|
95
111
|
|
96
112
|
def sentinel_client(sentinel_config)
|
@@ -99,13 +115,16 @@ class RedisClient
|
|
99
115
|
|
100
116
|
def resolve_replica
|
101
117
|
each_sentinel do |sentinel_client|
|
102
|
-
replicas = sentinel_client.call("SENTINEL", "replicas", @name)
|
118
|
+
replicas = sentinel_client.call("SENTINEL", "replicas", @name, &@to_list_of_hash)
|
103
119
|
next if replicas.empty?
|
104
120
|
|
105
121
|
replica = replicas.reject { |r| r["flags"].to_s.split(",").include?("disconnected") }.sample
|
106
122
|
replica ||= replicas.sample
|
107
123
|
return Config.new(host: replica["ip"], port: Integer(replica["port"]), **@client_config)
|
108
124
|
end
|
125
|
+
rescue ConnectionError
|
126
|
+
raise ConnectionError, "No sentinels available"
|
127
|
+
else
|
109
128
|
raise ConnectionError, "Couldn't locate a replica for role: #{@name}"
|
110
129
|
end
|
111
130
|
|
data/lib/redis_client/version.rb
CHANGED
data/lib/redis_client.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "set"
|
4
|
-
|
5
3
|
require "redis_client/version"
|
6
4
|
require "redis_client/command_builder"
|
7
5
|
require "redis_client/config"
|
@@ -78,17 +76,22 @@ class RedisClient
|
|
78
76
|
|
79
77
|
Error = Class.new(StandardError)
|
80
78
|
|
79
|
+
ProtocolError = Class.new(Error)
|
80
|
+
UnsupportedServer = Class.new(Error)
|
81
|
+
|
81
82
|
ConnectionError = Class.new(Error)
|
83
|
+
CannotConnectError = Class.new(ConnectionError)
|
82
84
|
|
83
85
|
FailoverError = Class.new(ConnectionError)
|
84
86
|
|
85
87
|
TimeoutError = Class.new(ConnectionError)
|
86
88
|
ReadTimeoutError = Class.new(TimeoutError)
|
87
89
|
WriteTimeoutError = Class.new(TimeoutError)
|
88
|
-
|
89
|
-
CheckoutTimeoutError = Class.new(ConnectTimeoutError)
|
90
|
+
CheckoutTimeoutError = Class.new(TimeoutError)
|
90
91
|
|
91
92
|
class CommandError < Error
|
93
|
+
attr_reader :command
|
94
|
+
|
92
95
|
class << self
|
93
96
|
def parse(error_message)
|
94
97
|
code = error_message.split(' ', 2).first
|
@@ -96,6 +99,10 @@ class RedisClient
|
|
96
99
|
klass.new(error_message)
|
97
100
|
end
|
98
101
|
end
|
102
|
+
|
103
|
+
def _set_command(command)
|
104
|
+
@command = command
|
105
|
+
end
|
99
106
|
end
|
100
107
|
|
101
108
|
AuthenticationError = Class.new(CommandError)
|
@@ -112,11 +119,11 @@ class RedisClient
|
|
112
119
|
|
113
120
|
class << self
|
114
121
|
def config(**kwargs)
|
115
|
-
Config.new(**kwargs)
|
122
|
+
Config.new(client_implementation: self, **kwargs)
|
116
123
|
end
|
117
124
|
|
118
125
|
def sentinel(**kwargs)
|
119
|
-
SentinelConfig.new(**kwargs)
|
126
|
+
SentinelConfig.new(client_implementation: self, **kwargs)
|
120
127
|
end
|
121
128
|
|
122
129
|
def new(arg = nil, **kwargs)
|
@@ -140,6 +147,11 @@ class RedisClient
|
|
140
147
|
@disable_reconnection = false
|
141
148
|
end
|
142
149
|
|
150
|
+
def inspect
|
151
|
+
id_string = " id=#{id}" if id
|
152
|
+
"#<#{self.class.name} #{config.server_url}#{id_string}>"
|
153
|
+
end
|
154
|
+
|
143
155
|
def size
|
144
156
|
1
|
145
157
|
end
|
@@ -171,7 +183,22 @@ class RedisClient
|
|
171
183
|
end
|
172
184
|
|
173
185
|
def call(*command, **kwargs)
|
174
|
-
command = @command_builder.generate
|
186
|
+
command = @command_builder.generate(command, kwargs)
|
187
|
+
result = ensure_connected do |connection|
|
188
|
+
Middlewares.call(command, config) do
|
189
|
+
connection.call(command, nil)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
if block_given?
|
194
|
+
yield result
|
195
|
+
else
|
196
|
+
result
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def call_v(command)
|
201
|
+
command = @command_builder.generate(command)
|
175
202
|
result = ensure_connected do |connection|
|
176
203
|
Middlewares.call(command, config) do
|
177
204
|
connection.call(command, nil)
|
@@ -186,7 +213,22 @@ class RedisClient
|
|
186
213
|
end
|
187
214
|
|
188
215
|
def call_once(*command, **kwargs)
|
189
|
-
command = @command_builder.generate
|
216
|
+
command = @command_builder.generate(command, kwargs)
|
217
|
+
result = ensure_connected(retryable: false) do |connection|
|
218
|
+
Middlewares.call(command, config) do
|
219
|
+
connection.call(command, nil)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
if block_given?
|
224
|
+
yield result
|
225
|
+
else
|
226
|
+
result
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def call_once_v(command)
|
231
|
+
command = @command_builder.generate(command)
|
190
232
|
result = ensure_connected(retryable: false) do |connection|
|
191
233
|
Middlewares.call(command, config) do
|
192
234
|
connection.call(command, nil)
|
@@ -201,14 +243,39 @@ class RedisClient
|
|
201
243
|
end
|
202
244
|
|
203
245
|
def blocking_call(timeout, *command, **kwargs)
|
204
|
-
command = @command_builder.generate
|
246
|
+
command = @command_builder.generate(command, kwargs)
|
247
|
+
error = nil
|
205
248
|
result = ensure_connected do |connection|
|
206
249
|
Middlewares.call(command, config) do
|
207
250
|
connection.call(command, timeout)
|
208
251
|
end
|
252
|
+
rescue ReadTimeoutError => error
|
253
|
+
break
|
209
254
|
end
|
210
255
|
|
211
|
-
if
|
256
|
+
if error
|
257
|
+
raise error
|
258
|
+
elsif block_given?
|
259
|
+
yield result
|
260
|
+
else
|
261
|
+
result
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
def blocking_call_v(timeout, command)
|
266
|
+
command = @command_builder.generate(command)
|
267
|
+
error = nil
|
268
|
+
result = ensure_connected do |connection|
|
269
|
+
Middlewares.call(command, config) do
|
270
|
+
connection.call(command, timeout)
|
271
|
+
end
|
272
|
+
rescue ReadTimeoutError => error
|
273
|
+
break
|
274
|
+
end
|
275
|
+
|
276
|
+
if error
|
277
|
+
raise error
|
278
|
+
elsif block_given?
|
212
279
|
yield result
|
213
280
|
else
|
214
281
|
result
|
@@ -220,8 +287,8 @@ class RedisClient
|
|
220
287
|
return to_enum(__callee__, *args, **kwargs)
|
221
288
|
end
|
222
289
|
|
223
|
-
args = @command_builder.generate
|
224
|
-
scan_list(1,
|
290
|
+
args = @command_builder.generate(["SCAN", 0] + args, kwargs)
|
291
|
+
scan_list(1, args, &block)
|
225
292
|
end
|
226
293
|
|
227
294
|
def sscan(key, *args, **kwargs, &block)
|
@@ -229,8 +296,8 @@ class RedisClient
|
|
229
296
|
return to_enum(__callee__, key, *args, **kwargs)
|
230
297
|
end
|
231
298
|
|
232
|
-
args = @command_builder.generate
|
233
|
-
scan_list(2,
|
299
|
+
args = @command_builder.generate(["SSCAN", key, 0] + args, kwargs)
|
300
|
+
scan_list(2, args, &block)
|
234
301
|
end
|
235
302
|
|
236
303
|
def hscan(key, *args, **kwargs, &block)
|
@@ -238,8 +305,8 @@ class RedisClient
|
|
238
305
|
return to_enum(__callee__, key, *args, **kwargs)
|
239
306
|
end
|
240
307
|
|
241
|
-
args = @command_builder.generate
|
242
|
-
scan_pairs(2,
|
308
|
+
args = @command_builder.generate(["HSCAN", key, 0] + args, kwargs)
|
309
|
+
scan_pairs(2, args, &block)
|
243
310
|
end
|
244
311
|
|
245
312
|
def zscan(key, *args, **kwargs, &block)
|
@@ -247,8 +314,8 @@ class RedisClient
|
|
247
314
|
return to_enum(__callee__, key, *args, **kwargs)
|
248
315
|
end
|
249
316
|
|
250
|
-
args = @command_builder.generate
|
251
|
-
scan_pairs(2,
|
317
|
+
args = @command_builder.generate(["ZSCAN", key, 0] + args, kwargs)
|
318
|
+
scan_pairs(2, args, &block)
|
252
319
|
end
|
253
320
|
|
254
321
|
def connected?
|
@@ -330,7 +397,12 @@ class RedisClient
|
|
330
397
|
end
|
331
398
|
|
332
399
|
def call(*command, **kwargs)
|
333
|
-
raw_connection.write(@command_builder.generate
|
400
|
+
raw_connection.write(@command_builder.generate(command, kwargs))
|
401
|
+
nil
|
402
|
+
end
|
403
|
+
|
404
|
+
def call_v(command)
|
405
|
+
raw_connection.write(@command_builder.generate(command))
|
334
406
|
nil
|
335
407
|
end
|
336
408
|
|
@@ -365,14 +437,29 @@ class RedisClient
|
|
365
437
|
end
|
366
438
|
|
367
439
|
def call(*command, **kwargs, &block)
|
368
|
-
command = @command_builder.generate
|
440
|
+
command = @command_builder.generate(command, kwargs)
|
369
441
|
(@blocks ||= [])[@commands.size] = block if block_given?
|
370
442
|
@commands << command
|
371
443
|
nil
|
372
444
|
end
|
373
445
|
|
374
|
-
def
|
375
|
-
command = @command_builder.generate
|
446
|
+
def call_v(command, &block)
|
447
|
+
command = @command_builder.generate(command)
|
448
|
+
(@blocks ||= [])[@commands.size] = block if block_given?
|
449
|
+
@commands << command
|
450
|
+
nil
|
451
|
+
end
|
452
|
+
|
453
|
+
def call_once(*command, **kwargs, &block)
|
454
|
+
command = @command_builder.generate(command, kwargs)
|
455
|
+
@retryable = false
|
456
|
+
(@blocks ||= [])[@commands.size] = block if block_given?
|
457
|
+
@commands << command
|
458
|
+
nil
|
459
|
+
end
|
460
|
+
|
461
|
+
def call_once_v(command, &block)
|
462
|
+
command = @command_builder.generate(command)
|
376
463
|
@retryable = false
|
377
464
|
(@blocks ||= [])[@commands.size] = block if block_given?
|
378
465
|
@commands << command
|
@@ -404,17 +491,14 @@ class RedisClient
|
|
404
491
|
end
|
405
492
|
|
406
493
|
def _coerce!(results)
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
end
|
494
|
+
results&.each_with_index do |result, index|
|
495
|
+
if result.is_a?(CommandError)
|
496
|
+
result._set_command(@commands[index + 1])
|
497
|
+
raise result
|
412
498
|
end
|
413
499
|
|
414
|
-
@blocks
|
415
|
-
|
416
|
-
results[index - 1] = block.call(results[index - 1])
|
417
|
-
end
|
500
|
+
if @blocks && block = @blocks[index + 1]
|
501
|
+
results[index] = block.call(result)
|
418
502
|
end
|
419
503
|
end
|
420
504
|
|
@@ -428,8 +512,17 @@ class RedisClient
|
|
428
512
|
@timeouts = nil
|
429
513
|
end
|
430
514
|
|
431
|
-
def blocking_call(timeout, *command, **kwargs)
|
432
|
-
command = @command_builder.generate
|
515
|
+
def blocking_call(timeout, *command, **kwargs, &block)
|
516
|
+
command = @command_builder.generate(command, kwargs)
|
517
|
+
@timeouts ||= []
|
518
|
+
@timeouts[@commands.size] = timeout
|
519
|
+
(@blocks ||= [])[@commands.size] = block if block_given?
|
520
|
+
@commands << command
|
521
|
+
nil
|
522
|
+
end
|
523
|
+
|
524
|
+
def blocking_call_v(timeout, command, &block)
|
525
|
+
command = @command_builder.generate(command)
|
433
526
|
@timeouts ||= []
|
434
527
|
@timeouts[@commands.size] = timeout
|
435
528
|
(@blocks ||= [])[@commands.size] = block if block_given?
|
@@ -524,6 +617,10 @@ class RedisClient
|
|
524
617
|
begin
|
525
618
|
@disable_reconnection = true
|
526
619
|
yield connection
|
620
|
+
rescue ConnectionError
|
621
|
+
connection&.close
|
622
|
+
close
|
623
|
+
raise
|
527
624
|
ensure
|
528
625
|
@disable_reconnection = previous_disable_reconnection
|
529
626
|
end
|
@@ -554,10 +651,23 @@ class RedisClient
|
|
554
651
|
role, = connection.call_pipelined(prelude, nil).last
|
555
652
|
config.check_role!(role)
|
556
653
|
else
|
557
|
-
|
654
|
+
unless prelude.empty?
|
655
|
+
connection.call_pipelined(prelude, nil)
|
656
|
+
end
|
558
657
|
end
|
559
658
|
|
560
659
|
connection
|
660
|
+
rescue FailoverError
|
661
|
+
raise
|
662
|
+
rescue ConnectionError => error
|
663
|
+
raise CannotConnectError, error.message, error.backtrace
|
664
|
+
rescue CommandError => error
|
665
|
+
if error.message.include?("ERR unknown command `HELLO`")
|
666
|
+
raise UnsupportedServer,
|
667
|
+
"Your Redis server version is too old. redis-client requires Redis 6+. (#{config.server_url})"
|
668
|
+
else
|
669
|
+
raise
|
670
|
+
end
|
561
671
|
end
|
562
672
|
end
|
563
673
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jean Boussier
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-08-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: connection_pool
|