redis-client 0.2.1 → 0.3.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.
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "io/wait" unless IO.method_defined?(:wait_readable) && IO.method_defined?(:wait_writable)
4
+
5
+ class RedisClient
6
+ class RubyConnection
7
+ class BufferedIO
8
+ EOL = "\r\n".b.freeze
9
+ EOL_SIZE = EOL.bytesize
10
+
11
+ attr_accessor :read_timeout, :write_timeout
12
+
13
+ def initialize(io, read_timeout:, write_timeout:, chunk_size: 4096)
14
+ @io = io
15
+ @buffer = "".b
16
+ @offset = 0
17
+ @chunk_size = chunk_size
18
+ @read_timeout = read_timeout
19
+ @write_timeout = write_timeout
20
+ @blocking_reads = false
21
+ end
22
+
23
+ def close
24
+ @io.to_io.close
25
+ end
26
+
27
+ def closed?
28
+ @io.to_io.closed?
29
+ end
30
+
31
+ def eof?
32
+ @offset >= @buffer.bytesize && @io.eof?
33
+ end
34
+
35
+ def with_timeout(new_timeout)
36
+ new_timeout = false if new_timeout == 0
37
+
38
+ previous_read_timeout = @read_timeout
39
+ previous_blocking_reads = @blocking_reads
40
+
41
+ if new_timeout
42
+ @read_timeout = new_timeout
43
+ else
44
+ @blocking_reads = true
45
+ end
46
+
47
+ begin
48
+ yield
49
+ ensure
50
+ @read_timeout = previous_read_timeout
51
+ @blocking_reads = previous_blocking_reads
52
+ end
53
+ end
54
+
55
+ def skip(offset)
56
+ ensure_remaining(offset)
57
+ @offset += offset
58
+ nil
59
+ end
60
+
61
+ def write(string)
62
+ total = remaining = string.bytesize
63
+ loop do
64
+ case bytes_written = @io.write_nonblock(string, exception: false)
65
+ when Integer
66
+ remaining -= bytes_written
67
+ if remaining > 0
68
+ string = string.byteslice(bytes_written..-1)
69
+ else
70
+ return total
71
+ end
72
+ when :wait_readable
73
+ @io.to_io.wait_readable(@read_timeout) or raise ReadTimeoutError
74
+ when :wait_writable
75
+ @io.to_io.wait_writable(@write_timeout) or raise WriteTimeoutError
76
+ when nil
77
+ raise Errno::ECONNRESET
78
+ else
79
+ raise "Unexpected `write_nonblock` return: #{bytes.inspect}"
80
+ end
81
+ end
82
+ end
83
+
84
+ def getbyte
85
+ ensure_remaining(1)
86
+ byte = @buffer.getbyte(@offset)
87
+ @offset += 1
88
+ byte
89
+ end
90
+
91
+ def gets_chomp
92
+ fill_buffer(false) if @offset >= @buffer.bytesize
93
+ until eol_index = @buffer.index(EOL, @offset)
94
+ fill_buffer(false)
95
+ end
96
+
97
+ line = @buffer.byteslice(@offset, eol_index - @offset)
98
+ @offset = eol_index + EOL_SIZE
99
+ line
100
+ end
101
+
102
+ def read_chomp(bytes)
103
+ ensure_remaining(bytes + EOL_SIZE)
104
+ str = @buffer.byteslice(@offset, bytes)
105
+ @offset += bytes + EOL_SIZE
106
+ str
107
+ end
108
+
109
+ private
110
+
111
+ def ensure_remaining(bytes)
112
+ needed = bytes - (@buffer.bytesize - @offset)
113
+ if needed > 0
114
+ fill_buffer(true, needed)
115
+ end
116
+ end
117
+
118
+ def fill_buffer(strict, size = @chunk_size)
119
+ remaining = size
120
+ empty_buffer = @offset >= @buffer.bytesize
121
+
122
+ loop do
123
+ bytes = if empty_buffer
124
+ @io.read_nonblock([remaining, @chunk_size].max, @buffer, exception: false)
125
+ else
126
+ @io.read_nonblock([remaining, @chunk_size].max, exception: false)
127
+ end
128
+ case bytes
129
+ when String
130
+ if empty_buffer
131
+ @offset = 0
132
+ empty_buffer = false
133
+ else
134
+ @buffer << bytes
135
+ end
136
+ remaining -= bytes.bytesize
137
+ return if !strict || remaining <= 0
138
+ when :wait_readable
139
+ unless @io.to_io.wait_readable(@read_timeout)
140
+ raise ReadTimeoutError unless @blocking_reads
141
+ end
142
+ when :wait_writable
143
+ @io.to_io.wait_writable(@write_timeout) or raise WriteTimeoutError
144
+ when nil
145
+ raise Errno::ECONNRESET
146
+ else
147
+ raise "Unexpected `read_nonblock` return: #{bytes.inspect}"
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -58,32 +58,6 @@ class RedisClient
58
58
  String.new(encoding: Encoding::BINARY, capacity: 128)
