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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6bd28ab66722c23791118818d83505de8cbc1b141c75959f2408b8a0c90cb6ad
4
- data.tar.gz: 30f85f2bb0df340facd9a743f63dcfd2ff8fa8b8a3e363c9fdeb2f1101d5d076
3
+ metadata.gz: 05e24d8136cd8dc878d579c850297b1a016133447fd1f6135bd29dba55cd764e
4
+ data.tar.gz: 9f67aa59e1dfe2370f5f786ef3dea8076abe172301d263a13624b65cb2ab3888
5
5
  SHA512:
6
- metadata.gz: 528b4cd6f9377f083a19ca1da7b117625ad32c1849c8cc53d795dc5af8e6d23c372fdb5714c043b7c796a6fcfda54ef28ee0142268846445cbfb60e00eabf625
7
- data.tar.gz: af0040e6a5cf498587818b5161122d57e3057a39a22d0eb5fc8252e4cb56e71ad61c0a381fbe35c75cd6f5e5e2998169487e33b74f7635f7e025f6e06d7351ca
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- redis-client (0.4.0)
4
+ redis-client (0.6.0)
5
5
  connection_pool
6
6
 
7
7
  GEM
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 redis commands to Ruby constructs,
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 tht all other configurtions take precedence, e.g. `RedisClient.config(url: "redis://localhost:3000" port: 6380)` will connect on port `6380`.
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 flatenned as well:
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 explictly cast the argument as a string.
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 explictly pass them as keyword arguments with `**`:
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 postional argument, but on Ruby 3 it will be interpreted as keyword
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` familly of methods:
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` returns `nil`.
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 uppon connection errors.
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!(args, kwargs)
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.each do |key, value|
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!(args, kwargs)
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.each do |key, value|
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
@@ -12,8 +12,8 @@ class RedisClient
12
12
  DEFAULT_DB = 0
13
13
 
14
14
  module Common
15
- attr_reader :db, :username, :password, :id, :ssl, :ssl_params, :command_builder,
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 || DEFAULT_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
- RedisClient.new(self, **kwargs)
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
- prelude << if @password
90
- ["HELLO", "3", "AUTH", @username, @password]
91
- else
92
- ["HELLO", "3"]
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
- uri = url && URI.parse(url)
114
- if uri
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.password
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&.user && uri&.password
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}
@@ -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: 128)
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)).to_set
143
+ parse_sequence(io, parse_integer(io))
148
144
  end
149
145
 
150
146
  def parse_map(io)
151
- Hash[*parse_sequence(io, parse_integer(io) * 2)]
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 ReadTimeoutError
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 WriteTimeoutError
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 ConnectionError, error.message
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
- raise ConnectionError, "Couldn't locate a master for role: #{@name}"
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class RedisClient
4
- VERSION = "0.4.0"
4
+ VERSION = "0.6.0"
5
5
  end
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
- ConnectTimeoutError = Class.new(TimeoutError)
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!(command, kwargs)
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!(command, kwargs)
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!(command, kwargs)
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 block_given?
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!(args, kwargs)
224
- scan_list(1, ["SCAN", 0, *args], &block)
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!(args, kwargs)
233
- scan_list(2, ["SSCAN", key, 0, *args], &block)
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!(args, kwargs)
242
- scan_pairs(2, ["HSCAN", key, 0, *args], &block)
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!(args, kwargs)
251
- scan_pairs(2, ["ZSCAN", key, 0, *args], &block)
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!(command, kwargs))
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!(command, kwargs)
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 call_once(*command, **kwargs)
375
- command = @command_builder.generate!(command, kwargs)
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
- if results
408
- results.each do |result|
409
- if result.is_a?(CommandError)
410
- raise result
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&.each_with_index do |block, index|
415
- if block
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!(command, kwargs)
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
- connection.call_pipelined(prelude, nil)
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.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-05-06 00:00:00.000000000 Z
11
+ date: 2022-08-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: connection_pool