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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f09f4bc8f3f2cf92a6beb3b02ef601209bc36073eee68e4d57c93f802b10db2f
4
- data.tar.gz: c3539ba4db69fac8a98649e27125409feb62d925b55a2c03bc75e87d26e60a89
3
+ metadata.gz: '097ad5acec4f5ea01788629c3fe2c606e54c1a2a0749c4e489487cf6bd446eae'
4
+ data.tar.gz: 275d778806cb7848d99ddcba33f1e932853efc9f0d149d6409692d1b39452d86
5
5
  SHA512:
6
- metadata.gz: '08bbfce855729654d6f74830a18a6aaea755f4790c58ccf92cc53ac321a6a3ca4d5935a885498c89d48d781b5affc16e757524e7d1f2355da57b5048ca968528'
7
- data.tar.gz: aac332fe44444ac5afdf5b23ba35e23dc3129a50c5c9c51628f24d151620c26108984574decd5ab25435f39ee305cf4933ab0413aaad7076cf525503c9ec50d3
6
+ metadata.gz: c553d706fe8b955a0c6b65a28e9d6a1ddcdec156a562ec5092396a0bca9fa72a251441b8675484fff2d4db3d8e2f335043d226bb4c9c3359e84acc9b7605543f
7
+ data.tar.gz: 68dcc5d81b1632dd60d22651d0ef600903fe607230d3ac396d5e98aab7694d374d5e577777234d7e514fc4df04b332e0fdb31b3c9971fb116c6b0e87c1ddc5f5
data/.rubocop.yml CHANGED
@@ -7,6 +7,7 @@ Layout/LineLength:
7
7
  Max: 120
8
8
  Exclude:
9
9
  - 'test/**/*'
10
+ - 'benchmark/**/*'
10
11
 
11
12
  Layout/CaseIndentation:
12
13
  EnforcedStyle: end
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Unreleased
2
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
+
3
11
  # 0.2.1
4
12
 
5
13
  - Use a more robust way to detect the current compiler.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- redis-client (0.2.1)
4
+ redis-client (0.3.0)
5
5
  connection_pool
6
6
 
7
7
  GEM
@@ -64,4 +64,4 @@ DEPENDENCIES
64
64
  toxiproxy
65
65
 
66
66
  BUNDLED WITH
67
- 2.3.8
67
+ 2.3.13
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: 1, bar: 2)
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:
@@ -338,6 +387,24 @@ redis.call("GET", "counter") # Will be retried up to 3 times.
338
387
  redis.call_once("INCR", "counter") # Won't be retried.