59
59
  end
60
60
 
61
- def coerce_command!(command)
62
- command = command.flat_map do |element|
63
- case element
64
- when Hash
65
- element.flatten
66
- when Set
67
- element.to_a
68
- else
69
- element
70
- end
71
- end
72
-
73
- command.map! do |element|
74
- case element
75
- when String
76
- element
77
- when Integer, Float, Symbol
78
- element.to_s
79
- else
80
- raise TypeError, "Unsupported command argument type: #{element.class}"
81
- end
82
- end
83
-
84
- command
85
- end
86
-
87
61
  def dump_any(object, buffer)
88
62
  method = DUMP_TYPES.fetch(object.class) do
89
63
  raise TypeError, "Unsupported command argument type: #{object.class}"
@@ -2,47 +2,42 @@
2
2
 
3
3
  require "socket"
4
4
  require "openssl"
5
- require "redis_client/buffered_io"
5
+ require "redis_client/connection_mixin"
6
+ require "redis_client/ruby_connection/buffered_io"
7
+ require "redis_client/ruby_connection/resp3"
6
8
 
7
9
  class RedisClient
8
- class Connection
9
- module Common
10
- def call(command, timeout)
11
- write(command)
12
- result = read(timeout)
13
- if result.is_a?(CommandError)
14
- raise result
15
- else
16
- result
17
- end
18
- end
10
+ class RubyConnection
11
+ include ConnectionMixin
19
12
 
20
- def call_pipelined(commands, timeouts)
21
- exception = nil
13
+ class << self
14
+ def ssl_context(ssl_params)
15
+ params = ssl_params.dup || {}
22
16
 
23
- size = commands.size
24
- results = Array.new(commands.size)
25
- write_multi(commands)
17
+ cert = params[:cert]
18
+ if cert.is_a?(String)
19
+ cert = File.read(cert) if File.exist?(cert)
20
+ params[:cert] = OpenSSL::X509::Certificate.new(cert)
21
+ end
26
22
 
27
- size.times do |index|
28
- timeout = timeouts && timeouts[index]
29
- result = read(timeout)
30
- if result.is_a?(CommandError)
31
- exception ||= result
32
- end
33
- results[index] = result
23
+ key = params[:key]
24
+ if key.is_a?(String)
25
+ key = File.read(key) if File.exist?(key)
26
+ params[:key] = OpenSSL::PKey.read(key)
34
27
  end
35
28
 
36
- if exception
37
- raise exception
38
- else
39
- results
29
+ context = OpenSSL::SSL::SSLContext.new
30
+ context.set_params(params)
31
+ if context.verify_mode != OpenSSL::SSL::VERIFY_NONE
32
+ if context.respond_to?(:verify_hostname) # Missing on JRuby
33
+ context.verify_hostname
34
+ end
40
35
  end
36
+
37
+ context
41
38
  end
42
39
  end
43
40
 
44
- include Common
45
-
46
41
  SUPPORTS_RESOLV_TIMEOUT = Socket.method(:tcp).parameters.any? { |p| p.last == :resolv_timeout }
47
42
 
48
43
  def initialize(config, connect_timeout:, read_timeout:, write_timeout:)
@@ -60,7 +55,7 @@ class RedisClient
60
55
  end
61
56
 
62
57
  if config.ssl
63
- socket = OpenSSL::SSL::SSLSocket.new(socket, config.openssl_context)
58
+ socket = OpenSSL::SSL::SSLSocket.new(socket, config.ssl_context)
64
59
  socket.hostname = config.host
65
60
  loop do
66
61
  case status = socket.connect_nonblock(exception: false)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class RedisClient
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/redis_client.rb CHANGED
@@ -1,12 +1,62 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
3
5
  require "redis_client/version"
6
+ require "redis_client/command_builder"
4
7
  require "redis_client/config"
5
8
  require "redis_client/sentinel_config"
