redis-client 0.1.0 → 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.
data/lib/redis_client.rb CHANGED
@@ -1,11 +1,86 @@
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"
9
+ require "redis_client/middlewares"
7
10
 
8
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
+
60
+ module Common
61
+ attr_reader :config, :id
62
+ attr_accessor :connect_timeout, :read_timeout, :write_timeout
63
+
64
+ def initialize(
65
+ config,
66
+ id: config.id,
67
+ connect_timeout: config.connect_timeout,
68
+ read_timeout: config.read_timeout,
69
+ write_timeout: config.write_timeout
70
+ )
71
+ @config = config
72
+ @id = id
73
+ @connect_timeout = connect_timeout
74
+ @read_timeout = read_timeout
75
+ @write_timeout = write_timeout
76
+ @command_builder = config.command_builder
77
+ end
78
+
79
+ def timeout=(timeout)
80
+ @connect_timeout = @read_timeout = @write_timeout = timeout
81
+ end
82
+ end
83
+
9
84
  Error = Class.new(StandardError)
10
85
 
11
86
  ConnectionError = Class.new(Error)
@@ -30,10 +105,14 @@ class RedisClient
30
105
 
31
106
  AuthenticationError = Class.new(CommandError)
32
107
  PermissionError = Class.new(CommandError)
108
+ ReadOnlyError = Class.new(CommandError)
109
+ WrongTypeError = Class.new(CommandError)
33
110
 
34
111
  CommandError::ERRORS = {
35
112
  "WRONGPASS" => AuthenticationError,
36
113
  "NOPERM" => PermissionError,
114
+ "READONLY" => ReadOnlyError,
115
+ "WRONGTYPE" => WrongTypeError,
37
116
  }.freeze
38
117
 
39
118
  class << self
@@ -52,23 +131,16 @@ class RedisClient
52
131
  super(config(**(arg || {}), **kwargs))
53
132
  end
54
133
  end
134
+
135
+ def register(middleware)
136
+ Middlewares.extend(middleware)
137
+ end
55
138
  end
56
139
 
57
- attr_reader :config, :id
58
- attr_accessor :connect_timeout, :read_timeout, :write_timeout
140
+ include Common
59
141
 
60
- def initialize(
61
- config,
62
- id: config.id,
63
- connect_timeout: config.connect_timeout,
64
- read_timeout: config.read_timeout,
65
- write_timeout: config.write_timeout
66
- )
67
- @config = config
68
- @id = id
69
- @connect_timeout = connect_timeout
70
- @read_timeout = read_timeout
71
- @write_timeout = write_timeout
142
+ def initialize(config, **)
143
+ super
72
144
  @raw_connection = nil
73
145
  @disable_reconnection = false
74
146
  end
@@ -83,86 +155,104 @@ class RedisClient
83
155
  alias_method :then, :with
84
156
 
85
157
  def timeout=(timeout)
86
- @connect_timeout = @read_timeout = @write_timeout = timeout
158
+ super
159
+ raw_connection.read_timeout = raw_connection.write_timeout = timeout if connected?
160
+ end
161
+
162
+ def read_timeout=(timeout)
163
+ super
164
+ raw_connection.read_timeout = timeout if connected?
165
+ end
166
+
167
+ def write_timeout=(timeout)
168
+ super
169
+ raw_connection.write_timeout = timeout if connected?
87
170
  end
88
171
 
89
172
  def pubsub
90
- sub = PubSub.new(ensure_connected)
173
+ sub = PubSub.new(ensure_connected, @command_builder)
91
174
  @raw_connection = nil
92
175
  sub
93
176
  end
94
177
 
95
- def call(*command)
96
- command = RESP3.coerce_command!(command)
178
+ def call(*command, **kwargs)
179
+ command = @command_builder.generate!(command, kwargs)
97
180
  result = ensure_connected do |connection|
98
- connection.write(command)
99
- connection.read
181
+ Middlewares.call(command, config) do
182
+ connection.call(command, nil)
183
+ end
100
184
  end
101
185
 
