redis-client 0.22.1 → 0.23.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: 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