6
- require "redis_client/connection"
7
9
  require "redis_client/middlewares"
8
10
 
9
11
  class RedisClient
12
+ @driver_definitions = {}
13
+ @drivers = {}
14
+
15
+ @default_driver = nil
16
+
17
+ class << self
18
+ def register_driver(name, &block)
19
+ @driver_definitions[name] = block
20
+ end
21
+
22
+ def driver(name)
23
+ return name if name.is_a?(Class)
24
+
25
+ name = name.to_sym
26
+ unless @driver_definitions.key?(name)
27
+ raise ArgumentError, "Unknown driver #{name.inspect}, expected one of: `#{DRIVER_DEFINITIONS.keys.inspect}`"
28
+ end
29
+
30
+ @drivers[name] ||= @driver_definitions[name]&.call
31
+ end
32
+
33
+ def default_driver
34
+ unless @default_driver
35
+ @driver_definitions.each_key do |name|
36
+ if @default_driver = driver(name)
37
+ break
38
+ end
39
+ rescue LoadError
40
+ end
41
+ end
42
+ @default_driver
43
+ end
44
+
45
+ def default_driver=(name)
46
+ @default_driver = driver(name)
47
+ end
48
+ end
49
+
50
+ register_driver :hiredis do
51
+ require "redis_client/hiredis_connection"
52
+ HiredisConnection
53
+ end
54
+
55
+ register_driver :ruby do
56
+ require "redis_client/ruby_connection"
57
+ RubyConnection
58
+ end
59
+
10
60
  module Common
11
61
  attr_reader :config, :id
12
62
  attr_accessor :connect_timeout, :read_timeout, :write_timeout
@@ -23,6 +73,7 @@ class RedisClient
23
73
  @connect_timeout = connect_timeout
24
74
  @read_timeout = read_timeout
25
75
  @write_timeout = write_timeout
76
+ @command_builder = config.command_builder
26
77
  end
27
78
 
28
79
  def timeout=(timeout)
@@ -54,10 +105,14 @@ class RedisClient
54
105
 
55
106
  AuthenticationError = Class.new(CommandError)
56
107
  PermissionError = Class.new(CommandError)
108
+ ReadOnlyError = Class.new(CommandError)
109
+ WrongTypeError = Class.new(CommandError)
57
110
 
58
111
  CommandError::ERRORS = {
59
112
  "WRONGPASS" => AuthenticationError,
60
113
  "NOPERM" => PermissionError,
114
+ "READONLY" => ReadOnlyError,
115
+ "WRONGTYPE" => WrongTypeError,
61
116
  }.freeze
62
117
 
63
118
  class << self
@@ -115,67 +170,89 @@ class RedisClient
115
170
  end
116
171
 
117
172
  def pubsub
118
- sub = PubSub.new(ensure_connected)
173
+ sub = PubSub.new(ensure_connected, @command_builder)
119
174
  @raw_connection = nil
120
175
  sub
121
176
  end
122
177
 
123
- def call(*command)
124
- command = RESP3.coerce_command!(command)
125
- ensure_connected do |connection|
178
+ def call(*command, **kwargs)
179
+ command = @command_builder.generate!(command, kwargs)
180
+ result = ensure_connected do |connection|
126
181
  Middlewares.call(command, config) do
127
182
  connection.call(command, nil)
128
183
  end
129
184
  end
185
+
186
+ if block_given?
187
+ yield result
188
+ else
189
+ result
190
+ end
130
191
  end
131
192
 
132
- def call_once(*command)
133
- command = RESP3.coerce_command!(command)
134
- ensure_connected(retryable: false) do |connection|
193
+ def call_once(*command, **kwargs)
194
+ command = @command_builder.generate!(command, kwargs)
195
+ result = ensure_connected(retryable: false) do |connection|
135
196
  Middlewares.call(command, config) do
136
197
  connection.call(command, nil)
137
198
  end
138
199
  end
200
+
201
+ if block_given?
202
+ yield result
203
+ else
204
+ result
205
+ end
139
206
  end
140
207
 
141
- def blocking_call(timeout, *command)
142
- command = RESP3.coerce_command!(command)
143
- ensure_connected do |connection|
208
+ def blocking_call(timeout, *command, **kwargs)
209
+ command = @command_builder.generate!(command, kwargs)
210
+ result = ensure_connected do |connection|
144
211
  Middlewares.call(command, config) do
145
212
  connection.call(command, timeout)
146
213
  end
147
214
  end
