redis-client 0.12.2 → 0.14.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: 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+