redis-client 0.18.0 → 0.24.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.
@@ -10,12 +10,12 @@ class RedisClient
10
10
 
11
11
  EOL = "\r\n".b.freeze
12
12
  EOL_SIZE = EOL.bytesize
13
- DUMP_TYPES = { # rubocop:disable Style/MutableConstant
13
+ DUMP_TYPES = {
14
14
  String => :dump_string,
15
15
  Symbol => :dump_symbol,
16
16
  Integer => :dump_numeric,
17
17
  Float => :dump_numeric,
18
- }
18
+ }.freeze
19
19
  PARSER_TYPES = {
20
20
  '#' => :parse_boolean,
21
21
  '$' => :parse_blob,
@@ -57,7 +57,7 @@ class RedisClient
57
57
  def dump_any(object, buffer)
58
58
  method = DUMP_TYPES.fetch(object.class) do |unexpected_class|
59
59
  if superclass = DUMP_TYPES.keys.find { |t| t > unexpected_class }
60
- DUMP_TYPES[unexpected_class] = DUMP_TYPES[superclass]
60
+ DUMP_TYPES[superclass]
61
61
  else
62
62
  raise TypeError, "Unsupported command argument type: #{unexpected_class}"
63
63
  end
@@ -111,15 +111,39 @@ class RedisClient
111
111
 
112
112
  def parse(io)
113
113
  type = io.getbyte
114
- method = PARSER_TYPES.fetch(type) do
114
+ if type == 35 # '#'.ord
115
+ parse_boolean(io)
116
+ elsif type == 36 # '$'.ord
117
+ parse_blob(io)
118
+ elsif type == 43 # '+'.ord
119
+ parse_string(io)
120
+ elsif type == 61 # '='.ord
121
+ parse_verbatim_string(io)
122
+ elsif type == 45 # '-'.ord
123
+ parse_error(io)
124
+ elsif type == 58 # ':'.ord
125
+ parse_integer(io)
126
+ elsif type == 40 # '('.ord
127
+ parse_integer(io)
128
+ elsif type == 44 # ','.ord
129
+ parse_double(io)
130
+ elsif type == 95 # '_'.ord
131
+ parse_null(io)
132
+ elsif type == 42 # '*'.ord
133
+ parse_array(io)
134
+ elsif type == 37 # '%'.ord
135
+ parse_map(io)
136
+ elsif type == 126 # '~'.ord
137
+ parse_set(io)
138
+ elsif type == 62 # '>'.ord
139
+ parse_array(io)
140
+ else
115
141
  raise UnknownType, "Unknown sigil type: #{type.chr.inspect}"
116
142
  end
117
- send(method, io)
118
143
  end
119
144
 
120
145
  def parse_string(io)
121
146
  str = io.gets_chomp
122
- str.force_encoding(Encoding.default_external)
123
147
  str.force_encoding(Encoding::BINARY) unless str.valid_encoding?
124
148
  str.freeze
125
149
  end
@@ -140,17 +164,17 @@ class RedisClient
140
164
  end
141
165
 
142
166
  def parse_array(io)
143
- parse_sequence(io, parse_integer(io))
167
+ parse_sequence(io, io.gets_integer)
144
168
  end
145
169
 
146
170
  def parse_set(io)
147
- parse_sequence(io, parse_integer(io))
171
+ parse_sequence(io, io.gets_integer)
148
172
  end
149
173
 
150
174
  def parse_map(io)
151
175
  hash = {}
152
- parse_integer(io).times do
153
- hash[parse(io)] = parse(io)
176
+ io.gets_integer.times do
177
+ hash[parse(io).freeze] = parse(io)
154
178
  end
155
179
  hash
156
180
  end
@@ -192,11 +216,10 @@ class RedisClient
192
216
  end
193
217
 
194
218
  def parse_blob(io)
195
- bytesize = parse_integer(io)
219
+ bytesize = io.gets_integer
196
220
  return if bytesize < 0 # RESP2 nil type
197
221
 
198
222
  str = io.read_chomp(bytesize)
199
- str.force_encoding(Encoding.default_external)
200
223
  str.force_encoding(Encoding::BINARY) unless str.valid_encoding?
201
224
  str
202
225
  end
@@ -40,6 +40,8 @@ class RedisClient
40
40
 
41
41
  SUPPORTS_RESOLV_TIMEOUT = Socket.method(:tcp).parameters.any? { |p| p.last == :resolv_timeout }
42
42
 
43
+ attr_reader :config
44
+
43
45
  def initialize(config, connect_timeout:, read_timeout:, write_timeout:)
44
46
  super()
45
47
  @config = config
@@ -72,8 +74,8 @@ class RedisClient
72
74
  buffer = RESP3.dump(command)
73
75
  begin
74
76
  @io.write(buffer)
75
- rescue SystemCallError, IOError => error
76
- raise ConnectionError, error.message
77
+ rescue SystemCallError, IOError, OpenSSL::SSL::SSLError => error
78
+ raise ConnectionError.with_config(error.message, config)
77
79
  end
78
80
  end
79
81
 
@@ -84,8 +86,8 @@ class RedisClient
84
86
  end
85
87
  begin
86
88
  @io.write(buffer)
87
- rescue SystemCallError, IOError => error
88
- raise ConnectionError, error.message
89
+ rescue SystemCallError, IOError, OpenSSL::SSL::SSLError => error
90
+ raise ConnectionError.with_config(error.message, config)
89
91
  end
90
92
  end
91
93
 
@@ -96,15 +98,15 @@ class RedisClient
96
98
  @io.with_timeout(timeout) { RESP3.load(@io) }
97
99
  end
98
100
  rescue RedisClient::RESP3::UnknownType => error
99
- raise RedisClient::ProtocolError, error.message
101
+ raise RedisClient::ProtocolError.with_config(error.message, config)
100
102
  rescue SystemCallError, IOError, OpenSSL::SSL::SSLError => error
101
- raise ConnectionError, error.message
103
+ raise ConnectionError.with_config(error.message, config)
102
104
  end
103
105
 
104
106
  def measure_round_trip_delay
105
- start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
107
+ start = RedisClient.now_ms
106
108
  call(["PING"], @read_timeout)
107
- Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start
109
+ RedisClient.now_ms - start
108
110
  end
109
111
 
110
112
  private
@@ -114,7 +116,12 @@ class RedisClient
114
116
  UNIXSocket.new(@config.path)
115
117
  else
116
118
  sock = if SUPPORTS_RESOLV_TIMEOUT
117
- Socket.tcp(@config.host, @config.port, connect_timeout: @connect_timeout, resolv_timeout: @connect_timeout)
119
+ begin
120
+ Socket.tcp(@config.host, @config.port, connect_timeout: @connect_timeout, resolv_timeout: @connect_timeout)
121
+ rescue Errno::ETIMEDOUT => timeout_error
122
+ timeout_error.message << ": #{@connect_timeout}s"
123
+ raise
124
+ end
118
125
  else
119
126
  Socket.tcp(@config.host, @config.port, connect_timeout: @connect_timeout)
120
127
  end
@@ -130,9 +137,9 @@ class RedisClient
130
137
  loop do
131
138
  case status = socket.connect_nonblock(exception: false)
132
139
  when :wait_readable
133
- socket.to_io.wait_readable(@connect_timeout) or raise CannotConnectError
140
+ socket.to_io.wait_readable(@connect_timeout) or raise CannotConnectError.with_config("", config)
134
141
  when :wait_writable
135
- socket.to_io.wait_writable(@connect_timeout) or raise CannotConnectError
142
+ socket.to_io.wait_writable(@connect_timeout) or raise CannotConnectError.with_config("", config)
136
143
  when socket
137
144
  break
138
145
  else
@@ -148,6 +155,7 @@ class RedisClient
148
155
  )
