redis-client 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|