redis-client 0.1.0 → 0.2.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: d53aaf68e8bf2bc7160a8b708d86c40d13a229a2bfa88734ada0ae03839d6330
4
- data.tar.gz: 7ebc1e2e488dbd0bc7895db4714237e893408f017222922cc94faec4b7646e7c
3
+ metadata.gz: eba18b5a126a8f4f9e76c842c41fd3ab2672c6d08be0d9023536e7fdbbf2ac1c
4
+ data.tar.gz: 0a012bad73ae54550cae8dde282fdaf644a04a1bd748cd37993a31592ccf2259
5
5
  SHA512:
6
- metadata.gz: 397746e753c9c44e6b6f7d046b9e8cb389c25c7a8d8dea40e7ab81c099f328d4f34c1d77d7c2c66c0ea7b12056f282a76c3dc5a700b1acee49b89cddc509070d
7
- data.tar.gz: a2fcda7b8bbef1a0988fcd7434306da5769dbcf1ad91b5f790186646eedd1b21fa289b044bacd3f3b91486f8d06e73c51d608f92662bcb366e3d3e871a138b40
6
+ metadata.gz: 37ccff70160eff897f9881784db776679e8b97cec95b89e6db3f13e5294799c3d6ca7dd97e83ce54699819943c7a10904fb76567a7944a8c83fc88408217d294
7
+ data.tar.gz: c366d95353b081199751910676a9b0e682ff7cb0559c91d7c5112ec7f486a2edb0afa9317f9066953bd13b2f359f6fcb0c4fda268852aa7176557cc42d6b0f94
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # Unreleased
2
+
3
+ # 0.2.0
4
+ - Added `RedisClient.register` as a public instrumentation API.
5
+ - Fix `read_timeout=` and `write_timeout=` to apply even when the client or pool is already connected.
6
+ - Properly convert DNS resolution errors into `RedisClient::ConnectionError`. Previously it would raise `SocketError`
7
+
1
8
  # 0.1.0
2
9
 
3
10
  - Initial Release
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- redis-client (0.0.0)
4
+ redis-client (0.2.0)
5
5
  connection_pool
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -270,6 +270,25 @@ end
270
270
 
271
271
  ## Production
272
272
 
273
+ ### Instrumentation
274
+
275
+ `redis-client` offers a public instrumentation API monitoring tools.
276
+
277
+ ```ruby
278
+ module MyRedisInstrumentation
279
+ def call(command, redis_config)
280
+ MyMonitoringService.instrument("redis.query") { super }
281
+ end
282
+
283
+ def call_pipelined(commands, redis_config)
284
+ MyMonitoringService.instrument("redis.pipeline") { super }
285
+ end
286
+ end
287
+ RedisClient.register(MyRedisInstrumentation)
288
+ ```
289
+
290
+ Note that this instrumentation is global.
291
+
273
292
  ### Timeouts
274
293
 
275
294
  The client allows you to configure connect, read, and write timeouts.
data/Rakefile CHANGED
@@ -19,7 +19,15 @@ 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"
28
+ t.libs << "lib"
29
+ t.test_files = FileList["test/sentinel/*_test.rb"]
30
+ end
23
31
  end
24
32
 
25
33
  namespace :hiredis do
@@ -74,13 +82,13 @@ namespace :benchmark do
74
82
  end
75
83
 
76
84
  if RUBY_PLATFORM == "java"
77
- task default: %i[test rubocop]
85
+ task default: %i[test test:sentinel rubocop]
78
86
  else
79
- task default: %i[compile test rubocop]
87
+ task default: %i[compile test test:sentinel rubocop]
80
88
  end
81
89
 
82
90
  if ENV["DRIVER"] == "hiredis"
83
- task ci: %i[compile test]
91
+ task ci: %i[compile test test:sentinel]
84
92
  else
85
- task ci: %i[test]
93
+ task ci: %i[test test:sentinel]
86
94
  end
@@ -0,0 +1,2 @@
1
+ _Init_hiredis_connection
2
+ _ruby_abi_version
@@ -0,0 +1,7 @@
1
+ hiredis_connection_1.0 {
2
+ global:
3
+ Init_hiredis_connection;
4
+ ruby_abi_version;
5
+ local:
6
+ *;
7
+ };
@@ -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
@@ -37,10 +37,18 @@ if RUBY_ENGINE == "ruby"
37
37
  end
38
38
 
39
39
  $CFLAGS << " -I#{hiredis_dir}"