149
156
  true
150
157
  rescue SystemCallError, OpenSSL::SSL::SSLError, SocketError => error
158
+ socket&.close
151
159
  raise CannotConnectError, error.message, error.backtrace
152
160
  end
153
161
 
@@ -38,9 +38,14 @@ class RedisClient
38
38
  end
39
39
 
40
40
  @to_list_of_hash = @to_hash = nil
41
+ password = if sentinel_password && !sentinel_password.respond_to?(:call)
42
+ ->(_) { sentinel_password }
43
+ else
44
+ sentinel_password
45
+ end
41
46
  @extra_config = {
42
47
  username: sentinel_username,
43
- password: sentinel_password,
48
+ password: password,
44
49
  db: nil,
45
50
  }
46
51
  if client_config[:protocol] == 2
@@ -112,6 +117,12 @@ class RedisClient
112
117
  end
113
118
  end
114
119
 
120
+ def resolved?
121
+ @mutex.synchronize do
122
+ !!@config
123
+ end
124
+ end
125
+
115
126
  private
116
127
 
117
128
  def sentinels_to_configs(sentinels)
@@ -188,6 +199,10 @@ class RedisClient
188
199
  if success
189
200
  @sentinel_configs.unshift(@sentinel_configs.delete(sentinel_config))
190
201
  end
