redis-client 0.12.2 → 0.14.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: f5e453ca140b38cff3f62e947e4168ca950117d1ff03562e5d6ed2bad0dbdd83
4
- data.tar.gz: 8fe7edeb11051105d2505f95e6cda7360c2cf6a438b93e5bb0f775538456f28d
3
+ metadata.gz: ac50722e5156b95e037fb5fd933cb804a3d52d0d767faa2bbddaccc7cbd38d07
4
+ data.tar.gz: 0a1645c3788dd195bf3567e14c0ba508b892357991359297c002c84bd2796e2e
5
5
  SHA512:
6
- metadata.gz: 27596015912265226cd39b7c6e27fff214e096166121aa0e8dba0a0f81a7ba45663c0d28239e3610d57c3955ace8cfc1f428c4af8f9373b80bb196eeb89c61a8
7
- data.tar.gz: 91b24e94e5332f4e74ca2dbb2e9adc60f7e8f9c5b698b1af5dfd5483bf836c95f6a79f39689272b742c6693ff8b6a6dd0d275e820f4a29f6d86b4dab650be6b4
6
+ metadata.gz: 0c93ddbae9ffdd770589eb6003eee594dc902a8d2698c99c38878b923ac80a91aa6a43892d015d9a4ba52ef19300c95970a8947e25f1844c4c01d4b88ff9d9ae
7
+ data.tar.gz: a06dd7eaa65ccf8544b4f04f7beffe2580d139783770170fcf76348c5760f78c5baa0d77e734a31111c720df6458bb8112a64960316618b831fdb6059b625963
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Unreleased
2
2
 
3
+ # 0.14.0
4
+
5
+ - hiredis binding now implement GC compaction and write barriers.
6
+ - hiredis binding now properly release the GVL around `connect(2)`.
7
+ - hiredis the client memory is now re-used on reconnection when possible to reduce allocation churn.
8
+
9
+ # 0.13.0
10
+
11
+ - Enable TCP keepalive on redis sockets. It sends a keep alive probe every 15 seconds for 2 minutes. #94.
12
+
3
13
  # 0.12.2
4
14
 
5
15
  - Cache calls to `Process.pid` on Ruby 3.1+. #91.
data/Gemfile.lock CHANGED
@@ -1,14 +1,14 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- redis-client (0.12.2)
4
+ redis-client (0.14.0)
5
5
  connection_pool
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
10
  ast (2.4.2)
11
- benchmark-ips (2.10.0)
11
+ benchmark-ips (2.11.0)
12
12
  byebug (11.1.3)
13
13
  connection_pool (2.3.0)
14
14
  hiredis (0.6.3)
data/Rakefile CHANGED
@@ -26,6 +26,7 @@ namespace :test do
26
26
  t.libs << "test"
27
27
  t.libs << "lib"
28
28
  t.test_files = FileList["test/**/*_test.rb"].exclude("test/sentinel/*_test.rb")
29
+ t.options = '-v' if ENV['CI'] || ENV['VERBOSE']
29
30
  end
30
31
 
31
32
  Rake::TestTask.new(:sentinel) do |t|
@@ -33,13 +34,16 @@ namespace :test do
33
34
  t.libs << "test"
34
35
  t.libs << "lib"
35
36
  t.test_files = FileList["test/sentinel/*_test.rb"]
37
+ t.options = '-v' if ENV['CI'] || ENV['VERBOSE']
36
38
  end
37
39
 
38
40
  Rake::TestTask.new(:hiredis) do |t|
39
41
  t.libs << "test/hiredis"
40
42
  t.libs << "test"
43
+ t.libs << "hiredis-client/lib"
41
44
  t.libs << "lib"
42
45
  t.test_files = FileList["test/**/*_test.rb"].exclude("test/sentinel/*_test.rb")
46
+ t.options = '-v' if ENV['CI'] || ENV['VERBOSE']
43
47
  end
44
48
  end
45
49
 
@@ -112,7 +112,9 @@ class RedisClient
112
112
  end
113
113
 
114
114
  def ssl_context
115
- @ssl_context ||= @driver.ssl_context(@ssl_params || {})
115
+ if ssl
116
+ @ssl_context ||= @driver.ssl_context(@ssl_params || {})
117
+ end
116
118
  end
117
119
 
118
120
  def server_url