215
+
216
+ if block_given?
217
+ yield result
218
+ else
219
+ result
220
+ end
148
221
  end
149
222
 
150
- def scan(*args, &block)
223
+ def scan(*args, **kwargs, &block)
151
224
  unless block_given?
152
- return to_enum(__callee__, *args)
225
+ return to_enum(__callee__, *args, **kwargs)
153
226
  end
154
227
 
228
+ args = @command_builder.generate!(args, kwargs)
155
229
  scan_list(1, ["SCAN", 0, *args], &block)
156
230
  end
157
231
 
158
- def sscan(key, *args, &block)
232
+ def sscan(key, *args, **kwargs, &block)
159
233
  unless block_given?
160
- return to_enum(__callee__, key, *args)
234
+ return to_enum(__callee__, key, *args, **kwargs)
161
235
  end
162
236
 
237
+ args = @command_builder.generate!(args, kwargs)
163
238
  scan_list(2, ["SSCAN", key, 0, *args], &block)
164
239
  end
165
240
 
166
- def hscan(key, *args, &block)
241
+ def hscan(key, *args, **kwargs, &block)
167
242
  unless block_given?
168
- return to_enum(__callee__, key, *args)
243
+ return to_enum(__callee__, key, *args, **kwargs)
169
244
  end
170
245
 
246
+ args = @command_builder.generate!(args, kwargs)
171
247
  scan_pairs(2, ["HSCAN", key, 0, *args], &block)
172
248
  end
173
249
 
174
- def zscan(key, *args, &block)
250
+ def zscan(key, *args, **kwargs, &block)
175
251
  unless block_given?
176
- return to_enum(__callee__, key, *args)
252
+ return to_enum(__callee__, key, *args, **kwargs)
177
253
  end
178
254
 
255
+ args = @command_builder.generate!(args, kwargs)
179
256
  scan_pairs(2, ["ZSCAN", key, 0, *args], &block)
180
257
  end
181
258
 
@@ -190,23 +267,27 @@ class RedisClient
190
267
  end
191
268
 
192
269
  def pipelined
193
- pipeline = Pipeline.new
270
+ pipeline = Pipeline.new(@command_builder)
194
271
  yield pipeline
195
272
 
196
273
  if pipeline._size == 0
197
274
  []
198
275
  else
199
- ensure_connected(retryable: pipeline._retryable?) do |connection|
276
+ results = ensure_connected(retryable: pipeline._retryable?) do |connection|
200
277
  commands = pipeline._commands
201
278
  Middlewares.call_pipelined(commands, config) do
202
279
  connection.call_pipelined(commands, pipeline._timeouts)
203
280
  end
204
281
  end
282
+
283
+ pipeline._coerce!(results)
205
284
  end
206
285
  end
207
286
 
208
287
  def multi(watch: nil, &block)
209
- if watch
288
+ transaction = nil
289
+
290
+ results = if watch
210
291
  # WATCH is stateful, so we can't reconnect if it's used, the whole transaction
211
292
  # has to be redone.
212
293
  ensure_connected(retryable: false) do |connection|
@@ -214,7 +295,7 @@ class RedisClient
214
295
  begin
215
296
  if transaction = build_transaction(&block)
216
297
  commands = transaction._commands
217
- Middlewares.call_pipelined(commands, config) do
298
+ results = Middlewares.call_pipelined(commands, config) do
218
299
  connection.call_pipelined(commands, nil)
219
300
  end.last
220
301
  else
@@ -239,15 +320,22 @@ class RedisClient
239
320
  end
240
321
  end
241
322
  end
323
+
324
+ if transaction
325
+ transaction._coerce!(results)
326
+ else
327
+ results
328
+ end
242
329
  end
243
330
 
244
331
  class PubSub
245
- def initialize(raw_connection)
332
+ def initialize(raw_connection, command_builder)
246
333
  @raw_connection = raw_connection
334
+ @command_builder = command_builder
247
335
  end
248
336
 
249
- def call(*command)
250
- raw_connection.write(RESP3.coerce_command!(command))
337
+ def call(*command, **kwargs)
338
+ raw_connection.write(@command_builder.generate!(command, kwargs))
251
339
  nil
252
340
  end
253
341
 
@@ -273,20 +361,26 @@ class RedisClient
273
361
  end
274
362
 
275
363
  class Multi
276
- def initialize
364
+ def initialize(command_builder)
365
+ @command_builder = command_builder
277
366
  @size = 0