202
+ # Redis Sentinels may be configured to have a lower maxclients setting than
203
+ # the Redis nodes. Close the connection to the Sentinel node to avoid using
204
+ # a connection.
205
+ sentinel_client.close
191
206
  end
192
207
  end
193
208
 
@@ -4,26 +4,41 @@ require "uri"
4
4
 
5
5
  class RedisClient
6
6
  class URLConfig
7
- DEFAULT_SCHEMA = "redis"
8
- SSL_SCHEMA = "rediss"
9
-
10
7
  attr_reader :url, :uri
11
8
 
12
9
  def initialize(url)
13
10
  @url = url
14
11
  @uri = URI(url)
15
- unless uri.scheme == DEFAULT_SCHEMA || uri.scheme == SSL_SCHEMA
16
- raise ArgumentError, "Invalid URL: #{url.inspect}"
12
+ @unix = false
13
+ @ssl = false
14
+ case uri.scheme
15
+ when "redis"
16
+ # expected
17
+ when "rediss"
18
+ @ssl = true
19
+ when "unix", nil
20
+ @unix = true
21
+ else
22
+ raise ArgumentError, "Unknown URL scheme: #{url.inspect}"
17
23
  end
18
24
  end
19
25
 
20
26
  def ssl?
21
- @uri.scheme == SSL_SCHEMA
27
+ @ssl
22
28
  end
23
29
 
24
30
  def db
25
- db_path = uri.path&.delete_prefix("/")
26
- Integer(db_path) if db_path && !db_path.empty?
31
+ unless @unix
32
+ db_path = uri.path&.delete_prefix("/")
33
+ return Integer(db_path) if db_path && !db_path.empty?
34
+ end
35
+
36
+ unless uri.query.nil? || uri.query.empty?
37
+ _, db_query = URI.decode_www_form(uri.query).find do |key, _|
38
+ key == "db"
39
+ end
40
+ return Integer(db_query) if db_query && !db_query.empty?
41
+ end
27
42
  end
28
43
 
29
44
  def username
@@ -44,6 +59,12 @@ class RedisClient
44
59
  uri.host.sub(/\A\[(.*)\]\z/, '\1')
45
60
  end
46
61
 
62
+ def path
63
+ if @unix
64
+ File.join(*[uri.host, uri.path].compact)
65
+ end
66
+ end
67
+
47
68
  def port
48
69
  return unless uri.port
49
70
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class RedisClient
4
- VERSION = "0.18.0"
4
+ VERSION = "0.24.0"
5
5
  end
data/lib/redis_client.rb CHANGED
@@ -45,6 +45,14 @@ class RedisClient
45
45
  def default_driver=(name)
46
46
  @default_driver = driver(name)
47
47
  end
48
+
49
+ def now
50
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
51
+ end
52
+
53
+ def now_ms
54
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
55
+ end
48
56
  end
49
57
 
50
58
  register_driver :ruby do
@@ -77,7 +85,29 @@ class RedisClient
77
85
  end
78
86
  end
79
87
 
80
- Error = Class.new(StandardError)
88
+ module HasConfig
89
+ attr_reader :config
90
+
91
+ def _set_config(config)
92
+ @config = config
93
+ end
94
+
95
+ def message
96
+ return super unless config&.resolved?
97
+
98
+ "#{super} (#{config.server_url})"
99
+ end
100
+ end
101
+
102
+ class Error < StandardError
103
+ include HasConfig
104
+
105
+ def self.with_config(message, config = nil)
106
+ new(message).tap do |error|
107
+ error._set_config(config)
108
+ end
109
+ end
110
+ end
81
111
 
82
112
  ProtocolError = Class.new(Error)
83
113
  UnsupportedServer = Class.new(Error)
@@ -114,7 +144,7 @@ class RedisClient
114
144
  end
115
145
  code ||= error_message.split(' ', 2).first