102
- if result.is_a?(CommandError)
103
- raise result
186
+ if block_given?
187
+ yield result
104
188
  else
105
189
  result
106
190
  end
107
191
  end
108
192
 
109
- def call_once(*command)
110
- command = RESP3.coerce_command!(command)
193
+ def call_once(*command, **kwargs)
194
+ command = @command_builder.generate!(command, kwargs)
111
195
  result = ensure_connected(retryable: false) do |connection|
112
- connection.write(command)
113
- connection.read
196
+ Middlewares.call(command, config) do
197
+ connection.call(command, nil)
198
+ end
114
199
  end
115
200
 
116
- if result.is_a?(CommandError)
117
- raise result
201
+ if block_given?
202
+ yield result
118
203
  else
119
204
  result
120
205
  end
121
206
  end
122
207
 
123
- def blocking_call(timeout, *command)
124
- command = RESP3.coerce_command!(command)
208
+ def blocking_call(timeout, *command, **kwargs)
209
+ command = @command_builder.generate!(command, kwargs)
125
210
  result = ensure_connected do |connection|
126
- connection.write(command)
127
- connection.read(timeout)
211
+ Middlewares.call(command, config) do
212
+ connection.call(command, timeout)
213
+ end
128
214
  end
129
215
 
130
- if result.is_a?(CommandError)
131
- raise result
216
+ if block_given?
217
+ yield result
132
218
  else
133
219
  result
134
220
  end
135
221
  end
136
222
 
137
- def scan(*args, &block)
223
+ def scan(*args, **kwargs, &block)
138
224
  unless block_given?
139
- return to_enum(__callee__, *args)
225
+ return to_enum(__callee__, *args, **kwargs)
140
226
  end
141
227
 
228
+ args = @command_builder.generate!(args, kwargs)
142
229
  scan_list(1, ["SCAN", 0, *args], &block)
143
230
  end
144
231
 
145
- def sscan(key, *args, &block)
232
+ def sscan(key, *args, **kwargs, &block)
146
233
  unless block_given?
147
- return to_enum(__callee__, key, *args)
234
+ return to_enum(__callee__, key, *args, **kwargs)
148
235
  end
149
236
 
237
+ args = @command_builder.generate!(args, kwargs)
150
238
  scan_list(2, ["SSCAN", key, 0, *args], &block)
151
239
  end
152
240
 
153
- def hscan(key, *args, &block)
241
+ def hscan(key, *args, **kwargs, &block)
154
242
  unless block_given?
155
- return to_enum(__callee__, key, *args)
243
+ return to_enum(__callee__, key, *args, **kwargs)
156
244
  end
157
245
 
246
+ args = @command_builder.generate!(args, kwargs)
158
247
  scan_pairs(2, ["HSCAN", key, 0, *args], &block)
159
248
  end
160
249
 
161
- def zscan(key, *args, &block)
250
+ def zscan(key, *args, **kwargs, &block)
162
251
  unless block_given?
163
- return to_enum(__callee__, key, *args)
252
+ return to_enum(__callee__, key, *args, **kwargs)
164
253
  end
165
254
 
255
+ args = @command_builder.generate!(args, kwargs)
166
256
  scan_pairs(2, ["ZSCAN", key, 0, *args], &block)
167
257
  end
168
258
 
@@ -177,27 +267,37 @@ class RedisClient
177
267
  end
178
268
 
179
269
  def pipelined
180
- pipeline = Pipeline.new
270
+ pipeline = Pipeline.new(@command_builder)
181
271
  yield pipeline
182
272
 
183
273
  if pipeline._size == 0
184
274
  []
185
275
  else
186
- ensure_connected(retryable: pipeline._retryable?) do |connection|
187
- call_pipelined(connection, pipeline._commands, pipeline._timeouts)
276
+ results = ensure_connected(retryable: pipeline._retryable?) do |connection|
277
+ commands = pipeline._commands
278
+ Middlewares.call_pipelined(commands, config) do
279
+ connection.call_pipelined(commands, pipeline._timeouts)
280
+ end
188
281
  end