@@ -6,9 +6,22 @@ class RedisClient
6
6
  @pending_reads = 0
7
7
  end
8
8
 
9
+ def reconnect
10
+ close
11
+ connect
12
+ end
13
+
14
+ def close
15
+ @pending_reads = 0
16
+ nil
17
+ end
18
+
9
19
  def revalidate
10
- if @pending_reads == 0 && connected?
11
- self
20
+ if @pending_reads > 0
21
+ close
22
+ false
23
+ else
24
+ connected?
12
25
  end
13
26
  end
14
27
 
@@ -42,43 +42,11 @@ class RedisClient
42
42
 
43
43
  def initialize(config, connect_timeout:, read_timeout:, write_timeout:)
44
44
  super()
45
- socket = if config.path
46
- UNIXSocket.new(config.path)
47
- else
48
- sock = if SUPPORTS_RESOLV_TIMEOUT
49
- Socket.tcp(config.host, config.port, connect_timeout: connect_timeout, resolv_timeout: connect_timeout)
50
- else
51
- Socket.tcp(config.host, config.port, connect_timeout: connect_timeout)
52
- end
53
- # disables Nagle's Algorithm, prevents multiple round trips with MULTI
54
- sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
55
- sock
56
- end
57
-
58
- if config.ssl
59
- socket = OpenSSL::SSL::SSLSocket.new(socket, config.ssl_context)
60
- socket.hostname = config.host
61
- loop do
62
- case status = socket.connect_nonblock(exception: false)
63
- when :wait_readable
64
- socket.to_io.wait_readable(connect_timeout) or raise CannotConnectError
65
- when :wait_writable
66
- socket.to_io.wait_writable(connect_timeout) or raise CannotConnectError
67
- when socket
68
- break
69
- else
70
- raise "Unexpected `connect_nonblock` return: #{status.inspect}"
71
- end
72
- end
73
- end
74
-
75
- @io = BufferedIO.new(
76
- socket,
77
- read_timeout: read_timeout,
78
- write_timeout: write_timeout,
79
- )
80
- rescue SystemCallError, OpenSSL::SSL::SSLError, SocketError => error
81
- raise CannotConnectError, error.message, error.backtrace
45
+ @config = config
46
+ @connect_timeout = connect_timeout
47
+ @read_timeout = read_timeout
48
+ @write_timeout = write_timeout
49
+ connect
82
50
  end
83
51
 
84
52
  def connected?
@@ -87,13 +55,16 @@ class RedisClient
87
55
 
88
56
  def close
89
57
  @io.close
58
+ super
90
59
  end
91
60
 
92
61
  def read_timeout=(timeout)
62
+ @read_timeout = timeout
93
63
  @io.read_timeout = timeout if @io
94
64
  end
95
65
 
96
66
  def write_timeout=(timeout)
67
+ @write_timeout = timeout
97
68
  @io.write_timeout = timeout if @io
98
69
  end
99
70
 
@@ -129,5 +100,78 @@ class RedisClient
129
100
  rescue SystemCallError, IOError, OpenSSL::SSL::SSLError => error
130
101
  raise ConnectionError, error.message
131
102
  end