40
- $LDFLAGS << " #{hiredis_dir}/libhiredis.a #{hiredis_dir}/libhiredis_ssl.a -lssl -lcrypto"
40
+ $LDFLAGS << " -lssl -lcrypto"
41
+ $libs << " #{hiredis_dir}/libhiredis.a #{hiredis_dir}/libhiredis_ssl.a "
41
42
  $CFLAGS << " -O3"
42
43
  $CFLAGS << " -std=c99 "
43
44
 
45
+ case RbConfig::CONFIG['CC']
46
+ when /gcc/i
47
+ $LDFLAGS << ' -Wl,--version-script="' << File.join(__dir__, 'export.gcc') << '"'
48
+ when /clang/i
49
+ $LDFLAGS << ' -Wl,-exported_symbols_list,"' << File.join(__dir__, 'export.clang') << '"'
50
+ end
51
+
44
52
  if ENV["EXT_PEDANTIC"]
45
53
  $CFLAGS << " -Werror"
46
54
  end
@@ -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 = rb_hash_new();
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
 
@@ -7,6 +7,8 @@ class RedisClient
7
7
  EOL = "\r\n".b.freeze
8
8
  EOL_SIZE = EOL.bytesize
9
9
 
10
+ attr_accessor :read_timeout, :write_timeout
11
+
10
12
  def initialize(io, read_timeout:, write_timeout:, chunk_size: 4096)
11
13
  @io = io
12
14
  @buffer = "".b
@@ -6,11 +6,54 @@ require "redis_client/buffered_io"
6
6
 
7
7
  class RedisClient
8
8
  class Connection
9
+ module Common
10
+ def call(command, timeout)
11
+ write(command)
12
+ result = read(timeout)
13
+ if result.is_a?(CommandError)
14
+ raise result
15
+ else
16
+ result
17
+ end
18
+ end
19
+
20
+ def call_pipelined(commands, timeouts)
21
+ exception = nil
22
+
23
+ size = commands.size
24
+ results = Array.new(commands.size)
25
+ write_multi(commands)
26
+
27
+ size.times do |index|
28
+ timeout = timeouts && timeouts[index]
29
+ result = read(timeout)
30
+ if result.is_a?(CommandError)
31
+ exception ||= result
32
+ end
33
+ results[index] = result
34
+ end
35
+
36
+ if exception
37
+ raise exception
38
+ else
39
+ results
40
+ end
41
+ end
42
+ end
43
+
44
+ include Common
45
+
46
+ SUPPORTS_RESOLV_TIMEOUT = Socket.method(:tcp).parameters.any? { |p| p.last == :resolv_timeout }
47
+
9
48
  def initialize(config, connect_timeout:, read_timeout:, write_timeout:)
10
49
  socket = if config.path
11
50
  UNIXSocket.new(config.path)
12
51
  else
13
- sock = Socket.tcp(config.host, config.port, connect_timeout: connect_timeout)
52
+ sock = if SUPPORTS_RESOLV_TIMEOUT
53
+ Socket.tcp(config.host, config.port, connect_timeout: connect_timeout, resolv_timeout: connect_timeout)
54
+ else
55
+ Socket.tcp(config.host, config.port, connect_timeout: connect_timeout)
56
+ end
14
57
  # disables Nagle's Algorithm, prevents multiple round trips with MULTI
15
58
  sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
16
59
  sock
@@ -40,7 +83,7 @@ class RedisClient
40
83
  )
41
84
  rescue Errno::ETIMEDOUT => error
42
85
  raise ConnectTimeoutError, error.message
43
- rescue SystemCallError, OpenSSL::SSL::SSLError => error
86
+ rescue SystemCallError, OpenSSL::SSL::SSLError, SocketError => error
44
87
  raise ConnectionError, error.message
45
88
  end
46
89
 
@@ -52,6 +95,14 @@ class RedisClient
52
95
  @io.close
53
96
  end
54
97
 
98
+ def read_timeout=(timeout)
99
+ @io.read_timeout = timeout if @io
100
+ end
101
+
102
+ def write_timeout=(timeout)
103
+ @io.write_timeout = timeout if @io
104
+ end
105
+
55
106
  def write(command)
56
107
  buffer = RESP3.dump(command)
57
108
  begin
@@ -4,6 +4,8 @@ require "redis_client/hiredis_connection.so"
4
4
 
5
5
  class RedisClient
6
6
  class HiredisConnection
7
+ include Connection::Common
8
+
7
9
  class SSLContext
8
10
  def initialize(ca_file: nil, ca_path: nil, cert: nil, key: nil, hostname: nil)