282
+
283
+ pipeline._coerce!(results)
189
284
  end
190
285
  end
191
286
 
192
287
  def multi(watch: nil, &block)
193
- if watch
288
+ transaction = nil
289
+
290
+ results = if watch
194
291
  # WATCH is stateful, so we can't reconnect if it's used, the whole transaction
195
292
  # has to be redone.
196
293
  ensure_connected(retryable: false) do |connection|
197
294
  call("WATCH", *watch)
198
295
  begin
199
296
  if transaction = build_transaction(&block)
200
- call_pipelined(connection, transaction._commands).last
297
+ commands = transaction._commands
298
+ results = Middlewares.call_pipelined(commands, config) do
299
+ connection.call_pipelined(commands, nil)
300
+ end.last
201
301
  else
202
302
  call("UNWATCH")
203
303
  []
@@ -213,19 +313,29 @@ class RedisClient
213
313
  []
214
314
  else
215
315
  ensure_connected(retryable: transaction._retryable?) do |connection|
216
- call_pipelined(connection, transaction._commands).last
316
+ commands = transaction._commands
317
+ Middlewares.call_pipelined(commands, config) do
318
+ connection.call_pipelined(commands, nil)
319
+ end.last
217
320
  end
218
321
  end
219
322
  end
323
+
324
+ if transaction
325
+ transaction._coerce!(results)
326
+ else
327
+ results
328
+ end
220
329
  end
221
330
 
222
331
  class PubSub
223
- def initialize(raw_connection)
332
+ def initialize(raw_connection, command_builder)
224
333
  @raw_connection = raw_connection
334
+ @command_builder = command_builder
225
335
  end
226
336
 
227
- def call(*command)
228
- raw_connection.write(RESP3.coerce_command!(command))
337
+ def call(*command, **kwargs)
338
+ raw_connection.write(@command_builder.generate!(command, kwargs))
229
339
  nil
230
340
  end
231
341
 
@@ -251,20 +361,26 @@ class RedisClient
251
361
  end
252
362
 
253
363
  class Multi
254
- def initialize
364
+ def initialize(command_builder)
365
+ @command_builder = command_builder
255
366
  @size = 0
256
367
  @commands = []
368
+ @blocks = nil
257
369
  @retryable = true
258
370
  end
259
371
 
260
- def call(*command)
261
- @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
262
376
  nil
263
377
  end
264
378
 
265
- def call_once(*command)
379
+ def call_once(*command, **kwargs)
380
+ command = @command_builder.generate!(command, kwargs)
266
381
  @retryable = false
267
- @commands << RESP3.coerce_command!(command)
382
+ (@blocks ||= [])[@commands.size] = block if block_given?
383
+ @commands << command
268
384
  nil
269
385
  end
270
386
 
@@ -272,6 +388,10 @@ class RedisClient
272
388
  @commands
273
389
  end
274
390
 
391
+ def _blocks
392
+ @blocks
393
+ end
394
+
275
395
  def _size
276
396
  @commands.size
277
397
  end
@@ -287,18 +407,38 @@ class RedisClient
287
407
  def _retryable?
288
408
  @retryable
289
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
290
428
  end
291
429
 
292
430
  class Pipeline < Multi
293
- def initialize
431
+ def initialize(_command_builder)
294
432
  super
295
433
  @timeouts = nil
296
434
  end
297
435
 
298
- def blocking_call(timeout, *command)
436
+ def blocking_call(timeout, *command, **kwargs)
437
+ command = @command_builder.generate!(command, kwargs)
299
438
  @timeouts ||= []
300
439
  @timeouts[@commands.size] = timeout
301
- @commands << RESP3.coerce_command!(command)
440
+ (@blocks ||= [])[@commands.size] = block if block_given?
441
+ @commands << command
302
442
  nil
303
443
  end
304
444
 
@@ -309,12 +449,24 @@ class RedisClient
309
449
  def _empty?
310
450
  @commands.empty?
311
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
312
464
  end
313
465
 
314
466
  private
315
467
 
316
468
  def build_transaction
