redis-client 0.27.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 286c927e4067dd45e1fcee65daab6e11af6d3b4d79604e1fc0b2a7a930f75201
4
- data.tar.gz: 899fab7bb4ce71f7bbf68ce00f9a008816690ff0ea8302b92edc2333a478d9e3
3
+ metadata.gz: 2fc78ecaeaed568c9917391585a7a185c90f8d7d569b8443c0fb2f0e918c2ca6
4
+ data.tar.gz: f9c7cbcb9c39267853681e926a71c27c3619bf0683fa5cd86885c585f2dfa239
5
5
  SHA512:
6
- metadata.gz: 4c900ff2f7bea9662273bcb0666c78a44adb6c7b34fa1d7e10d5d56cac6e2f868de8811d769f42ea52a8b70304cacf650dea7aadcc31b3ee88a4eb2dfc6f28bc
7
- data.tar.gz: fa6093d8d2101a120164f6d86857343d84cc69c077290ce6b41b80626918165045190ae3a004e8defc1f61c5fc72c623d0acf121a3b2efe32a4d14fb71787f74
6
+ metadata.gz: 3738437f5876f4f8c09f82a19e4e75462779dae006ed37ac06d3d0ca997bb0190fc676b705e5f6186a5854caff4d98dddc8b035c0fbaa854f9d77b179cb01c94
7
+ data.tar.gz: 4f9788a5547647cee34ddf01f60400e9645ca2c73f5329155b58670019733ec945331674f94b5a7720754ea2dd12b85d62040060166145beaea9b462514e6500
data/CHANGELOG.md CHANGED
@@ -1,8 +1,12 @@
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
+
3
7
  # 0.27.0
4
8
 
5
- - Added `idle_timeout` to revalidate connections that haven't been successfuly used in a long time. Defaults to 60 seconds.
9
+ - Added `idle_timeout` to revalidate connections that haven't been successfuly used in a long time. Defaults to 30 seconds.
6
10
  - Added `driver_info` configuration, to issue `CLIENT SETINFO` during connection prelude.
7
11
 
8
12
  # 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 `60` seconds.
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,31 @@ 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
+
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
+
147
172
  # Use 'mysecret' to authenticate against the mymaster instance, but skip authentication for the sentinels:
148
173
  SENTINELS = [{ host: '127.0.0.1', port: 26380 },
149
174
  { host: '127.0.0.1', port: 26381 }]
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class RedisClient
4
- VERSION = "0.27.0"
4
+ VERSION = "0.28.0"
5
5
  end
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
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.27.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