9
11
  if (error = init(ca_file, ca_path, cert, key, hostname))
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RedisClient
4
+ module Middlewares
5
+ extend self
6
+
7
+ def call(command, _config)
8
+ yield command
9
+ end
10
+ alias_method :call_pipelined, :call
11
+ end
12
+ end
@@ -6,17 +6,29 @@ class RedisClient
6
6
  class Pooled
7
7
  EMPTY_HASH = {}.freeze
8
8
 
9
- attr_reader :config
9
+ include Common
10
10
 
11
- def initialize(config, **kwargs)
12
- @config = config
11
+ def initialize(
12
+ config,
13
+ id: config.id,
14
+ connect_timeout: config.connect_timeout,
15
+ read_timeout: config.read_timeout,
16
+ write_timeout: config.write_timeout,
17
+ **kwargs
18
+ )
19
+ super(config, id: id, connect_timeout: connect_timeout, read_timeout: read_timeout, write_timeout: write_timeout)
13
20
  @pool_kwargs = kwargs
14
21
  @pool = new_pool
15
22
  @mutex = Mutex.new
16
23
  end
17
24
 
18
- def with(options = EMPTY_HASH, &block)
19
- pool.with(options, &block)
25
+ def with(options = EMPTY_HASH)
26
+ pool.with(options) do |client|
27
+ client.connect_timeout = connect_timeout
28
+ client.read_timeout = read_timeout
29
+ client.write_timeout = write_timeout
30
+ yield client
31
+ end
20
32
  rescue ConnectionPool::TimeoutError => error
21
33
  raise CheckoutTimeoutError, "Couldn't checkout a connection in time: #{error.message}"
22
34
  end
@@ -147,7 +147,7 @@ class RedisClient
147
147
  str = io.gets_chomp
148
148
  str.force_encoding(Encoding.default_external)
149
149
  str.force_encoding(Encoding::BINARY) unless str.valid_encoding?
150
- str
150
+ str.freeze
151
151
  end
152
152
 
153
153
  def parse_error(io)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class RedisClient
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/redis_client.rb CHANGED
@@ -4,8 +4,32 @@ require "redis_client/version"
4
4
  require "redis_client/config"
5
5
  require "redis_client/sentinel_config"
6
6
  require "redis_client/connection"
7
+ require "redis_client/middlewares"
7
8
 
8
9
  class RedisClient
10
+ module Common
11
+ attr_reader :config, :id
12
+ attr_accessor :connect_timeout, :read_timeout, :write_timeout
13
+
14
+ def initialize(
15
+ config,
16
+ id: config.id,
17
+ connect_timeout: config.connect_timeout,
18
+ read_timeout: config.read_timeout,
19
+ write_timeout: config.write_timeout
20
+ )
21
+ @config = config
22
+ @id = id
23
+ @connect_timeout = connect_timeout
24
+ @read_timeout = read_timeout
25
+ @write_timeout = write_timeout
26
+ end
27
+
28
+ def timeout=(timeout)
29
+ @connect_timeout = @read_timeout = @write_timeout = timeout
30
+ end
31
+ end
32
+
9
33
  Error = Class.new(StandardError)
10
34
 
11
35
  ConnectionError = Class.new(Error)
@@ -52,23 +76,16 @@ class RedisClient
52
76
  super(config(**(arg || {}), **kwargs))
53
77
  end
54
78
  end
79
+
80
+ def register(middleware)
81
+ Middlewares.extend(middleware)
82
+ end
55
83
  end
56
84
 
57
- attr_reader :config, :id
58
- attr_accessor :connect_timeout, :read_timeout, :write_timeout
59
-
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
85
+ include Common
86
+
87
+ def initialize(config, **)
88
+ super
72
89
  @raw_connection = nil
73
90
  @disable_reconnection = false
74
91
  end
@@ -83,7 +100,18 @@ class RedisClient
83
100
  alias_method :then, :with
84
101
 
85
102
  def timeout=(timeout)
86
- @connect_timeout = @read_timeout = @write_timeout = timeout
103
+ super
104
+ raw_connection.read_timeout = raw_connection.write_timeout = timeout if connected?
105
+ end
106
+
107
+ def read_timeout=(timeout)
108
+ super
109
+ raw_connection.read_timeout = timeout if connected?
110
+ end
111
+
112
+ def write_timeout=(timeout)
113
+ super
114
+ raw_connection.write_timeout = timeout if connected?
87
115
  end
88
116
 
89
117
  def pubsub
@@ -94,43 +122,28 @@ class RedisClient
94
122
 
95
123
  def call(*command)
96
124
  command = RESP3.coerce_command!(command)
