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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +19 -0
- data/Gemfile.lock +2 -2
- data/README.md +89 -3
- data/Rakefile +15 -6
- data/ext/redis_client/hiredis/export.clang +2 -0
- data/ext/redis_client/hiredis/export.gcc +7 -0
- data/ext/redis_client/hiredis/extconf.rb +24 -9
- data/ext/redis_client/hiredis/hiredis_connection.c +13 -1
- data/lib/redis_client/command_builder.rb +83 -0
- data/lib/redis_client/config.rb +9 -48
- data/lib/redis_client/connection_mixin.rb +38 -0
- data/lib/redis_client/decorator.rb +84 -0
- data/lib/redis_client/hiredis_connection.rb +16 -1
- data/lib/redis_client/middlewares.rb +12 -0
- data/lib/redis_client/pooled.rb +55 -35
- data/lib/redis_client/ruby_connection/buffered_io.rb +153 -0
- data/lib/redis_client/{resp3.rb → ruby_connection/resp3.rb} +1 -27
- data/lib/redis_client/{connection.rb → ruby_connection.rb} +51 -5
- data/lib/redis_client/version.rb +1 -1
- data/lib/redis_client.rb +239 -106
- metadata +11 -5
- data/lib/redis_client/buffered_io.rb +0 -149
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/
|
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
|
-
|
58
|
-
attr_accessor :connect_timeout, :read_timeout, :write_timeout
|
140
|
+
include Common
|
59
141
|
|
60
|
-
def initialize(
|
61
|
-
|
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
|
-
|
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 =
|
178
|
+
def call(*command, **kwargs)
|
179
|
+
command = @command_builder.generate!(command, kwargs)
|
97
180
|
result = ensure_connected do |connection|
|
98
|
-
|
99
|
-
|
181
|
+
Middlewares.call(command, config) do
|
182
|
+
connection.call(command, nil)
|
183
|
+
end
|
100
184
|
end
|
101
185
|
|
102
|
-
if
|
103
|
-
|
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 =
|
193
|
+
def call_once(*command, **kwargs)
|
194
|
+
command = @command_builder.generate!(command, kwargs)
|
111
195
|
result = ensure_connected(retryable: false) do |connection|
|
112
|
-
|
113
|
-
|
196
|
+
Middlewares.call(command, config) do
|
197
|
+
connection.call(command, nil)
|
198
|
+
end
|
114
199
|
end
|
115
200
|
|
116
|
-
if
|
117
|
-
|
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 =
|
208
|
+
def blocking_call(timeout, *command, **kwargs)
|
209
|
+
command = @command_builder.generate!(command, kwargs)
|
125
210
|
result = ensure_connected do |connection|
|
126
|
-
|
127
|
-
|
211
|
+
Middlewares.call(command, config) do
|
212
|
+
connection.call(command, timeout)
|
213
|
+
end
|
128
214
|
end
|
129
215
|
|
130
|
-
if
|
131
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
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
|
-
|
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
|
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
|
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 ||=
|
411
|
-
|
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
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
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
|
-
|
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.
|
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-
|
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/
|
92
|
+
- lib/redis_client/command_builder.rb
|
91
93
|
- lib/redis_client/config.rb
|
92
|
-
- lib/redis_client/
|
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/
|
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
|