redis-client 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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