103
+
104
+ private
105
+
106
+ def connect
107
+ socket = if @config.path
108
+ UNIXSocket.new(@config.path)
109
+ else
110
+ sock = if SUPPORTS_RESOLV_TIMEOUT
111
+ Socket.tcp(@config.host, @config.port, connect_timeout: @connect_timeout, resolv_timeout: @connect_timeout)
112
+ else
113
+ Socket.tcp(@config.host, @config.port, connect_timeout: @connect_timeout)
114
+ end
115
+ # disables Nagle's Algorithm, prevents multiple round trips with MULTI
116
+ sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
117
+ enable_socket_keep_alive(sock)
118
+ sock
119
+ end
120
+
121
+ if @config.ssl
122
+ socket = OpenSSL::SSL::SSLSocket.new(socket, @config.ssl_context)
123
+ socket.hostname = @config.host
124
+ loop do
125
+ case status = socket.connect_nonblock(exception: false)
126
+ when :wait_readable
127
+ socket.to_io.wait_readable(@connect_timeout) or raise CannotConnectError
128
+ when :wait_writable
129
+ socket.to_io.wait_writable(@connect_timeout) or raise CannotConnectError
130
+ when socket
131
+ break
132
+ else
133
+ raise "Unexpected `connect_nonblock` return: #{status.inspect}"
134
+ end
135
+ end
136
+ end
137
+
138
+ @io = BufferedIO.new(
139
+ socket,
140
+ read_timeout: @read_timeout,
141
+ write_timeout: @write_timeout,
142
+ )
143
+ true
144
+ rescue SystemCallError, OpenSSL::SSL::SSLError, SocketError => error
145
+ raise CannotConnectError, error.message, error.backtrace
146
+ end
147
+
148
+ KEEP_ALIVE_INTERVAL = 15 # Same as hiredis defaults
149
+ KEEP_ALIVE_TTL = 120 # Longer than hiredis defaults
150
+ KEEP_ALIVE_PROBES = (KEEP_ALIVE_TTL / KEEP_ALIVE_INTERVAL) - 1
151
+ private_constant :KEEP_ALIVE_INTERVAL
152
+ private_constant :KEEP_ALIVE_TTL
153
+ private_constant :KEEP_ALIVE_PROBES
154
+
155
+ if %i[SOL_SOCKET TCP_KEEPIDLE TCP_KEEPINTVL TCP_KEEPCNT].all? { |c| Socket.const_defined? c } # Linux
156
+ def enable_socket_keep_alive(socket)
157
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
158
+ socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, KEEP_ALIVE_INTERVAL)
159
+ socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, KEEP_ALIVE_INTERVAL)
160
+ socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, KEEP_ALIVE_PROBES)
161
+ end
162
+ elsif %i[IPPROTO_TCP TCP_KEEPINTVL TCP_KEEPCNT].all? { |c| Socket.const_defined? c } # macOS
163
+ def enable_socket_keep_alive(socket)
164
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
165
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_KEEPINTVL, KEEP_ALIVE_INTERVAL)
166
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_KEEPCNT, KEEP_ALIVE_PROBES)
167
+ end
168
+ elsif %i[SOL_SOCKET SO_KEEPALIVE].all? { |c| Socket.const_defined? c } # unknown POSIX
169
+ def enable_socket_keep_alive(socket)
170
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
171
+ end
172
+ else # unknown
173
+ def enable_socket_keep_alive(_socket)
174
+ end
175
+ end
132
176
  end
133
177
  end
@@ -13,9 +13,9 @@ class RedisClient
13
13
  end
14
14
 
15
15
  @to_list_of_hash = @to_hash = nil
16
- extra_config = {}
16
+ @extra_config = {}
17
17
  if client_config[:protocol] == 2
18
- extra_config[:protocol] = client_config[:protocol]
18
+ @extra_config[:protocol] = client_config[:protocol]
19
19
  @to_list_of_hash = lambda do |may_be_a_list|
20
20
  if may_be_a_list.is_a?(Array)
21
21
  may_be_a_list.map { |l| l.each_slice(2).to_h }
@@ -26,14 +26,7 @@ class RedisClient
26
26
  end
27
27
 
28
28
  @name = name
29
- @sentinel_configs = sentinels.map do |s|
30
- case s
31
- when String
32
- Config.new(**extra_config, url: s)
33
- else
34
- Config.new(**extra_config, **s)
35
- end
36
- end
29
+ @sentinel_configs = sentinels_to_configs(sentinels)
37
30
  @sentinels = {}.compare_by_identity
38
31
  @role = role
39
32
  @mutex = Mutex.new
@@ -93,6 +86,17 @@ class RedisClient
93
86
 
94
87
  private
95
88
 
89
+ def sentinels_to_configs(sentinels)
90
+ sentinels.map do |sentinel|
91
+ case sentinel
92
+ when String
93
+ Config.new(**@extra_config, url: sentinel)
94
+ else
95
+ Config.new(**@extra_config, **sentinel)
96
+ end
97
+ end
98
+ end
99
+
96
100
  def config
97
101
  @mutex.synchronize do
98
102
  @config ||= if @role == :master
@@ -106,9 +110,11 @@ class RedisClient
106
110
  def resolve_master
107
111
  each_sentinel do |sentinel_client|
108
112
  host, port = sentinel_client.call("SENTINEL", "get-master-addr-by-name", @name)
