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.
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '097ad5acec4f5ea01788629c3fe2c606e54c1a2a0749c4e489487cf6bd446eae'
|
4
|
+
data.tar.gz: 275d778806cb7848d99ddcba33f1e932853efc9f0d149d6409692d1b39452d86
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c553d706fe8b955a0c6b65a28e9d6a1ddcdec156a562ec5092396a0bca9fa72a251441b8675484fff2d4db3d8e2f335043d226bb4c9c3359e84acc9b7605543f
|
7
|
+
data.tar.gz: 68dcc5d81b1632dd60d22651d0ef600903fe607230d3ac396d5e98aab7694d374d5e577777234d7e514fc4df04b332e0fdb31b3c9971fb116c6b0e87c1ddc5f5
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,22 @@
|
|
1
|
+
# Unreleased
|
2
|
+
|
3
|
+
# 0.3.0
|
4
|
+
|
5
|
+
- `hiredis` is now the default driver when available.
|
6
|
+
- Add `RedisClient.default_driver=`.
|
7
|
+
- `#call` now takes an optional block to cast the return value.
|
8
|
+
- Treat `#call` keyword arguments as Redis flags.
|
9
|
+
- Fix `RedisClient#multi` returning some errors as values instead of raising them.
|
10
|
+
|
11
|
+
# 0.2.1
|
12
|
+
|
13
|
+
- Use a more robust way to detect the current compiler.
|
14
|
+
|
15
|
+
# 0.2.0
|
16
|
+
- Added `RedisClient.register` as a public instrumentation API.
|
17
|
+
- Fix `read_timeout=` and `write_timeout=` to apply even when the client or pool is already connected.
|
18
|
+
- Properly convert DNS resolution errors into `RedisClient::ConnectionError`. Previously it would raise `SocketError`
|
19
|
+
|
1
20
|
# 0.1.0
|
2
21
|
|
3
22
|
- Initial Release
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -145,19 +145,68 @@ redis.call("LPUSH", "list", "1", "2", "3", "4")
|
|
145
145
|
Hashes are flatenned as well:
|
146
146
|
|
147
147
|
```ruby
|
148
|
-
redis.call("HMSET", "hash", foo
|
149
|
-
redis.call("SET", "key", "value", ex: 5)
|
148
|
+
redis.call("HMSET", "hash", { "foo" => "1", "bar" => "2" })
|
150
149
|
```
|
151
150
|
|
152
151
|
is equivalent to:
|
153
152
|
|
154
153
|
```ruby
|
155
154
|
redis.call("HMSET", "hash", "foo", "1", "bar", "2")
|
156
|
-
redis.call("SET", "key", "value", "ex", "5")
|
157
155
|
```
|
158
156
|
|
159
157
|
Any other type requires the caller to explictly cast the argument as a string.
|
160
158
|
|
159
|
+
Keywords arguments are treated as Redis command flags:
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
redis.call("SET", "mykey", "value", nx: true, ex: 60)
|
163
|
+
redis.call("SET", "mykey", "value", nx: false, ex: nil)
|
164
|
+
```
|
165
|
+
|
166
|
+
is equivalent to:
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
redis.call("SET", "mykey", "value", "nx", "ex", "60")
|
170
|
+
redis.call("SET", "mykey", "value")
|
171
|
+
```
|
172
|
+
|
173
|
+
If flags are built dynamically, you'll have to explictly pass them as keyword arguments with `**`:
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
flags = {}
|
177
|
+
flags[:nx] = true if something?
|
178
|
+
redis.call("SET", "mykey", "value", **flags)
|
179
|
+
```
|
180
|
+
|
181
|
+
**Important Note**: because of the keyword argument semantic change between Ruby 2 and Ruby 3,
|
182
|
+
unclosed hash literals with string keys may be interpreted differently:
|
183
|
+
|
184
|
+
```ruby
|
185
|
+
redis.call("HMSET", "hash", "foo" => "bar")
|
186
|
+
```
|
187
|
+
|
188
|
+
On Ruby 2 `"foo" => "bar"` will be passed as a postional argument, but on Ruby 3 it will be interpreted as keyword
|
189
|
+
arguments. To avoid such problem, make sure to enclose hash literals:
|
190
|
+
|
191
|
+
```ruby
|
192
|
+
redis.call("HMSET", "hash", { "foo" => "bar" })
|
193
|
+
```
|
194
|
+
|
195
|
+
### Commands return values
|
196
|
+
|
197
|
+
Contrary to the `redis` gem, `redis-client` doesn't do any type casting on the return value of commands.
|
198
|
+
|
199
|
+
If you wish to cast the return value, you can pass a block to the `#call` familly of methods:
|
200
|
+
|
201
|
+
```ruby
|
202
|
+
redis.call("INCR", "counter") # => 1
|
203
|
+
redis.call("GET", "counter") # => "1"
|
204
|
+
redis.call("GET", "counter", &:to_i) # => 1
|
205
|
+
|
206
|
+
redis.call("EXISTS", "counter") # => 1
|
207
|
+
redis.call("EXISTS", "counter") { |c| c > 0 } # => true
|
208
|
+
```
|
209
|
+
|
161
210
|
### Blocking commands
|
162
211
|
|
163
212
|
For blocking commands such as `BRPOP`, a custom timeout duration can be passed as first argument of the `#blocking_call` method:
|
@@ -270,6 +319,25 @@ end
|
|
270
319
|
|
271
320
|
## Production
|
272
321
|
|
322
|
+
### Instrumentation
|
323
|
+
|
324
|
+
`redis-client` offers a public instrumentation API monitoring tools.
|
325
|
+
|
326
|
+
```ruby
|
327
|
+
module MyRedisInstrumentation
|
328
|
+
def call(command, redis_config)
|
329
|
+
MyMonitoringService.instrument("redis.query") { super }
|
330
|
+
end
|
331
|
+
|
332
|
+
def call_pipelined(commands, redis_config)
|
333
|
+
MyMonitoringService.instrument("redis.pipeline") { super }
|
334
|
+
end
|
335
|
+
end
|
336
|
+
RedisClient.register(MyRedisInstrumentation)
|
337
|
+
```
|
338
|
+
|
339
|
+
Note that this instrumentation is global.
|
340
|
+
|
273
341
|
### Timeouts
|
274
342
|
|
275
343
|
The client allows you to configure connect, read, and write timeouts.
|
@@ -319,6 +387,24 @@ redis.call("GET", "counter") # Will be retried up to 3 times.
|
|
319
387
|
redis.call_once("INCR", "counter") # Won't be retried.
|
320
388
|
```
|
321
389
|
|
390
|
+
### Drivers
|
391
|
+
|
392
|
+
`redis-client` ships with two connection implementations, a `hiredis` binding and a pure Ruby implementation.
|
393
|
+
|
394
|
+
The hiredis binding is only available on Linux, macOS and other POSIX platforms. When available it is the default.
|
395
|
+
|
396
|
+
The default driver can be set through `RedisClient.default_driver=`:
|
397
|
+
|
398
|
+
```ruby
|
399
|
+
RedisClient.default_driver = :ruby
|
400
|
+
```
|
401
|
+
|
402
|
+
You can also select the driver on a per connection basis:
|
403
|
+
|
404
|
+
```ruby
|
405
|
+
redis_config = RedisClient.config(driver: :ruby, ...)
|
406
|
+
```
|
407
|
+
|
322
408
|
## Notable differences with the `redis` gem
|
323
409
|
|
324
410
|
### Thread Safety
|
data/Rakefile
CHANGED
@@ -19,7 +19,16 @@ end
|
|
19
19
|
Rake::TestTask.new(:test) do |t|
|
20
20
|
t.libs << "test"
|
21
21
|
t.libs << "lib"
|
22
|
-
t.test_files = FileList["test/**/*_test.rb"]
|
22
|
+
t.test_files = FileList["test/**/*_test.rb"].exclude("test/sentinel/*_test.rb")
|
23
|
+
end
|
24
|
+
|
25
|
+
namespace :test do
|
26
|
+
Rake::TestTask.new(:sentinel) do |t|
|
27
|
+
t.libs << "test/sentinel"
|
28
|
+
t.libs << "test"
|
29
|
+
t.libs << "lib"
|
30
|
+
t.test_files = FileList["test/sentinel/*_test.rb"]
|
31
|
+
end
|
23
32
|
end
|
24
33
|
|
25
34
|
namespace :hiredis do
|
@@ -49,7 +58,7 @@ namespace :benchmark do
|
|
49
58
|
env = {}
|
50
59
|
args = []
|
51
60
|
args << "--yjit" if mode == :yjit
|
52
|
-
env["DRIVER"] =
|
61
|
+
env["DRIVER"] = mode == :hiredis ? "hiredis" : "ruby"
|
53
62
|
system(env, RbConfig.ruby, *args, "benchmark/#{suite}.rb", out: output)
|
54
63
|
end
|
55
64
|
|
@@ -74,13 +83,13 @@ namespace :benchmark do
|
|
74
83
|
end
|
75
84
|
|
76
85
|
if RUBY_PLATFORM == "java"
|
77
|
-
task default: %i[test rubocop]
|
86
|
+
task default: %i[test test:sentinel rubocop]
|
78
87
|
else
|
79
|
-
task default: %i[compile test rubocop]
|
88
|
+
task default: %i[compile test test:sentinel rubocop]
|
80
89
|
end
|
81
90
|
|
82
91
|
if ENV["DRIVER"] == "hiredis"
|
83
|
-
task ci: %i[compile test]
|
92
|
+
task ci: %i[compile test test:sentinel]
|
84
93
|
else
|
85
|
-
task ci: %i[test]
|
94
|
+
task ci: %i[test test:sentinel]
|
86
95
|
end
|
@@ -2,13 +2,13 @@
|
|
2
2
|
|
3
3
|
require "mkmf"
|
4
4
|
|
5
|
-
if RUBY_ENGINE == "ruby"
|
5
|
+
if RUBY_ENGINE == "ruby" && !RUBY_ENGINE.match?(/mswin/)
|
6
|
+
have_func("rb_hash_new_capa", "ruby.h")
|
7
|
+
|
6
8
|
hiredis_dir = File.expand_path('vendor', __dir__)
|
7
9
|
|
8
10
|
make_program = with_config("make-prog", ENV["MAKE"])
|
9
11
|
make_program ||= case RUBY_PLATFORM
|
10
|
-
when /mswin/
|
11
|
-
'nmake'
|
12
12
|
when /(bsd|solaris)/
|
13
13
|
'gmake'
|
14
14
|
else
|
@@ -31,18 +31,33 @@ if RUBY_ENGINE == "ruby"
|
|
31
31
|
end
|
32
32
|
|
33
33
|
Dir.chdir(hiredis_dir) do
|
34
|
-
flags =
|
35
|
-
|
36
|
-
|
34
|
+
flags = ["static", "USE_SSL=1"]
|
35
|
+
if openssl_lib
|
36
|
+
flags << %(CFLAGS="-I#{openssl_include}") << %(SSL_LDFLAGS="-L#{openssl_lib}")
|
37
|
+
end
|
38
|
+
|
39
|
+
flags << "OPTIMIZATION=-g" if ENV["EXT_PEDANTIC"]
|
40
|
+
|
41
|
+
unless system(make_program, *flags)
|
42
|
+
raise "Building hiredis failed"
|
43
|
+
end
|
37
44
|
end
|
38
45
|
|
39
46
|
$CFLAGS << " -I#{hiredis_dir}"
|
40
|
-
$LDFLAGS << "
|
41
|
-
$
|
47
|
+
$LDFLAGS << " -lssl -lcrypto"
|
48
|
+
$libs << " #{hiredis_dir}/libhiredis.a #{hiredis_dir}/libhiredis_ssl.a "
|
42
49
|
$CFLAGS << " -std=c99 "
|
43
|
-
|
44
50
|
if ENV["EXT_PEDANTIC"]
|
45
51
|
$CFLAGS << " -Werror"
|
52
|
+
$CFLAGS << " -g "
|
53
|
+
else
|
54
|
+
$CFLAGS << " -O3 "
|
55
|
+
end
|
56
|
+
|
57
|
+
if `cc --version`.match?(/ clang /i) || RbConfig::CONFIG['CC'].match?(/clang/i)
|
58
|
+
$LDFLAGS << ' -Wl,-exported_symbols_list,"' << File.join(__dir__, 'export.clang') << '"'
|
59
|
+
elsif RbConfig::CONFIG['CC'].match?(/gcc/i)
|
60
|
+
$LDFLAGS << ' -Wl,--version-script="' << File.join(__dir__, 'export.gcc') << '"'
|
46
61
|
end
|
47
62
|
|
48
63
|
$CFLAGS << " -Wno-declaration-after-statement" # Older compilers
|
@@ -38,6 +38,13 @@
|
|
38
38
|
#include "hiredis.h"
|
39
39
|
#include "hiredis_ssl.h"
|
40
40
|
|
41
|
+
#if !defined(HAVE_RB_HASH_NEW_CAPA)
|
42
|
+
static inline VALUE rb_hash_new_capa(long capa)
|
43
|
+
{
|
44
|
+
return rb_hash_new();
|
45
|
+
}
|
46
|
+
#endif
|
47
|
+
|
41
48
|
static VALUE rb_cSet, rb_eRedisClientCommandError, rb_eRedisClientConnectionError;
|
42
49
|
static VALUE rb_eRedisClientConnectTimeoutError, rb_eRedisClientReadTimeoutError, rb_eRedisClientWriteTimeoutError;
|
43
50
|
static ID id_parse, id_add, id_new;
|
@@ -148,6 +155,10 @@ static void *reply_create_string(const redisReadTask *task, char *cstr, size_t l
|
|
148
155
|
rb_enc_associate(string, rb_ascii8bit_encoding());
|
149
156
|
}
|
150
157
|
|
158
|
+
if (task->type == REDIS_REPLY_STATUS) {
|
159
|
+
rb_str_freeze(string);
|
160
|
+
}
|
161
|
+
|
151
162
|
if (task->type == REDIS_REPLY_ERROR) {
|
152
163
|
string = rb_funcall(rb_eRedisClientCommandError, id_parse, 1, string);
|
153
164
|
}
|
@@ -162,7 +173,7 @@ static void *reply_create_array(const redisReadTask *task, size_t elements) {
|
|
162
173
|
value = rb_ary_new_capa(elements);
|
163
174
|
break;
|
164
175
|
case REDIS_REPLY_MAP:
|
165
|
-
value =
|
176
|
+
value = rb_hash_new_capa(elements / 2);
|
166
177
|
break;
|
167
178
|
case REDIS_REPLY_SET:
|
168
179
|
value = rb_funcallv(rb_cSet, id_new, 0, NULL);
|
@@ -428,6 +439,7 @@ static VALUE hiredis_connect_finish(hiredis_connection_t *connection, redisConte
|
|
428
439
|
|
429
440
|
/* Check for socket error */
|
430
441
|
if (getsockopt(context->fd, SOL_SOCKET, SO_ERROR, &optval, &optlen) < 0) {
|
442
|
+
context->err = REDIS_ERR_IO;
|
431
443
|
redis_raise_error_and_disconnect(context, rb_eRedisClientConnectTimeoutError);
|
432
444
|
}
|
433
445
|
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class RedisClient
|
4
|
+
module CommandBuilder
|
5
|
+
extend self
|
6
|
+
|
7
|
+
if Symbol.method_defined?(:name)
|
8
|
+
def generate!(args, kwargs)
|
9
|
+
command = args.flat_map do |element|
|
10
|
+
case element
|
11
|
+
when Hash
|
12
|
+
element.flatten
|
13
|
+
when Set
|
14
|
+
element.to_a
|
15
|
+
else
|
16
|
+
element
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
kwargs.each do |key, value|
|
21
|
+
if value
|
22
|
+
if value == true
|
23
|
+
command << key.name
|
24
|
+
else
|
25
|
+
command << key.name << value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
command.map! do |element|
|
31
|
+
case element
|
32
|
+
when String
|
33
|
+
element
|
34
|
+
when Symbol
|
35
|
+
element.name
|
36
|
+
when Integer, Float
|
37
|
+
element.to_s
|
38
|
+
else
|
39
|
+
raise TypeError, "Unsupported command argument type: #{element.class}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
command
|
44
|
+
end
|
45
|
+
else
|
46
|
+
def generate!(args, kwargs)
|
47
|
+
command = args.flat_map do |element|
|
48
|
+
case element
|
49
|
+
when Hash
|
50
|
+
element.flatten
|
51
|
+
when Set
|
52
|
+
element.to_a
|
53
|
+
else
|
54
|
+
element
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
kwargs.each do |key, value|
|
59
|
+
if value
|
60
|
+
if value == true
|
61
|
+
command << key.to_s
|
62
|
+
else
|
63
|
+
command << key.to_s << value
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
command.map! do |element|
|
69
|
+
case element
|
70
|
+
when String
|
71
|
+
element
|
72
|
+
when Integer, Float, Symbol
|
73
|
+
element.to_s
|
74
|
+
else
|
75
|
+
raise TypeError, "Unsupported command argument type: #{element.class}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
command
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/redis_client/config.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "openssl"
|
3
4
|
require "uri"
|
4
5
|
|
5
6
|
class RedisClient
|
@@ -11,7 +12,7 @@ class RedisClient
|
|
11
12
|
DEFAULT_DB = 0
|
12
13
|
|
13
14
|
module Common
|
14
|
-
attr_reader :db, :username, :password, :id, :ssl, :ssl_params,
|
15
|
+
attr_reader :db, :username, :password, :id, :ssl, :ssl_params, :command_builder,
|
15
16
|
:connect_timeout, :read_timeout, :write_timeout, :driver, :connection_prelude
|
16
17
|
|
17
18
|
alias_method :ssl?, :ssl
|
@@ -27,7 +28,8 @@ class RedisClient
|
|
27
28
|
connect_timeout: timeout,
|
28
29
|
ssl: nil,
|
29
30
|
ssl_params: nil,
|
30
|
-
driver:
|
31
|
+
driver: nil,
|
32
|
+
command_builder: CommandBuilder,
|
31
33
|
reconnect_attempts: false
|
32
34
|
)
|
33
35
|
@username = username || DEFAULT_USERNAME
|
@@ -41,17 +43,9 @@ class RedisClient
|
|
41
43
|
@read_timeout = read_timeout
|
42
44
|
@write_timeout = write_timeout
|
43
45
|
|
44
|
-
@driver =
|
45
|
-
|
46
|
-
|
47
|
-
when :hiredis
|
48
|
-
unless defined?(RedisClient::HiredisConnection)
|
49
|
-
require "redis_client/hiredis_connection"
|
50
|
-
end
|
51
|
-
HiredisConnection
|
52
|
-
else
|
53
|
-
raise ArgumentError, "Unknown driver #{driver.inspect}, expected one of: `:ruby`, `:hiredis`"
|
54
|
-
end
|
46
|
+
@driver = driver ? RedisClient.driver(driver) : RedisClient.default_driver
|
47
|
+
|
48
|
+
@command_builder = command_builder
|
55
49
|
|
56
50
|
reconnect_attempts = Array.new(reconnect_attempts, 0).freeze if reconnect_attempts.is_a?(Integer)
|
57
51
|
@reconnect_attempts = reconnect_attempts
|
@@ -84,41 +78,8 @@ class RedisClient
|
|
84
78
|
false
|
85
79
|
end
|
86
80
|
|
87
|
-
def
|
88
|
-
@
|
89
|
-
ca_file: @ssl_params[:ca_file],
|
90
|
-
ca_path: @ssl_params[:ca_path],
|
91
|
-
cert: @ssl_params[:cert],
|
92
|
-
key: @ssl_params[:key],
|
93
|
-
hostname: @ssl_params[:hostname],
|
94
|
-
)
|
95
|
-
end
|
96
|
-
|
97
|
-
def openssl_context
|
98
|
-
@openssl_context ||= begin
|
99
|
-
params = @ssl_params.dup || {}
|
100
|
-
|
101
|
-
cert = params[:cert]
|
102
|
-
if cert.is_a?(String)
|
103
|
-
cert = File.read(cert) if File.exist?(cert)
|
104
|
-
params[:cert] = OpenSSL::X509::Certificate.new(cert)
|
105
|
-
end
|
106
|
-
|
107
|
-
key = params[:key]
|
108
|
-
if key.is_a?(String)
|
109
|
-
key = File.read(key) if File.exist?(key)
|
110
|
-
params[:key] = OpenSSL::PKey.read(key)
|
111
|
-
end
|
112
|
-
|
113
|
-
context = OpenSSL::SSL::SSLContext.new
|
114
|
-
context.set_params(params)
|
115
|
-
if context.verify_mode != OpenSSL::SSL::VERIFY_NONE
|
116
|
-
if context.respond_to?(:verify_hostname) # Missing on JRuby
|
117
|
-
context.verify_hostname
|
118
|
-
end
|
119
|
-
end
|
120
|
-
context
|
121
|
-
end
|
81
|
+
def ssl_context
|
82
|
+
@ssl_context ||= @driver.ssl_context(@ssl_params)
|
122
83
|
end
|
123
84
|
|
124
85
|
private
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class RedisClient
|
4
|
+
module ConnectionMixin
|
5
|
+
def call(command, timeout)
|
6
|
+
write(command)
|
7
|
+
result = read(timeout)
|
8
|
+
if result.is_a?(CommandError)
|
9
|
+
raise result
|
10
|
+
else
|
11
|
+
result
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def call_pipelined(commands, timeouts)
|
16
|
+
exception = nil
|
17
|
+
|
18
|
+
size = commands.size
|
19
|
+
results = Array.new(commands.size)
|
20
|
+
write_multi(commands)
|
21
|
+
|
22
|
+
size.times do |index|
|
23
|
+
timeout = timeouts && timeouts[index]
|
24
|
+
result = read(timeout)
|
25
|
+
if result.is_a?(CommandError)
|
26
|
+
exception ||= result
|
27
|
+
end
|
28
|
+
results[index] = result
|
29
|
+
end
|
30
|
+
|
31
|
+
if exception
|
32
|
+
raise exception
|
33
|
+
else
|
34
|
+
results
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class RedisClient
|
4
|
+
module Decorator
|
5
|
+
class << self
|
6
|
+
def create(commands_mixin)
|
7
|
+
client_decorator = Class.new(Client)
|
8
|
+
client_decorator.include(commands_mixin)
|
9
|
+
|
10
|
+
pipeline_decorator = Class.new(Pipeline)
|
11
|
+
pipeline_decorator.include(commands_mixin)
|
12
|
+
client_decorator.const_set(:Pipeline, pipeline_decorator)
|
13
|
+
|
14
|
+
client_decorator
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module CommandsMixin
|
19
|
+
def initialize(client)
|
20
|
+
@client = client
|
21
|
+
end
|
22
|
+
|
23
|
+
%i(call call_once blocking_call).each do |method|
|
24
|
+
class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
|
25
|
+
def #{method}(*args, &block)
|
26
|
+
@client.#{method}(*args, &block)
|
27
|
+
end
|
28
|
+
ruby2_keywords :#{method} if respond_to?(:ruby2_keywords, true)
|
29
|
+
RUBY
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Pipeline
|
34
|
+
include CommandsMixin
|
35
|
+
end
|
36
|
+
|
37
|
+
class Client
|
38
|
+
include CommandsMixin
|
39
|
+
|
40
|
+
def initialize(_client)
|
41
|
+
super
|
42
|
+
@_pipeline_class = self.class::Pipeline
|
43
|
+
end
|
44
|
+
|
45
|
+
def with(*args)
|
46
|
+
@client.with(*args) { |c| yield self.class.new(c) }
|
47
|
+
end
|
48
|
+
ruby2_keywords :with if respond_to?(:ruby2_keywords, true)
|
49
|
+
|
50
|
+
def pipelined
|
51
|
+
@client.pipelined { |p| yield @_pipeline_class.new(p) }
|
52
|
+
end
|
53
|
+
|
54
|
+
def multi(**kwargs)
|
55
|
+
@client.multi(**kwargs) { |p| yield @_pipeline_class.new(p) }
|
56
|
+
end
|
57
|
+
|
58
|
+
%i(close scan hscan sscan zscan).each do |method|
|
59
|
+
class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
|
60
|
+
def #{method}(*args, &block)
|
61
|
+
@client.#{method}(*args, &block)
|
62
|
+
end
|
63
|
+
ruby2_keywords :#{method} if respond_to?(:ruby2_keywords, true)
|
64
|
+
RUBY
|
65
|
+
end
|
66
|
+
|
67
|
+
%i(id config size connect_timeout read_timeout write_timeout).each do |reader|
|
68
|
+
class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
|
69
|
+
def #{reader}
|
70
|
+
@client.#{reader}
|
71
|
+
end
|
72
|
+
RUBY
|
73
|
+
end
|
74
|
+
|
75
|
+
%i(timeout connect_timeout read_timeout write_timeout).each do |writer|
|
76
|
+
class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
|
77
|
+
def #{writer}=(value)
|
78
|
+
@client.#{writer} = value
|
79
|
+
end
|
80
|
+
RUBY
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -1,9 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "redis_client/hiredis_connection.so"
|
4
|
+
require "redis_client/connection_mixin"
|
4
5
|
|
5
6
|
class RedisClient
|
6
7
|
class HiredisConnection
|
8
|
+
include ConnectionMixin
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def ssl_context(ssl_params)
|
12
|
+
HiredisConnection::SSLContext.new(
|
13
|
+
ca_file: ssl_params[:ca_file],
|
14
|
+
ca_path: ssl_params[:ca_path],
|
15
|
+
cert: ssl_params[:cert],
|
16
|
+
key: ssl_params[:key],
|
17
|
+
hostname: ssl_params[:hostname],
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
7
22
|
class SSLContext
|
8
23
|
def initialize(ca_file: nil, ca_path: nil, cert: nil, key: nil, hostname: nil)
|
9
24
|
if (error = init(ca_file, ca_path, cert, key, hostname))
|
@@ -24,7 +39,7 @@ class RedisClient
|
|
24
39
|
end
|
25
40
|
|
26
41
|
if config.ssl
|
27
|
-
init_ssl(config.
|
42
|
+
init_ssl(config.ssl_context)
|
28
43
|
end
|
29
44
|
end
|
30
45
|
|