278
367
  @commands = []
368
+ @blocks = nil
279
369
  @retryable = true
280
370
  end
281
371
 
282
- def call(*command)
283
- @commands << RESP3.coerce_command!(command)
372
+ def call(*command, **kwargs, &block)
373
+ command = @command_builder.generate!(command, kwargs)
374
+ (@blocks ||= [])[@commands.size] = block if block_given?
375
+ @commands << command
284
376
  nil
285
377
  end
286
378
 
287
- def call_once(*command)
379
+ def call_once(*command, **kwargs)
380
+ command = @command_builder.generate!(command, kwargs)
288
381
  @retryable = false
289
- @commands << RESP3.coerce_command!(command)
382
+ (@blocks ||= [])[@commands.size] = block if block_given?
383
+ @commands << command
290
384
  nil
291
385
  end
292
386
 
@@ -294,6 +388,10 @@ class RedisClient
294
388
  @commands
295
389
  end
296
390
 
391
+ def _blocks
392
+ @blocks
393
+ end
394
+
297
395
  def _size
298
396
  @commands.size
299
397
  end
@@ -309,18 +407,38 @@ class RedisClient
309
407
  def _retryable?
310
408
  @retryable
311
409
  end
410
+
411
+ def _coerce!(results)
412
+ if results
413
+ results.each do |result|
414
+ if result.is_a?(CommandError)
415
+ raise result
416
+ end
417
+ end
418
+
419
+ @blocks&.each_with_index do |block, index|
420
+ if block
421
+ results[index - 1] = block.call(results[index - 1])
422
+ end
423
+ end
424
+ end
425
+
426
+ results
427
+ end
312
428
  end
313
429
 
314
430
  class Pipeline < Multi
315
- def initialize
431
+ def initialize(_command_builder)
316
432
  super
317
433
  @timeouts = nil
318
434
  end
319
435
 
320
- def blocking_call(timeout, *command)
436
+ def blocking_call(timeout, *command, **kwargs)
437
+ command = @command_builder.generate!(command, kwargs)
321
438
  @timeouts ||= []
322
439
  @timeouts[@commands.size] = timeout
323
- @commands << RESP3.coerce_command!(command)
440
+ (@blocks ||= [])[@commands.size] = block if block_given?
441
+ @commands << command
324
442
  nil
325
443
  end
326
444
 
@@ -331,12 +449,24 @@ class RedisClient
331
449
  def _empty?
332
450
  @commands.empty?
333
451
  end
452
+
453
+ def _coerce!(results)
454
+ return results unless results
455
+
456
+ @blocks&.each_with_index do |block, index|
457
+ if block
458
+ results[index] = block.call(results[index])
459
+ end
460
+ end
461
+
462
+ results
463
+ end
334
464
  end
335
465
 
336
466
  private
337
467
 
338
468
  def build_transaction
339
- transaction = Multi.new
469
+ transaction = Multi.new(@command_builder)
340
470
  transaction.call("MULTI")
341
471
  yield transaction
342
472
  transaction.call("EXEC")
@@ -436,5 +566,6 @@ class RedisClient
436
566
  end
437
567
  end
438
568
 
439
- require "redis_client/resp3"
440
569
  require "redis_client/pooled"
570
+
571
+ RedisClient.default_driver
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.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jean Boussier
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-04-29 00:00:00.000000000 Z
11
+ date: 2022-05-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: connection_pool
@@ -89,13 +89,16 @@ files:
89
89
  - ext/redis_client/hiredis/vendor/win32.h
90
90
  - lib/redis-client.rb
91
91
  - lib/redis_client.rb
92
- - lib/redis_client/buffered_io.rb
92
+ - lib/redis_client/command_builder.rb
93
93
  - lib/redis_client/config.rb
94
- - lib/redis_client/connection.rb
94
+ - lib/redis_client/connection_mixin.rb
95
+ - lib/redis_client/decorator.rb
95
96
  - lib/redis_client/hiredis_connection.rb
96
97
  - lib/redis_client/middlewares.rb
97
98
  - lib/redis_client/pooled.rb
98
- - lib/redis_client/resp3.rb
99
+ - lib/redis_client/ruby_connection.rb
100
+ - lib/redis_client/ruby_connection/buffered_io.rb
101
+ - lib/redis_client/ruby_connection/resp3.rb
99
102
  - lib/redis_client/sentinel_config.rb
100
103
  - lib/redis_client/version.rb
101
104
  - redis-client.gemspec