116
146
  klass = ERRORS.fetch(code, self)
117
- klass.new(error_message)
147
+ klass.new(error_message.strip)
118
148
  end
119
149
  end
120
150
  end
@@ -222,17 +252,18 @@ class RedisClient
222
252
 
223
253
  def timeout=(timeout)
224
254
  super
225
- raw_connection.read_timeout = raw_connection.write_timeout = timeout if connected?
255
+ @raw_connection&.read_timeout = timeout
256
+ @raw_connection&.write_timeout = timeout
226
257
  end
227
258
 
228
259
  def read_timeout=(timeout)
229
260
  super
230
- raw_connection.read_timeout = timeout if connected?
261
+ @raw_connection&.read_timeout = timeout
231
262
  end
232
263
 
233
264
  def write_timeout=(timeout)
234
265
  super
235
- raw_connection.write_timeout = timeout if connected?
266
+ @raw_connection&.write_timeout = timeout
236
267
  end
237
268
 
238
269
  def pubsub
@@ -386,7 +417,7 @@ class RedisClient
386
417
  end
387
418
 
388
419
  def connected?
389
- @raw_connection&.connected?
420
+ @raw_connection&.revalidate
390
421
  end
391
422
 
392
423
  def close
@@ -398,7 +429,7 @@ class RedisClient
398
429
  ensure_connected(retryable: false, &block)
399
430
  end
400
431
 
401
- def pipelined
432
+ def pipelined(exception: true)
402
433
  pipeline = Pipeline.new(@command_builder)
403
434
  yield pipeline
404
435
 
@@ -408,7 +439,7 @@ class RedisClient
408
439
  results = ensure_connected(retryable: pipeline._retryable?) do |connection|
409
440
  commands = pipeline._commands
410
441
  @middlewares.call_pipelined(commands, config) do
411
- connection.call_pipelined(commands, pipeline._timeouts)
442
+ connection.call_pipelined(commands, pipeline._timeouts, exception: exception)
412
443
  end
413
444
  end
414
445
 
@@ -669,6 +700,7 @@ class RedisClient
669
700
  elsif retryable
670
701
  tries = 0
671
702
  connection = nil
703
+ preferred_error = nil
672
704
  begin
673
705
  connection = raw_connection
674
706
  if block_given?
@@ -677,13 +709,20 @@ class RedisClient
677
709
  connection
678
710
  end
679
711
  rescue ConnectionError, ProtocolError => error
712
+ preferred_error ||= error
680
713
  close
681
714
 
715
+ if error.is_a?(CircuitBreaker::OpenCircuitError)
716
+ raise preferred_error
717
+ else
718
+ preferred_error = error
719
+ end
720
+
682
721
  if !@disable_reconnection && config.retry_connecting?(tries, error)
683
722
  tries += 1
684
723
  retry
685
724
  else
686
- raise
725
+ raise preferred_error
687
726
  end
688
727
  end
689
728
  else
@@ -746,10 +785,13 @@ class RedisClient
746
785
  end
747
786
  end
748
787
  end
749
- rescue FailoverError, CannotConnectError
750
- raise
788
+ rescue FailoverError, CannotConnectError => error
789
+ error._set_config(config)
790
+ raise error
751
791
  rescue ConnectionError => error
752
- raise CannotConnectError, error.message, error.backtrace
792
+ connect_error = CannotConnectError.with_config(error.message, config)
793
+ connect_error.set_backtrace(error.backtrace)
794
+ raise connect_error
753
795
  rescue CommandError => error
