redis-client 0.1.0 → 0.2.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 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