redis-client 0.22.1 → 0.23.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: ac943e2497c5e52f402307d0acb4ba73cb25f6946efe66cb398a40a6b378b7e1
4
- data.tar.gz: '034386c130f23d555834fa788a1ae8409d90dbf35bd0312045e35c715c26899d'
3
+ metadata.gz: 588c7b4963a757c4d297236e15e0fd2b84d1137f42f14ca26dcfdf3349588126
4
+ data.tar.gz: cc68ebf122ca5c9044c9c0e08713071e60b151c526f37699d7fa5d28c2e52e5f
5
5
  SHA512:
6
- metadata.gz: b83ef90c1f9f5994c7fbcf3fb304f41d977fbb04cdb0c02e08bc6294b064e939fad19b225fb7ac7b5fd95103712c46d690cdeaa724a2e07e6d343d35a483a14c
7
- data.tar.gz: 0e3f65ac5ecceedc577d87dbf25604b331be6cfc4ac92c6f54633dc1acbc6ae4519ce32df3a4033a2594c5451194abbf0b80d9a6bcdb996169401af1805bb230
6
+ metadata.gz: 3a23804a8dbf13a35f8dae709586c44b3b81bd4d6d671ed79890b734b9e722f996c6d514bc609da511a0695deeefbfdfafcf8545fd6a0f7d8144d1936f72f417
7
+ data.tar.gz: 1be3aff9e63209a7424c0078f1fe26d8f45212034f0a8d18fa8cb778f7bdb12200effa4796082dd7a1096f18f840cdf84ddff8a9ab3ff2ad6a552db9db595484
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Unreleased
2
2
 
3
+ # 0.23.0
4
+
5
+ - Allow `password` to be a callable. Makes it easy to implement short lived password authentication strategies.
6
+ - Fix a thread safety issue in `hiredis-client` when using the `pubsub` client concurrently.
7
+
8
+ # 0.22.2
9
+
10
+ - Fix the sentinel client to properly extend timeout for blocking commands.
11
+ - Fix IPv6 support in `RedisClient::Config#server_url`.
12
+
3
13
  # 0.22.1
4
14
 
5
15
  - Fix `ProtocolError: Unknown sigil type` errors when using SSL connection. See #190.
data/README.md CHANGED
@@ -1,9 +1,10 @@
1
1
  # RedisClient
2
2
 