754
796
  if error.message.match?(/ERR unknown command ['`]HELLO['`]/)
755
797
  raise UnsupportedServer,
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.0
4
+ version: 0.24.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jean Boussier
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2023-10-26 00:00:00.000000000 Z
10
+ date: 2025-03-05 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: connection_pool
@@ -24,7 +23,6 @@ dependencies:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
25
  version: '0'
27
- description:
28
26
  email:
29
27
  - jean.boussier@gmail.com
30
28
  executables: []
@@ -32,11 +30,8 @@ extensions: []
32
30
  extra_rdoc_files: []
33
31
  files:
34
32
  - CHANGELOG.md
35
- - Gemfile
36
- - Gemfile.lock
37
33
  - LICENSE.md
38
34
  - README.md
39
- - Rakefile
40
35
  - lib/redis-client.rb
41
36
  - lib/redis_client.rb
42
37
  - lib/redis_client/circuit_breaker.rb
@@ -53,7 +48,6 @@ files:
53
48
  - lib/redis_client/sentinel_config.rb
54
49
  - lib/redis_client/url_config.rb
55
50
  - lib/redis_client/version.rb
56
- - redis-client.gemspec
57
51
  homepage: https://github.com/redis-rb/redis-client
58
52
  licenses:
59
53
  - MIT
@@ -62,7 +56,6 @@ metadata:
62
56
  homepage_uri: https://github.com/redis-rb/redis-client
63
57
  source_code_uri: https://github.com/redis-rb/redis-client
64
58
  changelog_uri: https://github.com/redis-rb/redis-client/blob/master/CHANGELOG.md
65
- post_install_message:
66
59
  rdoc_options: []
67
60
  require_paths:
68
61
  - lib
@@ -70,15 +63,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
70
63
  requirements:
71
64
  - - ">="
72
65
  - !ruby/object:Gem::Version
73
- version: 2.5.0
66
+ version: 2.6.0
74
67
  required_rubygems_version: !ruby/object:Gem::Requirement
75
68
  requirements:
76
69
  - - ">="
77
70
  - !ruby/object:Gem::Version
78
71
  version: '0'
79
72
  requirements: []
80
- rubygems_version: 3.4.10
81
- signing_key:
73
+ rubygems_version: 3.6.2
82
74
  specification_version: 4
83
75
  summary: Simple low-level client for Redis 6+
84
76
  test_files: []
data/Gemfile DELETED
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source "https://rubygems.org"
4
-
5
- # Specify your gem's dependencies in redis-client.gemspec
6
- gemspec name: "redis-client"
7
-
8
- gem "minitest"
9
- gem "rake", "~> 13.0"
10
- gem "rake-compiler"
11
- gem "rubocop"
12
- gem "rubocop-minitest"
13
- gem "toxiproxy"
14
-
15
- group :benchmark do
16
- gem "benchmark-ips"
17
- gem "hiredis"
18
- gem "redis", "~> 4.6"
19
- gem "stackprof", platform: :mri
20
- end
21
-
22
- gem "byebug", platform: :mri
data/Gemfile.lock DELETED
@@ -1,66 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- redis-client (0.18.0)
5
- connection_pool
6
-
7
- GEM
8
- remote: https://rubygems.org/
9
- specs:
10
- ast (2.4.2)
11
- benchmark-ips (2.12.0)
12
- byebug (11.1.3)
13
- connection_pool (2.4.1)
14
- hiredis (0.6.3)
15
- hiredis (0.6.3-java)
16
- minitest (5.15.0)
17
- parallel (1.22.1)
18
- parser (3.1.2.1)
19
- ast (~> 2.4.1)
20
- rainbow (3.1.1)
21
- rake (13.0.6)
22
- rake-compiler (1.2.5)
23
- rake
24
- redis (4.6.0)
25
- regexp_parser (2.5.0)
26
- rexml (3.2.5)
27
- rubocop (1.28.2)
28
- parallel (~> 1.10)
29
- parser (>= 3.1.0.0)
30
- rainbow (>= 2.2.2, < 4.0)
31
- regexp_parser (>= 1.8, < 3.0)
32
- rexml
33
- rubocop-ast (>= 1.17.0, < 2.0)
34
- ruby-progressbar (~> 1.7)
35
- unicode-display_width (>= 1.4.0, < 3.0)
36
- rubocop-ast (1.17.0)
37
- parser (>= 3.1.1.0)
38
- rubocop-minitest (0.19.1)
39
- rubocop (>= 0.90, < 2.0)
40
- ruby-progressbar (1.11.0)
41
- stackprof (0.2.25)
42
- toxiproxy (2.0.2)
43
- unicode-display_width (2.2.0)
44
-
45
- PLATFORMS
46
- ruby
47
- universal-java-18
48
- x86_64-darwin-20
49
- x86_64-linux
50
-
51
- DEPENDENCIES
52
- benchmark-ips
53
- byebug
54
- hiredis
55
- minitest
56
- rake (~> 13.0)
57
- rake-compiler
58
- redis (~> 4.6)
59
- redis-client!
60
- rubocop
61
- rubocop-minitest
62
- stackprof
63
- toxiproxy
64
-
65
- BUNDLED WITH
66
- 2.3.13