redis-client 0.26.4 → 0.28.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 +26 -0
- data/lib/redis_client/config.rb +34 -2
- data/lib/redis_client/hash_ring.rb +87 -0
- data/lib/redis_client/version.rb +1 -1
- data/lib/redis_client.rb +41 -3
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2fc78ecaeaed568c9917391585a7a185c90f8d7d569b8443c0fb2f0e918c2ca6
|
|
4
|
+
data.tar.gz: f9c7cbcb9c39267853681e926a71c27c3619bf0683fa5cd86885c585f2dfa239
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3738437f5876f4f8c09f82a19e4e75462779dae006ed37ac06d3d0ca997bb0190fc676b705e5f6186a5854caff4d98dddc8b035c0fbaa854f9d77b179cb01c94
|
|
7
|
+
data.tar.gz: 4f9788a5547647cee34ddf01f60400e9645ca2c73f5329155b58670019733ec945331674f94b5a7720754ea2dd12b85d62040060166145beaea9b462514e6500
|
data/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
# Unreleased
|
|
2
2
|
|
|
3
|
+
# 0.28.0
|
|
4
|
+
|
|
5
|
+
- Added `RedisClient::HashRing` for horizontal sharing (compatible with `Redis::Distributed` from `redis-rb`).
|
|
6
|
+
|
|
7
|
+
# 0.27.0
|
|
8
|
+
|
|
9
|
+
- Added `idle_timeout` to revalidate connections that haven't been successfuly used in a long time. Defaults to 30 seconds.
|
|
10
|
+
- Added `driver_info` configuration, to issue `CLIENT SETINFO` during connection prelude.
|
|
11
|
+
|
|
3
12
|
# 0.26.4
|
|
4
13
|
|
|
5
|
-
- Further improve `rediss://` URLs used with Redis sentinel. Now avoid override explictly set `ssl:`
|
|
14
|
+
- Further improve `rediss://` URLs used with Redis sentinel. Now avoid override explictly set `ssl:` parameter.
|
|
6
15
|
- Fix compatibility with `redis-rb` in sentinel mode.
|
|
7
16
|
|
|
8
17
|
# 0.26.3
|
data/README.md
CHANGED
|
@@ -83,6 +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 `30` seconds.
|
|
86
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.
|
|
87
88
|
- `circuit_breaker`: A Hash with circuit breaker configuration. Defaults to `nil`. See the [circuit breaker section](#circuit-breaker) for details.
|
|
88
89
|
- `protocol:` The version of the RESP protocol to use. Default to `3`.
|
|
@@ -143,6 +144,31 @@ redis_config = RedisClient.sentinel(name: 'mymaster', sentinel_username: 'appuse
|
|
|
143
144
|
If you specify a username and/or password at the top level for your main Redis instance, Sentinel *will not* using thouse credentials
|
|
144
145
|
|
|
145
146
|
```ruby
|
|
147
|
+
|
|
148
|
+
### Consistent Hashing
|
|
149
|
+
|
|
150
|
+
To horizontally shard keys across multiple servers without relying on clustering, a `RedisClient::HashRing` class is provided:
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
ring = RedisClient.ring(
|
|
154
|
+
RedisClient.config(host: "10.0.1.1", port: 6380).new_pool(timeout: 0.5, size: 3),
|
|
155
|
+
RedisClient.config(host: "10.0.1.2", port: 6380).new_pool(timeout: 0.5, size: 3),
|
|
156
|
+
RedisClient.config(host: "10.0.1.3", port: 6380).new_pool(timeout: 0.5, size: 3),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
ring.node_for("cache_key").call("GET", "cache_key") # => "value"
|
|
160
|
+
|
|
161
|
+
ring.nodes_for("key1", "key2", "key3").each do |node, keys|
|
|
162
|
+
node.call("DEL", *keys)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
ring.nodes.each do |node|
|
|
166
|
+
node.close
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
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.
|
|
171
|
+
|
|
146
172
|
# Use 'mysecret' to authenticate against the mymaster instance, but skip authentication for the sentinels:
|
|
147
173
|
SENTINELS = [{ host: '127.0.0.1', port: 26380 },
|
|
148
174
|
{ host: '127.0.0.1', port: 26381 }]
|
data/lib/redis_client/config.rb
CHANGED
|
@@ -10,11 +10,12 @@ class RedisClient
|
|
|
10
10
|
DEFAULT_PORT = 6379
|
|
11
11
|
DEFAULT_USERNAME = "default"
|
|
12
12
|
DEFAULT_DB = 0
|
|
13
|
+
DEFAULT_IDLE_TIMEOUT = 30.0
|
|
13
14
|
|
|
14
15
|
module Common
|
|
15
16
|
attr_reader :db, :id, :ssl, :ssl_params, :command_builder, :inherit_socket,
|
|
16
17
|
:connect_timeout, :read_timeout, :write_timeout, :driver, :protocol,
|
|
17
|
-
:middlewares_stack, :custom, :circuit_breaker
|
|
18
|
+
:middlewares_stack, :custom, :circuit_breaker, :driver_info, :idle_timeout
|
|
18
19
|
|
|
19
20
|
alias_method :ssl?, :ssl
|
|
20
21
|
|
|
@@ -27,6 +28,7 @@ class RedisClient
|
|
|
27
28
|
read_timeout: timeout,
|
|
28
29
|
write_timeout: timeout,
|
|
29
30
|
connect_timeout: timeout,
|
|
31
|
+
idle_timeout: DEFAULT_IDLE_TIMEOUT,
|
|
30
32
|
ssl: nil,
|
|
31
33
|
custom: {},
|
|
32
34
|
ssl_params: nil,
|
|
@@ -37,7 +39,8 @@ class RedisClient
|
|
|
37
39
|
inherit_socket: false,
|
|
38
40
|
reconnect_attempts: false,
|
|
39
41
|
middlewares: false,
|
|
40
|
-
circuit_breaker: nil
|
|
42
|
+
circuit_breaker: nil,
|
|
43
|
+
driver_info: nil
|
|
41
44
|
)
|
|
42
45
|
@username = username
|
|
43
46
|
@password = password && !password.respond_to?(:call) ? ->(_) { password } : password
|
|
@@ -54,6 +57,7 @@ class RedisClient
|
|
|
54
57
|
@connect_timeout = connect_timeout
|
|
55
58
|
@read_timeout = read_timeout
|
|
56
59
|
@write_timeout = write_timeout
|
|
60
|
+
@idle_timeout = idle_timeout
|
|
57
61
|
|
|
58
62
|
@driver = driver ? RedisClient.driver(driver) : RedisClient.default_driver
|
|
59
63
|
|
|
@@ -84,6 +88,8 @@ class RedisClient
|
|
|
84
88
|
end
|
|
85
89
|
end
|
|
86
90
|
@middlewares_stack = middlewares_stack
|
|
91
|
+
|
|
92
|
+
@driver_info = driver_info
|
|
87
93
|
end
|
|
88
94
|
|
|
89
95
|
def connection_prelude
|
|
@@ -107,6 +113,13 @@ class RedisClient
|
|
|
107
113
|
prelude << ["SELECT", @db.to_s]
|
|
108
114
|
end
|
|
109
115
|
|
|
116
|
+
# Add CLIENT SETINFO commands for lib-name and lib-ver
|
|
117
|
+
# These commands are supported in Redis 7.2+
|
|
118
|
+
if @driver_info
|
|
119
|
+
prelude << ["CLIENT", "SETINFO", "LIB-NAME", build_lib_name]
|
|
120
|
+
prelude << ["CLIENT", "SETINFO", "LIB-VER", RedisClient::VERSION]
|
|
121
|
+
end
|
|
122
|
+
|
|
110
123
|
# Deep freeze all the strings and commands
|
|
111
124
|
prelude.map! do |commands|
|
|
112
125
|
commands = commands.map { |str| str.frozen? ? str : str.dup.freeze }
|
|
@@ -123,6 +136,25 @@ class RedisClient
|
|
|
123
136
|
@username || DEFAULT_USERNAME
|
|
124
137
|
end
|
|
125
138
|
|
|
139
|
+
# Build the library name for CLIENT SETINFO LIB-NAME.
|
|
140
|
+
#
|
|
141
|
+
# @param driver_info [String, Array<String>, nil] Upstream driver info
|
|
142
|
+
# @return [String] Library name with optional upstream driver info
|
|
143
|
+
# @raise [ArgumentError] if driver_info is not a String or Array
|
|
144
|
+
def build_lib_name
|
|
145
|
+
return "redis-client" if @driver_info.nil?
|
|
146
|
+
|
|
147
|
+
info = case @driver_info
|
|
148
|
+
when String then @driver_info
|
|
149
|
+
when Array then @driver_info.join(";")
|
|
150
|
+
else raise ArgumentError, "driver_info must be a String or Array of Strings"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
return "redis-client" if info.empty?
|
|
154
|
+
|
|
155
|
+
"redis-client(#{info})"
|
|
156
|
+
end
|
|
157
|
+
|
|
126
158
|
def resolved?
|
|
127
159
|
true
|
|
128
160
|
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
|
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
|
|
@@ -229,6 +239,12 @@ class RedisClient
|
|
|
229
239
|
SentinelConfig.new(client_implementation: self, **kwargs)
|
|
230
240
|
end
|
|
231
241
|
|
|
242
|
+
def ring(*clients, **options)
|
|
243
|
+
clients.flatten!
|
|
244
|
+
require "redis_client/hash_ring" unless defined?(HashRing)
|
|
245
|
+
HashRing.new(clients, **options)
|
|
246
|
+
end
|
|
247
|
+
|
|
232
248
|
def new(arg = nil, **kwargs)
|
|
233
249
|
if arg.is_a?(Config::Common)
|
|
234
250
|
super
|
|
@@ -269,6 +285,10 @@ class RedisClient
|
|
|
269
285
|
config.read_timeout
|
|
270
286
|
end
|
|
271
287
|
|
|
288
|
+
def idle_timeout
|
|
289
|
+
config.idle_timeout
|
|
290
|
+
end
|
|
291
|
+
|
|
272
292
|
def db
|
|
273
293
|
config.db
|
|
274
294
|
end
|
|
@@ -758,7 +778,9 @@ class RedisClient
|
|
|
758
778
|
@retry_attempt = config.retriable?(tries) ? tries : nil
|
|
759
779
|
connection = raw_connection
|
|
760
780
|
if block_given?
|
|
761
|
-
yield connection
|
|
781
|
+
result = yield connection
|
|
782
|
+
@last_used_at = RedisClient.now
|
|
783
|
+
result
|
|
762
784
|
else
|
|
763
785
|
connection
|
|
764
786
|
end
|
|
@@ -785,7 +807,9 @@ class RedisClient
|
|
|
785
807
|
begin
|
|
786
808
|
@disable_reconnection = true
|
|
787
809
|
@raw_connection.retry_attempt = nil
|
|
788
|
-
yield connection
|
|
810
|
+
result = yield connection
|
|
811
|
+
@last_used_at = RedisClient.now
|
|
812
|
+
result
|
|
789
813
|
rescue ConnectionError, ProtocolError
|
|
790
814
|
close
|
|
791
815
|
raise
|
|
@@ -799,6 +823,16 @@ class RedisClient
|
|
|
799
823
|
if @raw_connection.nil? || !@raw_connection.revalidate
|
|
800
824
|
connect
|
|
801
825
|
end
|
|
826
|
+
|
|
827
|
+
if config.idle_timeout && (@last_used_at + config.idle_timeout) < RedisClient.now
|
|
828
|
+
@middlewares.call(["PING"], config) do
|
|
829
|
+
@raw_connection.call(["PING"], nil)
|
|
830
|
+
rescue ConnectionError, ProtocolError
|
|
831
|
+
close
|
|
832
|
+
connect
|
|
833
|
+
end
|
|
834
|
+
end
|
|
835
|
+
|
|
802
836
|
@raw_connection.retry_attempt = @retry_attempt
|
|
803
837
|
@raw_connection
|
|
804
838
|
end
|
|
@@ -820,6 +854,7 @@ class RedisClient
|
|
|
820
854
|
)
|
|
821
855
|
end
|
|
822
856
|
end
|
|
857
|
+
@last_used_at = RedisClient.now
|
|
823
858
|
@raw_connection.retry_attempt = @retry_attempt
|
|
824
859
|
|
|
825
860
|
prelude = config.connection_prelude.dup
|
|
@@ -856,6 +891,9 @@ class RedisClient
|
|
|
856
891
|
if error.message.match?(/ERR unknown command ['`]HELLO['`]/)
|
|
857
892
|
raise UnsupportedServer,
|
|
858
893
|
"redis-client requires Redis 6+ with HELLO command available (#{config.server_url})"
|
|
894
|
+
# Ignore CLIENT SETINFO errors (Redis < 7.2 doesn't support it)
|
|
895
|
+
elsif error.message.match?(/unknown subcommand.*setinfo/i)
|
|
896
|
+
# Silently ignore - CLIENT SETINFO is not supported on Redis < 7.2
|
|
859
897
|
else
|
|
860
898
|
raise
|
|
861
899
|
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.28.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
|