3
- `redis-client` is a simple, low-level, client for Redis 6+.
3
+ `redis-client` is a simple, low-level, client for [Redis](https://redis.io/) 6+, [Valkey](https://valkey.io/) 7+, [KeyDB](https://docs.keydb.dev/),
4
+ and several other databases that implement the same `RESP3` protocol.
4
5
 
5
6
  Contrary to the `redis` gem, `redis-client` doesn't try to map all Redis commands to Ruby constructs,
6
- it merely is a thin wrapper on top of the RESP3 protocol.
7
+ it merely is a thin wrapper on top of the `RESP3` protocol.
7
8
 
8
9
  ## Installation
9
10
 
@@ -77,7 +78,7 @@ redis.call("GET", "mykey")
77
78
  - `db`: The database to select after connecting, defaults to `0`.
78
79
  - `id` ID for the client connection, assigns name to current connection by sending `CLIENT SETNAME`.
79
80
  - `username` Username to authenticate against server, defaults to `"default"`.
80
- - `password` Password to authenticate against server.
81
+ - `password` Password to authenticate against server. Can either be a String or a callable that recieve `username` as argument and return a passowrd as a String.
81
82
  - `timeout`: The general timeout in seconds, default to `1.0`.
82
83
  - `connect_timeout`: The connection timeout, takes precedence over the general timeout when connecting to the server.
83
84
  - `read_timeout`: The read timeout, takes precedence over the general timeout when reading responses from the server.
@@ -89,7 +90,7 @@ redis.call("GET", "mykey")
89
90
 
90
91
  ### Sentinel support
91
92
 
92
- The client is able to perform automatic failover by using [Redis Sentinel](https://redis.io/docs/manual/sentinel/).
93
+ The client is able to perform automatic failover by using [Redis Sentinel](https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/).
93
94
 
94
95
  To connect using Sentinel, use:
95
96
 
@@ -149,7 +150,7 @@ SENTINELS = [{ host: '127.0.0.1', port: 26380 },
149
150
  redis_config = RedisClient.sentinel(name: 'mymaster', sentinels: SENTINELS, role: :master, password: 'mysecret')
150
151
  ```
151
152
 
152
- So you have to provide Sentinel credential and Redis explictly even they are the same
153
+ So you have to provide Sentinel credential and Redis explicitly even they are the same
153
154
 
154
155
  ```ruby
155
156
  # Use 'mysecret' to authenticate against the mymaster instance and sentinel
@@ -524,7 +525,7 @@ recover for a while.
524
525
 
525
526
  [Circuit breakers are a pattern that does exactly that](https://en.wikipedia.org/wiki/Circuit_breaker_design_pattern).
526
527
 
527
- Configuation options:
528
+ Configuration options:
528
529
 
529
530
  - `error_threshold`. The amount of errors to encounter within `error_threshold_timeout` amount of time before opening the circuit, that is to start rejecting requests instantly.
530
531
  - `error_threshold_timeout`. The amount of time in seconds that `error_threshold` errors must occur to open the circuit. Defaults to `error_timeout` seconds if not set.
@@ -63,7 +63,7 @@ class RedisClient
63
63
  private
64
64
 
65
65
  def refresh_state
66
- now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
66
+ now = RedisClient.now
67
67
  @lock.synchronize do
68
68
  if @errors.last < (now - @error_timeout)
69
69
  if @success_threshold > 0
@@ -78,7 +78,7 @@ class RedisClient
78
78
  end
79
79
 
80
80
  def record_error
81
- now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
81
+ now = RedisClient.now
82
82
  expiry = now - @error_timeout
83
83
  @lock.synchronize do
84
84
  if @state == :closed
@@ -12,8 +12,8 @@ class RedisClient
12
12
  DEFAULT_DB = 0
13
13
 
14
14
  module Common
15
- attr_reader :db, :password, :id, :ssl, :ssl_params, :command_builder, :inherit_socket,
16
- :connect_timeout, :read_timeout, :write_timeout, :driver, :connection_prelude, :protocol,
15
+ attr_reader :db, :id, :ssl, :ssl_params, :command_builder, :inherit_socket,
16
+ :connect_timeout, :read_timeout, :write_timeout, :driver, :protocol,
17
17
  :middlewares_stack, :custom, :circuit_breaker
18
18
 
19
19
  alias_method :ssl?, :ssl
@@ -70,7 +70,7 @@ class RedisClient
70
70
 
71
71
  reconnect_attempts = Array.new(reconnect_attempts, 0).freeze if reconnect_attempts.is_a?(Integer)
72
72
  @reconnect_attempts = reconnect_attempts
73
- @connection_prelude = build_connection_prelude
73
+ @connection_prelude = (build_connection_prelude unless @password.respond_to?(:call))
74
74
 
75
75
  circuit_breaker = CircuitBreaker.new(**circuit_breaker) if circuit_breaker.is_a?(Hash)
76
76
  if @circuit_breaker = circuit_breaker
@@ -87,6 +87,22 @@ class RedisClient
87
87
  @middlewares_stack = middlewares_stack
88
88
  end
89
89
 
90
+ def connection_prelude
91
+ if @password.respond_to?(:call)
92
+ build_connection_prelude
93
+ else
94
+ @connection_prelude
95
+ end
96
+ end
97
+
98
+ def password
99
+ if @password.respond_to?(:call)
100
+ @password.call(username)
101
+ else
102
+ @password
103
+ end
104
+ end
105
+
90
106
  def username
91
107
  @username || DEFAULT_USERNAME
92
108
  end
@@ -133,7 +149,13 @@ class RedisClient
133
149
  url = "#{url}?db=#{db}"
134
150
  end
135
151
  else
136
- url = "redis#{'s' if ssl?}://#{host}:#{port}"
152
+ # add brackets to IPv6 address
153
+ redis_host = if host.count(":") >= 2
154
+ "[#{host}]"
155
+ else
156
+ host
157
+ end
158
+ url = "redis#{'s' if ssl?}://#{redis_host}:#{port}"
137
159
  if db != 0
138
160
  url = "#{url}/#{db}"
139
161
  end
@@ -145,17 +167,18 @@ class RedisClient
145
167
 
146
168
  def build_connection_prelude
147
169
  prelude = []
170
+ pass = password
148
171
  if protocol == 3
149
- prelude << if @password
150
- ["HELLO", "3", "AUTH", @username || DEFAULT_USERNAME, @password]
172
+ prelude << if pass
173
+ ["HELLO", "3", "AUTH", username, pass]
151
174
  else
152
175
  ["HELLO", "3"]
153
176
  end
154
- elsif @password
177
+ elsif pass
155
178
  prelude << if @username && !@username.empty?
156
- ["AUTH", @username, @password]
179
+ ["AUTH", @username, pass]
157
180
  else
158
- ["AUTH", @password]
181
+ ["AUTH", pass]
159
182
  end
160
183
  end
161
184
 
@@ -28,7 +28,7 @@ class RedisClient
28
28
  def call(command, timeout)
29
29
  @pending_reads += 1
30
30
  write(command)
31
- result = read(timeout)
31
+ result = read(connection_timeout(timeout))
32
32
  @pending_reads -= 1
33
33
  if result.is_a?(Error)
34
34
  result._set_command(command)
@@ -49,7 +49,7 @@ class RedisClient
49
49
 
50
50
  size.times do |index|
51
51
  timeout = timeouts && timeouts[index]
52
- result = read(timeout)
52
+ result = read(connection_timeout(timeout))
53
53
  @pending_reads -= 1
54
54
 
55
55
  # A multi/exec command can return an array of results.
@@ -73,5 +73,14 @@ class RedisClient
73
73
  results
74
74
  end
75
75
  end
76
+
77
+ def connection_timeout(timeout)
78
+ return timeout unless timeout && timeout > 0
79
+
80
+ # Can't use the command timeout argument as the connection timeout
81
+ # otherwise it would be very racy. So we add the regular read_timeout on top
82
+ # to account for the network delay.
83
+ timeout + config.read_timeout
84
+ end
76
85
  end
77
86
  end
@@ -104,9 +104,9 @@ class RedisClient
104
104
  end
105
105
 
106
106
  def measure_round_trip_delay
107
- start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
107
+ start = RedisClient.now_ms
108
108
  call(["PING"], @read_timeout)
109
- Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start
109
+ RedisClient.now_ms - start
110
110
  end
111
111
 
112
112
  private
@@ -116,7 +116,12 @@ class RedisClient
116
116
  UNIXSocket.new(@config.path)
117
117
  else
118
118
  sock = if SUPPORTS_RESOLV_TIMEOUT
119
- Socket.tcp(@config.host, @config.port, connect_timeout: @connect_timeout, resolv_timeout: @connect_timeout)
119
+ begin
120
+ Socket.tcp(@config.host, @config.port, connect_timeout: @connect_timeout, resolv_timeout: @connect_timeout)
121
+ rescue Errno::ETIMEDOUT => timeout_error
122
+ timeout_error.message << ": #{@connect_timeout}s"
123
+ raise
124
+ end
120
125
  else
121
126
  Socket.tcp(@config.host, @config.port, connect_timeout: @connect_timeout)
122
127
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class RedisClient
4
- VERSION = "0.22.1"
4
+ VERSION = "0.23.0"
5
5
  end
data/lib/redis_client.rb CHANGED
@@ -45,6 +45,14 @@ class RedisClient
45
45
  def default_driver=(name)
46
46
  @default_driver = driver(name)
47
47
  end
48
+
49
+ def now
50
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
51
+ end
52
+
53
+ def now_ms
54
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
55
+ end
48
56
  end
49
57
 
50
58
  register_driver :ruby do
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.22.1
4
+ version: 0.23.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jean Boussier
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-04-16 00:00:00.000000000 Z
11
+ date: 2024-12-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: connection_pool
@@ -32,11 +32,8 @@ extensions: []
32
32
  extra_rdoc_files: []
33
33
  files:
34
34
  - CHANGELOG.md
35
- - Gemfile
36
- - Gemfile.lock
37
35
  - LICENSE.md
38
36
  - README.md
39
- - Rakefile
40
37
  - lib/redis-client.rb
41
38
  - lib/redis_client.rb
42
39
  - lib/redis_client/circuit_breaker.rb
@@ -53,7 +50,6 @@ files:
53
50
  - lib/redis_client/sentinel_config.rb
54
51
  - lib/redis_client/url_config.rb
55
52
  - lib/redis_client/version.rb
56
- - redis-client.gemspec
57
53
  homepage: https://github.com/redis-rb/redis-client
58
54
  licenses:
59
55
  - MIT
@@ -77,7 +73,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
77
73
  - !ruby/object:Gem::Version
78
74
  version: '0'
79
75
  requirements: []
80
- rubygems_version: 3.5.5
76
+ rubygems_version: 3.0.3.1
81
77
  signing_key:
82
78
  specification_version: 4
83
79
  summary: Simple low-level client for Redis 6+
data/Gemfile DELETED
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source "https://rubygems.org"
4
-
5
- # Specify your gem's dependencies in redis-client.gemspec
6
- gemspec name: "redis-client"
7
-
8
- gem "minitest"
9
- gem "rake", "~> 13.2"
10
- gem "rake-compiler"
11
- gem "rubocop"
12
- gem "rubocop-minitest"
13
- gem "toxiproxy"
14
-
15
- group :benchmark do
16
- gem "benchmark-ips"
17
- gem "hiredis"
18
- gem "redis", "~> 4.6"
19
- gem "stackprof", platform: :mri
20
- end
21
-
22
- gem "byebug", platform: :mri
data/Gemfile.lock DELETED
@@ -1,72 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- redis-client (0.22.1)
5
- connection_pool
6
-
7
- GEM
8
- remote: https://rubygems.org/
9
- specs:
10
- ast (2.4.2)
11
- benchmark-ips (2.13.0)
12
- byebug (11.1.3)
13
- connection_pool (2.4.1)
14
- hiredis (0.6.3)
15
- hiredis (0.6.3-java)
16
- json (2.7.1)
17
- json (2.7.1-java)
18
- minitest (5.22.3)
19
- parallel (1.24.0)
20
- parser (3.3.0.5)
21
- ast (~> 2.4.1)
22
- racc
23
- racc (1.7.3)
24
- racc (1.7.3-java)
25
- rainbow (3.1.1)
26
- rake (13.2.1)
27
- rake-compiler (1.2.7)
28
- rake
29
- redis (4.6.0)
30
- regexp_parser (2.9.0)
31
- rexml (3.2.6)
32
- rubocop (1.50.2)
33
- json (~> 2.3)
34
- parallel (~> 1.10)
35
- parser (>= 3.2.0.0)
36
- rainbow (>= 2.2.2, < 4.0)
37
- regexp_parser (>= 1.8, < 3.0)
38
- rexml (>= 3.2.5, < 4.0)
39
- rubocop-ast (>= 1.28.0, < 2.0)
40
- ruby-progressbar (~> 1.7)
41
- unicode-display_width (>= 2.4.0, < 3.0)
42
- rubocop-ast (1.30.0)
43
- parser (>= 3.2.1.0)
44
- rubocop-minitest (0.30.0)
45
- rubocop (>= 1.39, < 2.0)
46
- ruby-progressbar (1.13.0)
47
- stackprof (0.2.26)
48
- toxiproxy (2.0.2)
49
- unicode-display_width (2.5.0)
50
-
51
- PLATFORMS
52
- ruby
53
- universal-java-18
54
- x86_64-darwin-20
55
- x86_64-linux
56
-
57
- DEPENDENCIES
58
- benchmark-ips
59
- byebug
60
- hiredis
61
- minitest
62
- rake (~> 13.2)
63
- rake-compiler
64
- redis (~> 4.6)
65
- redis-client!
66
- rubocop
67
- rubocop-minitest
68
- stackprof
69
- toxiproxy
70
-
71
- BUNDLED WITH
72
- 2.3.13
data/Rakefile DELETED
@@ -1,126 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rake/extensiontask"
4
- require "rake/testtask"
5
- require 'rubocop/rake_task'
6
-
7
- RuboCop::RakeTask.new
8
-
9
- require "rake/clean"
10
- CLOBBER.include "pkg"
11
- require "bundler/gem_helper"
12
- Bundler::GemHelper.install_tasks(name: "redis-client")
13
- Bundler::GemHelper.install_tasks(dir: "hiredis-client", name: "hiredis-client")
14
-
15
- gemspec = Gem::Specification.load("redis-client.gemspec")
16
- Rake::ExtensionTask.new do |ext|
17
- ext.name = "hiredis_connection"
18
- ext.ext_dir = "hiredis-client/ext/redis_client/hiredis"
19
- ext.lib_dir = "hiredis-client/lib/redis_client"
20
- ext.gem_spec = gemspec
21
- CLEAN.add("#{ext.ext_dir}/vendor/*.{a,o}")
22
- end
23
-
24
- namespace :test do
25
- Rake::TestTask.new(:ruby) do |t|
26
- t.libs << "test"
27
- t.libs << "lib"
28
- t.test_files = FileList["test/**/*_test.rb"].exclude("test/sentinel/*_test.rb")
29
- t.options = '-v' if ENV['CI'] || ENV['VERBOSE']
30
- end
31
-
32
- Rake::TestTask.new(:sentinel) do |t|
33
- t.libs << "test/sentinel"
34
- t.libs << "test"
35
- t.libs << "lib"
36
- t.test_files = FileList["test/sentinel/*_test.rb"]
37
- t.options = '-v' if ENV['CI'] || ENV['VERBOSE']
38
- end
39
-
40
- Rake::TestTask.new(:hiredis) do |t|
41
- t.libs << "test/hiredis"
42
- t.libs << "test"
43
- t.libs << "hiredis-client/lib"
44
- t.libs << "lib"
45
- t.test_files = FileList["test/**/*_test.rb"].exclude("test/sentinel/*_test.rb")
46
- t.options = '-v' if ENV['CI'] || ENV['VERBOSE']
47
- end
48
- end
49
-
50
- hiredis_supported = RUBY_ENGINE == "ruby" && !RUBY_PLATFORM.match?(/mswin/)
51
- if hiredis_supported
52
- task test: %i[test:ruby test:hiredis test:sentinel]
53
- else
54
- task test: %i[test:ruby test:sentinel]
55
- end
56
-
57
- namespace :hiredis do
58
- task :download do
59
- version = "1.0.2"
60
- archive_path = "tmp/hiredis-#{version}.tar.gz"
61
- url = "https://github.com/redis/hiredis/archive/refs/tags/v#{version}.tar.gz"
62
- system("curl", "-L", url, out: archive_path) or raise "Downloading of #{url} failed"
63
- system("rm", "-rf", "hiredis-client/ext/redis_client/hiredis/vendor/")
64
- system("mkdir", "-p", "hiredis-client/ext/redis_client/hiredis/vendor/")
65
- system(
66
- "tar", "xvzf", archive_path,
67
- "-C", "hiredis-client/ext/redis_client/hiredis/vendor",
68
- "--strip-components", "1",
69
- )
70
- system("rm", "-rf", "hiredis-client/ext/redis_client/hiredis/vendor/examples")
71
- end
72
- end
73
-
74
- benchmark_suites = %w(single pipelined drivers)
75
- benchmark_modes = %i[ruby yjit hiredis]
76
- namespace :benchmark do
77
- benchmark_suites.each do |suite|
78
- benchmark_modes.each do |mode|
79
- next if suite == "drivers" && mode == :hiredis
80
-
81
- name = "#{suite}_#{mode}"
82
- desc name
83
- task name do
84
- output_path = "benchmark/#{name}.md"
85
- sh "rm", "-f", output_path
86
- File.open(output_path, "w+") do |output|
87
- output.puts("ruby: `#{RUBY_DESCRIPTION}`\n\n")
88
- output.puts("redis-server: `#{`redis-server -v`.strip}`\n\n")
89
- output.puts
90
- output.flush
91
- env = {}
92
- args = []
93
- args << "--yjit" if mode == :yjit
94
- env["DRIVER"] = mode == :hiredis ? "hiredis" : "ruby"
95
- system(env, RbConfig.ruby, *args, "benchmark/#{suite}.rb", out: output)
96
- end
97
-
98
- skipping = false
99
- output = File.readlines(output_path).reject do |line|
100
- if skipping
101
- if line == "Comparison:\n"
102
- skipping = false
103
- true
104
- else
105
- skipping
106
- end
107
- else
108
- skipping = true if line.start_with?("Warming up ---")
109
- skipping
110
- end
111
- end
112
- File.write(output_path, output.join)
113
- end
114
- end
115
- end
116
-
117
- task all: benchmark_suites.flat_map { |s| benchmark_modes.flat_map { |m| "#{s}_#{m}" } }
118
- end
119
-
120
- if hiredis_supported
121
- task default: %i[compile test rubocop]
122
- task ci: %i[compile test:ruby test:hiredis]
123
- else
124
- task default: %i[test rubocop]
125
- task ci: %i[test:ruby]
126
- end
data/redis-client.gemspec DELETED
@@ -1,32 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "lib/redis_client/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "redis-client"
7
- spec.version = RedisClient::VERSION
8
- spec.authors = ["Jean Boussier"]
9
- spec.email = ["jean.boussier@gmail.com"]
10
-
11
- spec.summary = "Simple low-level client for Redis 6+"
12
- spec.homepage = "https://github.com/redis-rb/redis-client"
13
- spec.license = "MIT"
14
- spec.required_ruby_version = ">= 2.6.0"
15
-
16
- spec.metadata["allowed_push_host"] = "https://rubygems.org"
17
-
18
- spec.metadata["homepage_uri"] = spec.homepage
19
- spec.metadata["source_code_uri"] = spec.homepage
20
- spec.metadata["changelog_uri"] = File.join(spec.homepage, "blob/master/CHANGELOG.md")
21
-
22
- # Specify which files should be added to the gem when it is released.
23
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
- spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
- `git ls-files -z`.split("\x0").reject do |f|
26
- (f == __FILE__) || f.match(%r{\A(?:(?:bin|hiredis-client|test|spec|features|benchmark)/|\.(?:git|rubocop))})
27
- end
28
- end
29
- spec.require_paths = ["lib"]
30
-
31
- spec.add_runtime_dependency "connection_pool"
32
- end