317
- transaction = Multi.new
469
+ transaction = Multi.new(@command_builder)
318
470
  transaction.call("MULTI")
319
471
  yield transaction
320
472
  transaction.call("EXEC")
@@ -347,29 +499,6 @@ class RedisClient
347
499
  nil
348
500
  end
349
501
 
350
- def call_pipelined(connection, commands, timeouts = nil)
351
- exception = nil
352
-
353
- size = commands.size
354
- results = Array.new(commands.size)
355
- connection.write_multi(commands)
356
-
357
- size.times do |index|
358
- timeout = timeouts && timeouts[index]
359
- result = connection.read(timeout)
360
- if result.is_a?(CommandError)
361
- exception ||= result
362
- end
363
- results[index] = result
364
- end
365
-
366
- if exception
367
- raise exception
368
- else
369
- results
370
- end
371
- end
372
-
373
502
  def ensure_connected(retryable: true)
374
503
  if @disable_reconnection
375
504
  yield @raw_connection
@@ -407,32 +536,36 @@ class RedisClient
407
536
  end
408
537
 
409
538
  def raw_connection
410
- @raw_connection ||= begin
411
- connection = config.driver.new(
412
- config,
413
- connect_timeout: connect_timeout,
414
- read_timeout: read_timeout,
415
- write_timeout: write_timeout,
416
- )
417
-
418
- prelude = config.connection_prelude.dup
419
-
420
- if id
421
- prelude << ["CLIENT", "SETNAME", id.to_s]
422
- end
539
+ @raw_connection ||= connect
540
+ end
423
541
 
424
- if config.sentinel?
425
- prelude << ["ROLE"]
426
- role, = call_pipelined(connection, prelude).last
427
- config.check_role!(role)
428
- else
429
- call_pipelined(connection, prelude)
430
- end
542
+ def connect
543
+ connection = config.driver.new(
544
+ config,
545
+ connect_timeout: connect_timeout,
546
+ read_timeout: read_timeout,
547
+ write_timeout: write_timeout,
548
+ )
549
+
550
+ prelude = config.connection_prelude.dup
431
551
 
432
- connection
552
+ if id
553
+ prelude << ["CLIENT", "SETNAME", id.to_s]
433
554
  end
555
+
556
+ # The connection prelude is deliberately not sent to Middlewares
557
+ if config.sentinel?
558
+ prelude << ["ROLE"]
559
+ role, = connection.call_pipelined(prelude, nil).last
560
+ config.check_role!(role)
561
+ else
562
+ connection.call_pipelined(prelude, nil)
563
+ end
564
+
565
+ connection
434
566
  end
435
567
  end
436
568
 
437
- require "redis_client/resp3"
438
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.1.0
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-15 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
@@ -39,6 +39,8 @@ files:
39
39
  - LICENSE.md
40
40
  - README.md
41
41
  - Rakefile
42
+ - ext/redis_client/hiredis/export.clang
43
+ - ext/redis_client/hiredis/export.gcc
42
44
  - ext/redis_client/hiredis/extconf.rb
43
45
  - ext/redis_client/hiredis/hiredis_connection.c
44
46
  - ext/redis_client/hiredis/vendor/.gitignore
@@ -87,12 +89,16 @@ files:
87
89
  - ext/redis_client/hiredis/vendor/win32.h
88
90
  - lib/redis-client.rb
89
91
  - lib/redis_client.rb
90
- - lib/redis_client/buffered_io.rb
92
+ - lib/redis_client/command_builder.rb
91
93
  - lib/redis_client/config.rb
92
- - lib/redis_client/connection.rb
94
+ - lib/redis_client/connection_mixin.rb
95
+ - lib/redis_client/decorator.rb
93
96
  - lib/redis_client/hiredis_connection.rb
97
+ - lib/redis_client/middlewares.rb
94
98
  - lib/redis_client/pooled.rb
95
- - 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
96
102
  - lib/redis_client/sentinel_config.rb
97
103
  - lib/redis_client/version.rb
98
104
  - redis-client.gemspec