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 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