redis-client 0.27.0 → 0.29.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 +4 -4
- data/CHANGELOG.md +10 -1
- data/README.md +31 -2
- data/lib/redis_client/connection_mixin.rb +6 -4
- data/lib/redis_client/hash_ring.rb +87 -0
- data/lib/redis_client/sentinel_config.rb +4 -0
- data/lib/redis_client/version.rb +1 -1
- data/lib/redis_client.rb +41 -14
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dc26284bc8ddd661e51d52ecf7afcf6d853b566128baa4176908eed4ed7e6ce2
|
|
4
|
+
data.tar.gz: 4051332c5b82220c88cb43efc0257e6dc61cba6b2c61ace4429d197591ecbd53
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7f200a254286ae8c63dd708f4cc1d945b33a8ec96e5466de8b6fe966ddb2ebdca727cb532d817a05021eabdba9809eb2450a0ad31dfc24e39ae2e4473ca9a36a
|
|
7
|
+
data.tar.gz: 192f624ea10bf819f8d4f98d980861b945cbe5fad8a079dbb0ed247d031b70646eb16cf9a34eb9f0b7b80abb7eee191a3b006bc46a7b0ac8d7b9a9924ee00d1a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
# Unreleased
|
|
2
2
|
|
|
3
|
+
# 0.29.0
|
|
4
|
+
|
|
5
|
+
- Fix connecting to Redis 7.1 and older with `driver_info:`set.
|
|
6
|
+
- Added `RedisClient::Error#next_error` for an easier way to check all errors produced by a pipeline.
|
|
7
|
+
|
|
8
|
+
# 0.28.0
|
|
9
|
+
|
|
10
|
+
- Added `RedisClient::HashRing` for horizontal sharing (compatible with `Redis::Distributed` from `redis-rb`).
|
|
11
|
+
|
|
3
12
|
# 0.27.0
|
|
4
13
|
|
|
5
|
-
- Added `idle_timeout` to revalidate connections that haven't been successfuly used in a long time. Defaults to
|
|
14
|
+
- Added `idle_timeout` to revalidate connections that haven't been successfuly used in a long time. Defaults to 30 seconds.
|
|
6
15
|
- Added `driver_info` configuration, to issue `CLIENT SETINFO` during connection prelude.
|
|
7
16
|
|
|
8
17
|
# 0.26.4
|
data/README.md
CHANGED
|
@@ -83,7 +83,7 @@ redis.call("GET", "mykey")
|
|
|
83
83
|
- `connect_timeout`: The connection timeout, takes precedence over the general timeout when connecting to the server.
|
|
84
84
|
- `read_timeout`: The read timeout, takes precedence over the general timeout when reading responses from the server.
|
|
85
85
|
- `write_timeout`: The write timeout, takes precedence over the general timeout when sending commands to the server.
|
|
86
|
-
- `idle_timeout`: Amount of time after which an idle connection has to be revalidated with a PING command. Defaults to `
|
|
86
|
+
- `idle_timeout`: Amount of time after which an idle connection has to be revalidated with a PING command. Defaults to `30` seconds.
|
|
87
87
|
- `reconnect_attempts`: Specify how many times the client should retry to send queries. Defaults to `0`. Makes sure to read the [reconnection section](#reconnection) before enabling it.
|
|
88
88
|
- `circuit_breaker`: A Hash with circuit breaker configuration. Defaults to `nil`. See the [circuit breaker section](#circuit-breaker) for details.
|
|
89
89
|
- `protocol:` The version of the RESP protocol to use. Default to `3`.
|
|
@@ -144,6 +144,7 @@ redis_config = RedisClient.sentinel(name: 'mymaster', sentinel_username: 'appuse
|
|
|
144
144
|
If you specify a username and/or password at the top level for your main Redis instance, Sentinel *will not* using thouse credentials
|
|
145
145
|
|
|
146
146
|
```ruby
|
|
147
|
+
|
|
147
148
|
# Use 'mysecret' to authenticate against the mymaster instance, but skip authentication for the sentinels:
|
|
148
149
|
SENTINELS = [{ host: '127.0.0.1', port: 26380 },
|
|
149
150
|
{ host: '127.0.0.1', port: 26381 }]
|
|
@@ -167,6 +168,30 @@ Also the `name`, `password`, `username` and `db` for Redis instance can be passe
|
|
|
167
168
|
redis_config = RedisClient.sentinel(url: "redis://appuser:mysecret@mymaster/10", sentinels: SENTINELS, role: :master)
|
|
168
169
|
```
|
|
169
170
|
|
|
171
|
+
### Consistent Hashing
|
|
172
|
+
|
|
173
|
+
To horizontally shard keys across multiple servers without relying on clustering, a `RedisClient::HashRing` class is provided:
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
ring = RedisClient.ring(
|
|
177
|
+
RedisClient.config(host: "10.0.1.1", port: 6380).new_pool(timeout: 0.5, size: 3),
|
|
178
|
+
RedisClient.config(host: "10.0.1.2", port: 6380).new_pool(timeout: 0.5, size: 3),
|
|
179
|
+
RedisClient.config(host: "10.0.1.3", port: 6380).new_pool(timeout: 0.5, size: 3),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
ring.node_for("cache_key").call("GET", "cache_key") # => "value"
|
|
183
|
+
|
|
184
|
+
ring.nodes_for("key1", "key2", "key3").each do |node, keys|
|
|
185
|
+
node.call("DEL", *keys)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
ring.nodes.each do |node|
|
|
189
|
+
node.close
|
|
190
|
+
end
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Note that regular clients do respond to `node_for`, `nodes_for` and `nodes`, so that code that support `RedisClient.ring` is also usable with a single server.
|
|
194
|
+
|
|
170
195
|
### Type support
|
|
171
196
|
|
|
172
197
|
Only a select few Ruby types are supported as arguments beside strings.
|
|
@@ -336,7 +361,11 @@ end
|
|
|
336
361
|
|
|
337
362
|
#### Exception management
|
|
338
363
|
|
|
339
|
-
|
|
364
|
+
By default, when a pipeline produce multiple command errors, only the first encountered error is raised.
|
|
365
|
+
|
|
366
|
+
However, the raised exception have a `.next_error` method you can use like a linked list to check all encountered errors.
|
|
367
|
+
|
|
368
|
+
Alternatively, the `exception` flag in the `#pipelined` method of `RedisClient` is a feature that modifies the pipeline execution
|
|
340
369
|
behavior. When set to `false`, it doesn't raise an exception when a command error occurs. Instead, it allows the
|
|
341
370
|
pipeline to execute all commands, and any failed command will be available in the returned array. (Defaults to `true`)
|
|
342
371
|
|
|
@@ -47,7 +47,7 @@ class RedisClient
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def call_pipelined(commands, timeouts, exception: true)
|
|
50
|
-
|
|
50
|
+
first_error = last_error = nil
|
|
51
51
|
|
|
52
52
|
size = commands.size
|
|
53
53
|
results = Array.new(commands.size)
|
|
@@ -69,14 +69,16 @@ class RedisClient
|
|
|
69
69
|
result._set_command(commands[index])
|
|
70
70
|
result._set_config(config)
|
|
71
71
|
result._set_retry_attempt(@retry_attempt)
|
|
72
|
-
|
|
72
|
+
|
|
73
|
+
last_error&._set_next_error(result)
|
|
74
|
+
first_error ||= last_error = result
|
|
73
75
|
end
|
|
74
76
|
|
|
75
77
|
results[index] = result
|
|
76
78
|
end
|
|
77
79
|
|
|
78
|
-
if
|
|
79
|
-
raise
|
|
80
|
+
if first_error && exception
|
|
81
|
+
raise first_error
|
|
80
82
|
else
|
|
81
83
|
results
|
|
82
84
|
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'zlib'
|
|
4
|
+
|
|
5
|
+
class RedisClient
|
|
6
|
+
class HashRing
|
|
7
|
+
POINTS_PER_SERVER = 160
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
attr_writer :digest
|
|
11
|
+
|
|
12
|
+
def digest
|
|
13
|
+
@digest ||= begin
|
|
14
|
+
require 'digest/md5'
|
|
15
|
+
Digest::MD5
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
attr_reader :nodes
|
|
21
|
+
|
|
22
|
+
def initialize(nodes = [], replicas: POINTS_PER_SERVER, digest: self.class.digest)
|
|
23
|
+
@replicas = replicas
|
|
24
|
+
@ring = {}
|
|
25
|
+
@digest = digest
|
|
26
|
+
ids = {}
|
|
27
|
+
@nodes = nodes.dup.freeze
|
|
28
|
+
nodes.each do |node|
|
|
29
|
+
id = node.id || node.config.server_url
|
|
30
|
+
if ids[id]
|
|
31
|
+
raise ArgumentError, "duplicate node id: #{id.inspect}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
ids[id] = true
|
|
35
|
+
|
|
36
|
+
replicas.times do |i|
|
|
37
|
+
@ring[server_hash_for("#{id}:#{i}".freeze)] = node
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
@sorted_keys = @ring.keys
|
|
41
|
+
@sorted_keys.sort!
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# get the node in the hash ring for this key
|
|
45
|
+
def node_for(key)
|
|
46
|
+
hash = hash_for(key)
|
|
47
|
+
idx = binary_search(@sorted_keys, hash)
|
|
48
|
+
@ring[@sorted_keys[idx]]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def nodes_for(*keys)
|
|
52
|
+
keys.flatten!
|
|
53
|
+
mapping = {}
|
|
54
|
+
keys.each do |key|
|
|
55
|
+
(mapping[node_for(key)] ||= []) << key
|
|
56
|
+
end
|
|
57
|
+
mapping
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def hash_for(key)
|
|
63
|
+
Zlib.crc32(key)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def server_hash_for(key)
|
|
67
|
+
@digest.digest(key).unpack1("L>")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Find the closest index in HashRing with value <= the given value
|
|
71
|
+
def binary_search(ary, value)
|
|
72
|
+
upper = ary.size
|
|
73
|
+
lower = 0
|
|
74
|
+
|
|
75
|
+
while lower < upper
|
|
76
|
+
mid = (lower + upper) / 2
|
|
77
|
+
if ary[mid] > value
|
|
78
|
+
upper = mid
|
|
79
|
+
else
|
|
80
|
+
lower = mid + 1
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
upper - 1
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -116,6 +116,10 @@ class RedisClient
|
|
|
116
116
|
end
|
|
117
117
|
|
|
118
118
|
def check_role!(role)
|
|
119
|
+
unless role.is_a?(String)
|
|
120
|
+
raise TypeError, "Expected role to be a string, got: #{role.inspect}"
|
|
121
|
+
end
|
|
122
|
+
|
|
119
123
|
if @role == :master
|
|
120
124
|
unless role == "master"
|
|
121
125
|
sleep SENTINEL_DELAY
|
data/lib/redis_client/version.rb
CHANGED
data/lib/redis_client.rb
CHANGED
|
@@ -61,7 +61,7 @@ class RedisClient
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
module Common
|
|
64
|
-
attr_reader :config, :id
|
|
64
|
+
attr_reader :config, :id, :nodes
|
|
65
65
|
attr_accessor :connect_timeout, :read_timeout, :write_timeout
|
|
66
66
|
|
|
67
67
|
def initialize(
|
|
@@ -78,11 +78,21 @@ class RedisClient
|
|
|
78
78
|
@write_timeout = write_timeout
|
|
79
79
|
@command_builder = config.command_builder
|
|
80
80
|
@pid = PIDCache.pid
|
|
81
|
+
@nodes = [self].freeze
|
|
81
82
|
end
|
|
82
83
|
|
|
83
84
|
def timeout=(timeout)
|
|
84
85
|
@connect_timeout = @read_timeout = @write_timeout = timeout
|
|
85
86
|
end
|
|
87
|
+
|
|
88
|
+
def node_for(_key)
|
|
89
|
+
self
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def nodes_for(*keys)
|
|
93
|
+
keys.flatten!
|
|
94
|
+
{ self => keys }
|
|
95
|
+
end
|
|
86
96
|
end
|
|
87
97
|
|
|
88
98
|
module HasConfig
|
|
@@ -138,11 +148,17 @@ class RedisClient
|
|
|
138
148
|
include HasConfig
|
|
139
149
|
include Retriable
|
|
140
150
|
|
|
151
|
+
attr_reader :next_error
|
|
152
|
+
|
|
141
153
|
def self.with_config(message, config = nil)
|
|
142
154
|
error = new(message)
|
|
143
155
|
error._set_config(config)
|
|
144
156
|
error
|
|
145
157
|
end
|
|
158
|
+
|
|
159
|
+
def _set_next_error(error) # :nodoc:
|
|
160
|
+
@next_error = error
|
|
161
|
+
end
|
|
146
162
|
end
|
|
147
163
|
|
|
148
164
|
ProtocolError = Class.new(Error)
|
|
@@ -229,6 +245,12 @@ class RedisClient
|
|
|
229
245
|
SentinelConfig.new(client_implementation: self, **kwargs)
|
|
230
246
|
end
|
|
231
247
|
|
|
248
|
+
def ring(*clients, **options)
|
|
249
|
+
clients.flatten!
|
|
250
|
+
require "redis_client/hash_ring" unless defined?(HashRing)
|
|
251
|
+
HashRing.new(clients, **options)
|
|
252
|
+
end
|
|
253
|
+
|
|
232
254
|
def new(arg = nil, **kwargs)
|
|
233
255
|
if arg.is_a?(Config::Common)
|
|
234
256
|
super
|
|
@@ -847,19 +869,26 @@ class RedisClient
|
|
|
847
869
|
prelude << ["CLIENT", "SETNAME", id]
|
|
848
870
|
end
|
|
849
871
|
|
|
850
|
-
# The connection prelude is deliberately not sent to Middlewares
|
|
851
872
|
if config.sentinel?
|
|
852
873
|
prelude << ["ROLE"]
|
|
853
|
-
|
|
854
|
-
|
|
874
|
+
end
|
|
875
|
+
|
|
876
|
+
unless prelude.empty?
|
|
877
|
+
results = @middlewares.call_pipelined(prelude, config) do
|
|
878
|
+
@raw_connection.call_pipelined(prelude, nil, exception: false)
|
|
855
879
|
end
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
880
|
+
|
|
881
|
+
results.each do |result|
|
|
882
|
+
# CLIENT SETINFO is unsupported on Redis < 7.2. The pipeline drained all responses before raising,
|
|
883
|
+
# so if that's the only error, the socket is healthy: keep it.
|
|
884
|
+
if result.is_a?(CommandError) && !(result.command[0] == "CLIENT" && result.command[1] == "SETINFO")
|
|
885
|
+
raise result
|
|
861
886
|
end
|
|
862
887
|
end
|
|
888
|
+
|
|
889
|
+
if config.sentinel?
|
|
890
|
+
config.check_role!(results.last.first)
|
|
891
|
+
end
|
|
863
892
|
end
|
|
864
893
|
rescue FailoverError, CannotConnectError => error
|
|
865
894
|
@raw_connection&.close
|
|
@@ -872,12 +901,10 @@ class RedisClient
|
|
|
872
901
|
raise connect_error
|
|
873
902
|
rescue CommandError => error
|
|
874
903
|
@raw_connection&.close
|
|
875
|
-
|
|
904
|
+
|
|
905
|
+
if error.command&.first == "HELLO" && error.message.match?(/ERR unknown command/)
|
|
876
906
|
raise UnsupportedServer,
|
|
877
|
-
"redis-client requires Redis 6+ with HELLO command available (#{config.server_url})"
|
|
878
|
-
# Ignore CLIENT SETINFO errors (Redis < 7.2 doesn't support it)
|
|
879
|
-
elsif error.message.match?(/unknown subcommand.*setinfo/i)
|
|
880
|
-
# Silently ignore - CLIENT SETINFO is not supported on Redis < 7.2
|
|
907
|
+
"redis-client requires Redis 6+ with HELLO command available (#{config.server_url})", cause: error
|
|
881
908
|
else
|
|
882
909
|
raise
|
|
883
910
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: redis-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.29.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jean Boussier
|
|
@@ -39,6 +39,7 @@ files:
|
|
|
39
39
|
- lib/redis_client/config.rb
|
|
40
40
|
- lib/redis_client/connection_mixin.rb
|
|
41
41
|
- lib/redis_client/decorator.rb
|
|
42
|
+
- lib/redis_client/hash_ring.rb
|
|
42
43
|
- lib/redis_client/middlewares.rb
|
|
43
44
|
- lib/redis_client/pid_cache.rb
|
|
44
45
|
- lib/redis_client/pooled.rb
|
|
@@ -70,7 +71,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
70
71
|
- !ruby/object:Gem::Version
|
|
71
72
|
version: '0'
|
|
72
73
|
requirements: []
|
|
73
|
-
rubygems_version: 4.0.
|
|
74
|
+
rubygems_version: 4.0.6
|
|
74
75
|
specification_version: 4
|
|
75
76
|
summary: Simple low-level client for Redis 6+
|
|
76
77
|
test_files: []
|