339
388
  ```
340
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
+
341
408
  ## Notable differences with the `redis` gem
342
409
 
343
410
  ### Thread Safety
data/Rakefile CHANGED
@@ -24,6 +24,7 @@ end
24
24
 
25
25
  namespace :test do
26
26
  Rake::TestTask.new(:sentinel) do |t|
27
+ t.libs << "test/sentinel"
27
28
  t.libs << "test"
28
29
  t.libs << "lib"
29
30
  t.test_files = FileList["test/sentinel/*_test.rb"]
@@ -57,7 +58,7 @@ namespace :benchmark do
57
58
  env = {}
58
59
  args = []
59
60
  args << "--yjit" if mode == :yjit
60
- env["DRIVER"] = "hiredis" if mode == :hiredis
61
+ env["DRIVER"] = mode == :hiredis ? "hiredis" : "ruby"
61
62
  system(env, RbConfig.ruby, *args, "benchmark/#{suite}.rb", out: output)
62
63
  end
63
64
 
@@ -31,16 +31,28 @@ if RUBY_ENGINE == "ruby" && !RUBY_ENGINE.match?(/mswin/)
31
31
  end
32
32
 
33
33
  Dir.chdir(hiredis_dir) do
34
- flags = %(CFLAGS="-I#{openssl_include}" SSL_LDFLAGS="-L#{openssl_lib}") if openssl_lib
35
- success = system("#{make_program} static USE_SSL=1 #{flags}")
36
- raise "Building hiredis failed" unless success
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
47
  $LDFLAGS << " -lssl -lcrypto"
41
48
  $libs << " #{hiredis_dir}/libhiredis.a #{hiredis_dir}/libhiredis_ssl.a "
42
- $CFLAGS << " -O3"
43
49
  $CFLAGS << " -std=c99 "
50
+ if ENV["EXT_PEDANTIC"]
51
+ $CFLAGS << " -Werror"
52
+ $CFLAGS << " -g "
53
+ else
54
+ $CFLAGS << " -O3 "
55
+ end
44
56
 
45
57
  if `cc --version`.match?(/ clang /i) || RbConfig::CONFIG['CC'].match?(/clang/i)
46
58
  $LDFLAGS << ' -Wl,-exported_symbols_list,"' << File.join(__dir__, 'export.clang') << '"'
@@ -48,10 +60,6 @@ if RUBY_ENGINE == "ruby" && !RUBY_ENGINE.match?(/mswin/)
48
60
  $LDFLAGS << ' -Wl,--version-script="' << File.join(__dir__, 'export.gcc') << '"'
49
61
  end
50
62
 
51
- if ENV["EXT_PEDANTIC"]
52
- $CFLAGS << " -Werror"
53
- end
54
-
55
63
  $CFLAGS << " -Wno-declaration-after-statement" # Older compilers
56
64
  $CFLAGS << " -Wno-compound-token-split-by-macro" # Older rubies on macos
57
65
 
@@ -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
@@ -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: :ruby,
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 = case driver
45
- when :ruby
46
- Connection
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 hiredis_ssl_context
88
- @hiredis_ssl_context ||= HiredisConnection::SSLContext.new(
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,10 +1,23 @@
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
7
- include Connection::Common
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
8
21
 
9
22
  class SSLContext
10
23
  def initialize(ca_file: nil, ca_path: nil, cert: nil, key: nil, hostname: nil)
@@ -26,7 +39,7 @@ class RedisClient
26
39
  end
27
40
 
28
41
  if config.ssl
29
- init_ssl(config.hiredis_ssl_context)
42
+ init_ssl(config.ssl_context)
30
43
  end
31
44
  end
32
45
 
@@ -49,40 +49,48 @@ class RedisClient
49
49
  pool.size
50
50
  end
51
51
 
52
- %w(pipelined).each do |method|
53
- class_eval <<~RUBY, __FILE__, __LINE__ + 1
54
- def #{method}(&block)
55
- with { |r| r.#{method}(&block) }
56
- end
57
- RUBY
58
- end
59
-
60
- %w(multi).each do |method|
61
- class_eval <<~RUBY, __FILE__, __LINE__ + 1
62
- def #{method}(**kwargs, &block)
63
- with { |r| r.#{method}(**kwargs, &block) }
64
- end
65
- RUBY
66
- end
52
+ methods = %w(pipelined multi pubsub call call_once blocking_call)
53
+ iterable_methods = %w(scan sscan hscan zscan)
54
+ begin
55
+ methods.each do |method|
56
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
57
+ def #{method}(...)
58
+ with { |r| r.#{method}(...) }
59
+ end
60
+ RUBY
61
+ end
67
62
 
68
- %w(call call_once blocking_call pubsub).each do |method|
69
- class_eval <<~RUBY, __FILE__, __LINE__ + 1
70
- def #{method}(*args)
71
- with { |r| r.#{method}(*args) }
72
- end
73
- RUBY
74
- end
63
+ iterable_methods.each do |method|
64
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
65
+ def #{method}(...)
66
+ unless block_given?
67
+ return to_enum(__callee__, ...)
68
+ end
75
69
 
76
- %w(scan sscan hscan zscan).each do |method|
77
- class_eval <<~RUBY, __FILE__, __LINE__ + 1
78
- def #{method}(*args, &block)
79
- unless block_given?
80
- return to_enum(__callee__, *args)
70
+ with { |r| r.#{method}(...) }
71
+ end
72
+ RUBY
73
+ end
74
+ rescue SyntaxError
75
+ methods.each do |method|
76
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
77
+ def #{method}(*args, &block)
78
+ with { |r| r.#{method}(*args, &block) }
81
79
  end
80
+ RUBY
81
+ end
82
82
 
83
- with { |r| r.#{method}(*args, &block) }
84
- end
85
- RUBY
83
+ iterable_methods.each do |method|
84
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
85
+ def #{method}(*args, &block)
86
+ unless block_given?
87
+ return to_enum(__callee__, *args)
88
+ end
89
+
90
+ with { |r| r.#{method}(*args, &block) }
91
+ end
92
+ RUBY
93
+ end
86
94
  end
87
95
 
88
96
  private