97
- result = ensure_connected do |connection|
98
- connection.write(command)
99
- connection.read
100
- end
101
-
102
- if result.is_a?(CommandError)
103
- raise result
104
- else
105
- result
125
+ ensure_connected do |connection|
126
+ Middlewares.call(command, config) do
127
+ connection.call(command, nil)
128
+ end
106
129
  end
107
130
  end
108
131
 
109
132
  def call_once(*command)
110
133
  command = RESP3.coerce_command!(command)
111
- result = ensure_connected(retryable: false) do |connection|
112
- connection.write(command)
113
- connection.read
114
- end
115
-
116
- if result.is_a?(CommandError)
117
- raise result
118
- else
119
- result
134
+ ensure_connected(retryable: false) do |connection|
135
+ Middlewares.call(command, config) do
136
+ connection.call(command, nil)
137
+ end
120
138
  end
121
139
  end
122
140
 
123
141
  def blocking_call(timeout, *command)
124
142
  command = RESP3.coerce_command!(command)
125
- result = ensure_connected do |connection|
126
- connection.write(command)
127
- connection.read(timeout)
128
- end
129
-
130
- if result.is_a?(CommandError)
131
- raise result
132
- else
133
- result
143
+ ensure_connected do |connection|
144
+ Middlewares.call(command, config) do
145
+ connection.call(command, timeout)
146
+ end
134
147
  end
135
148
  end
136
149
 
@@ -184,7 +197,10 @@ class RedisClient
184
197
  []
185
198
  else
186
199
  ensure_connected(retryable: pipeline._retryable?) do |connection|
187
- call_pipelined(connection, pipeline._commands, pipeline._timeouts)
200
+ commands = pipeline._commands
201
+ Middlewares.call_pipelined(commands, config) do
202
+ connection.call_pipelined(commands, pipeline._timeouts)
203
+ end
188
204
  end
189
205
  end
190
206
  end
@@ -197,7 +213,10 @@ class RedisClient
197
213
  call("WATCH", *watch)
198
214
  begin
199
215
  if transaction = build_transaction(&block)
200
- call_pipelined(connection, transaction._commands).last
216
+ commands = transaction._commands
217
+ Middlewares.call_pipelined(commands, config) do
218
+ connection.call_pipelined(commands, nil)
219
+ end.last
201
220
  else
202
221
  call("UNWATCH")
203
222
  []
@@ -213,7 +232,10 @@ class RedisClient
213
232
  []
214
233
  else
215
234
  ensure_connected(retryable: transaction._retryable?) do |connection|
216
- call_pipelined(connection, transaction._commands).last
235
+ commands = transaction._commands
236
+ Middlewares.call_pipelined(commands, config) do
237
+ connection.call_pipelined(commands, nil)
238
+ end.last
217
239
  end
218
240
  end
219
241
  end
@@ -347,29 +369,6 @@ class RedisClient
347
369
  nil
348
370
  end
349
371
 
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
372
  def ensure_connected(retryable: true)
374
373
  if @disable_reconnection
375
374
  yield @raw_connection
@@ -407,30 +406,33 @@ class RedisClient
407
406
  end
408
407
 
409
408
  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
409
+ @raw_connection ||= connect
410
+ end
423
411
 
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
412
+ def connect
413
+ connection = config.driver.new(
414
+ config,
415
+ connect_timeout: connect_timeout,
416
+ read_timeout: read_timeout,
417
+ write_timeout: write_timeout,
418
+ )
431
419
 
432
- connection
420
+ prelude = config.connection_prelude.dup
421
+
422
+ if id
423
+ prelude << ["CLIENT", "SETNAME", id.to_s]
433
424
  end
425
+
426
+ # The connection prelude is deliberately not sent to Middlewares
427
+ if config.sentinel?
428
+ prelude << ["ROLE"]
429
+ role, = connection.call_pipelined(prelude, nil).last
430
+ config.check_role!(role)
431
+ else
432
+ connection.call_pipelined(prelude, nil)
433
+ end
434
+
435
+ connection
434
436
  end
435
437
  end
436
438
 
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.2.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-04-28 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
@@ -91,6 +93,7 @@ files:
91
93
  - lib/redis_client/config.rb
92
94
  - lib/redis_client/connection.rb
93
95
  - lib/redis_client/hiredis_connection.rb
96
+ - lib/redis_client/middlewares.rb
94
97
  - lib/redis_client/pooled.rb
95
98
  - lib/redis_client/resp3.rb
96
99
  - lib/redis_client/sentinel_config.rb