109
- if host && port
110
- return Config.new(host: host, port: Integer(port), **@client_config)
111
- end
113
+ next unless host && port
114
+
115
+ refresh_sentinels(sentinel_client)
116
+
117
+ return Config.new(host: host, port: Integer(port), **@client_config)
112
118
  end
113
119
  rescue ConnectionError
114
120
  raise ConnectionError, "No sentinels available"
@@ -159,5 +165,19 @@ class RedisClient
159
165
 
160
166
  raise last_error if last_error
161
167
  end
168
+
169
+ def refresh_sentinels(sentinel_client)
170
+ sentinel_response = sentinel_client.call("SENTINEL", "sentinels", @name, &@to_list_of_hash)
171
+ sentinels = sentinel_response.map do |sentinel|
172
+ { host: sentinel.fetch("ip"), port: Integer(sentinel.fetch("port")) }
173
+ end
174
+ new_sentinels = sentinels.select do |sentinel|
175
+ @sentinel_configs.none? do |sentinel_config|
176
+ sentinel_config.host == sentinel.fetch(:host) && sentinel_config.port == sentinel.fetch(:port)
177
+ end
178
+ end
179
+
180
+ @sentinel_configs.concat sentinels_to_configs(new_sentinels)
181
+ end
162
182
  end
163
183
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class RedisClient
4
- VERSION = "0.12.2"
4
+ VERSION = "0.14.0"
5
5
  end
data/lib/redis_client.rb CHANGED
@@ -346,7 +346,6 @@ class RedisClient
346
346
 
347
347
  def close
348
348
  @raw_connection&.close
349
- @raw_connection = nil
350
349
  self
351
350
  end
352
351
 
@@ -430,7 +429,7 @@ class RedisClient
430
429
 
431
430
  def close
432
431
  @raw_connection&.close
433
- @raw_connection = nil
432
+ @raw_connection = nil # PubSub can't just reconnect
434
433
  self
435
434
  end
436
435
 
@@ -654,20 +653,28 @@ class RedisClient
654
653
  end
655
654
 
656
655
  def raw_connection
657
- @raw_connection = @raw_connection&.revalidate
658
- @raw_connection ||= connect
656
+ if @raw_connection.nil? || !@raw_connection.revalidate
657
+ connect
658
+ end
659
+ @raw_connection
659
660
  end
660
661
 
661
662
  def connect
662
663
  @pid = PIDCache.pid
663
664
 
664
- connection = @middlewares.connect(config) do
665
- config.driver.new(
666
- config,
667
- connect_timeout: connect_timeout,
668
- read_timeout: read_timeout,
669
- write_timeout: write_timeout,
670
- )
665
+ if @raw_connection
666
+ @middlewares.connect(config) do
667
+ @raw_connection.reconnect
668
+ end
669
+ else
670
+ @raw_connection = @middlewares.connect(config) do
671
+ config.driver.new(
672
+ config,
673
+ connect_timeout: connect_timeout,
674
+ read_timeout: read_timeout,
675
+ write_timeout: write_timeout,
676
+ )
677
+ end
671
678
  end
672
679
 
673
680
  prelude = config.connection_prelude.dup
@@ -680,18 +687,16 @@ class RedisClient
680
687
  if config.sentinel?
681
688
  prelude << ["ROLE"]
682
689
  role, = @middlewares.call_pipelined(prelude, config) do
683
- connection.call_pipelined(prelude, nil).last
690
+ @raw_connection.call_pipelined(prelude, nil).last
684
691
  end
685
692
  config.check_role!(role)
686
693
  else
687
694
  unless prelude.empty?
688
695
  @middlewares.call_pipelined(prelude, config) do
689
- connection.call_pipelined(prelude, nil)
696
+ @raw_connection.call_pipelined(prelude, nil)
690
697
  end
691
698
  end
692
699
  end
693
-
694
- connection
695
700
  rescue FailoverError, CannotConnectError
696
701
  raise
697
702
  rescue ConnectionError => error
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.12.2
4
+ version: 0.14.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: 2023-02-16 00:00:00.000000000 Z
11
+ date: 2023-03-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: connection_pool
@@ -76,7 +76,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
76
76
  - !ruby/object:Gem::Version
77
77
  version: '0'
78
78
  requirements: []
79
- rubygems_version: 3.4.1
79
+ rubygems_version: 3.4.6
80
80
  signing_key:
81
81
  specification_version: 4
82
82
  summary: Simple low-level client for Redis 6+