redis-client 0.1.0 → 0.3.0

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