redis-cluster 0.0.7
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 +7 -0
- data/README.md +36 -0
- data/lib/redis-cluster.rb +208 -0
- data/lib/redis_cluster/client.rb +54 -0
- data/lib/redis_cluster/cluster.rb +179 -0
- data/lib/redis_cluster/function.rb +23 -0
- data/lib/redis_cluster/function/hash.rb +150 -0
- data/lib/redis_cluster/function/key.rb +107 -0
- data/lib/redis_cluster/function/list.rb +139 -0
- data/lib/redis_cluster/function/scan.rb +90 -0
- data/lib/redis_cluster/function/set.rb +83 -0
- data/lib/redis_cluster/function/sorted_set.rb +375 -0
- data/lib/redis_cluster/function/string.rb +234 -0
- data/lib/redis_cluster/future.rb +28 -0
- data/lib/redis_cluster/version.rb +5 -0
- metadata +72 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 703b89b40d08e4c0df9bf8eae2ac03add59c8ef6
|
4
|
+
data.tar.gz: b592ec99efaf8aea624538b421c0481d2dc1b42d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5ef6234f0223f21cc93baa713c732afa01f638b1d7daf7d99d5c7e63d06d04449b837d31b781b7861cfba1d9f0d12fb972409eea9be71620f5c2a45f9977e2e7
|
7
|
+
data.tar.gz: ca36b2b72dd9e41e7a39c6caf5b5b5b1bba62c4048a7ca998702c688924aff9320ffc97bf2f37f1274e25b8e04e2489f469cb242c60fab4a8669a63764c52afd
|
data/README.md
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# redis-cluster
|
2
|
+
[](https://circleci.com/gh/bukalapak/redis-cluster)
|
3
|
+
[](https://codecov.io/gh/bukalapak/redis-cluster)
|
4
|
+
|
5
|
+
## Getting started
|
6
|
+
|
7
|
+
Install redis-cluster.
|
8
|
+
````ruby
|
9
|
+
gem install 'redis-cluster'
|
10
|
+
````
|
11
|
+
|
12
|
+
This will start a redis from seed servers. Currently it only support master read configuration.
|
13
|
+
````ruby
|
14
|
+
seed = ['127.0.0.1:7001', '127.0.0.1:7002']
|
15
|
+
redis = RedisCluster.new(seed, redis_opts: { timeout: 5, connect_timeout: 1 }, cluster_opts: { read_mode: :master_slave, silent: true, logger: Logger.new })
|
16
|
+
````
|
17
|
+
|
18
|
+
## Configuration
|
19
|
+
|
20
|
+
### redis_opts
|
21
|
+
Option for Redis::Client instance. Set timeout, ssl, etc here.
|
22
|
+
|
23
|
+
### cluster_opts
|
24
|
+
Option for RedisCluster.
|
25
|
+
- `read_mode`: for read command, RedisClient can try to read from slave if specified. Supported option is `:master`(default), `:slave`, and `:master_slave`.
|
26
|
+
- `silent`: whether or not RedisCluster will raise error.
|
27
|
+
- `logger`: if specified. RedisCluster will log all of RedisCluster errors here.
|
28
|
+
|
29
|
+
## Limitation
|
30
|
+
All multi keys operation, cluster command, multi-exec, and some commands are not supported.
|
31
|
+
|
32
|
+
## Pipeline
|
33
|
+
Can be used with same interface as standalone redis client. See [redis pipeline](https://github.com/redis/redis-rb#pipelining)
|
34
|
+
|
35
|
+
## Contributing
|
36
|
+
[Fork the project](https://github.com/bukalapak/redis-cluster) and send pull requests.
|
@@ -0,0 +1,208 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'redis'
|
3
|
+
|
4
|
+
require_relative 'redis_cluster/cluster'
|
5
|
+
require_relative 'redis_cluster/client'
|
6
|
+
require_relative 'redis_cluster/future'
|
7
|
+
require_relative 'redis_cluster/function'
|
8
|
+
|
9
|
+
# RedisCluster is a client for redis-cluster *huh*
|
10
|
+
class RedisCluster
|
11
|
+
include MonitorMixin
|
12
|
+
include Function
|
13
|
+
|
14
|
+
attr_reader :cluster, :options
|
15
|
+
|
16
|
+
def initialize(seeds, redis_opts: nil, cluster_opts: nil)
|
17
|
+
@options = cluster_opts || {}
|
18
|
+
@cluster = Cluster.new(seeds, redis_opts || {})
|
19
|
+
|
20
|
+
super()
|
21
|
+
end
|
22
|
+
|
23
|
+
def logger
|
24
|
+
options[:logger]
|
25
|
+
end
|
26
|
+
|
27
|
+
def silent?
|
28
|
+
options[:silent]
|
29
|
+
end
|
30
|
+
|
31
|
+
def read_mode
|
32
|
+
options[:read_mode] || :master
|
33
|
+
end
|
34
|
+
|
35
|
+
def connected?
|
36
|
+
cluster.connected?
|
37
|
+
end
|
38
|
+
|
39
|
+
def close
|
40
|
+
safely{ cluster.close }
|
41
|
+
end
|
42
|
+
|
43
|
+
def pipeline?
|
44
|
+
!@pipeline.nil?
|
45
|
+
end
|
46
|
+
|
47
|
+
def call(keys, command, opts = {})
|
48
|
+
opts[:transform] ||= NOOP
|
49
|
+
slot = slot_for([ keys ].flatten )
|
50
|
+
|
51
|
+
safely do
|
52
|
+
if pipeline?
|
53
|
+
call_pipeline(slot, command, opts)
|
54
|
+
else
|
55
|
+
call_immediately(slot, command, opts)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def pipelined
|
61
|
+
return yield if pipeline?
|
62
|
+
|
63
|
+
safely do
|
64
|
+
begin
|
65
|
+
@pipeline = []
|
66
|
+
yield
|
67
|
+
|
68
|
+
try = 3
|
69
|
+
while !@pipeline.empty? && try.positive?
|
70
|
+
try -= 1
|
71
|
+
moved = false
|
72
|
+
mapped_future = map_pipeline(@pipeline)
|
73
|
+
|
74
|
+
@pipeline = []
|
75
|
+
mapped_future.each do |url, futures|
|
76
|
+
leftover, move = do_pipelined(url, futures)
|
77
|
+
moved ||= move
|
78
|
+
|
79
|
+
@pipeline.concat(leftover)
|
80
|
+
end
|
81
|
+
|
82
|
+
cluster.reset if moved
|
83
|
+
end
|
84
|
+
|
85
|
+
@pipeline.first.value unless @pipeline.empty?
|
86
|
+
ensure
|
87
|
+
@pipeline = nil
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
NOOP = ->(v){ v }
|
95
|
+
CROSSSLOT_ERROR = Redis::CommandError.new("CROSSSLOT Keys in request don't hash to the same slot")
|
96
|
+
|
97
|
+
def safely
|
98
|
+
synchronize{ yield } if block_given?
|
99
|
+
rescue StandardError => e
|
100
|
+
logger&.error(e)
|
101
|
+
raise e unless silent?
|
102
|
+
end
|
103
|
+
|
104
|
+
def slot_for(keys)
|
105
|
+
slot = keys.map{ |k| cluster.slot_for(k) }.uniq
|
106
|
+
slot.size == 1 ? slot.first : ( raise CROSSSLOT_ERROR )
|
107
|
+
end
|
108
|
+
|
109
|
+
def call_immediately(slot, command, transform:, read: false)
|
110
|
+
try = 3
|
111
|
+
asking = false
|
112
|
+
reply = nil
|
113
|
+
mode = read ? read_mode : :master
|
114
|
+
client = cluster.public_send(mode, slot)
|
115
|
+
|
116
|
+
while try.positive?
|
117
|
+
begin
|
118
|
+
try -= 1
|
119
|
+
|
120
|
+
client.push([:asking]) if asking
|
121
|
+
reply = client.call(command)
|
122
|
+
|
123
|
+
err, url = scan_reply(reply)
|
124
|
+
return transform.call(reply) unless err
|
125
|
+
|
126
|
+
cluster.reset if err == :moved
|
127
|
+
asking = err == :ask
|
128
|
+
client = cluster[url]
|
129
|
+
rescue Redis::CannotConnectError => e
|
130
|
+
asking = false
|
131
|
+
cluster.reset
|
132
|
+
client = cluster.public_send(mode, slot)
|
133
|
+
reply = e
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
raise reply
|
138
|
+
end
|
139
|
+
|
140
|
+
def call_pipeline(slot, command, opts)
|
141
|
+
Future.new(slot, command, opts[:transform]).tap do |future|
|
142
|
+
@pipeline << future
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def map_pipeline(pipe)
|
147
|
+
futures = ::Hash.new{ |h, k| h[k] = [] }
|
148
|
+
pipe.each do |future|
|
149
|
+
url = future.url || cluster.master(future.slot).url
|
150
|
+
futures[url] << future
|
151
|
+
end
|
152
|
+
|
153
|
+
return futures
|
154
|
+
end
|
155
|
+
|
156
|
+
def do_pipelined(url, futures)
|
157
|
+
moved = false
|
158
|
+
leftover = []
|
159
|
+
|
160
|
+
rev_index = {}
|
161
|
+
idx = 0
|
162
|
+
client = cluster[url]
|
163
|
+
|
164
|
+
futures.each_with_index do |future, i|
|
165
|
+
if future.asking
|
166
|
+
client.push([:asking])
|
167
|
+
idx += 1
|
168
|
+
end
|
169
|
+
|
170
|
+
rev_index[idx] = i
|
171
|
+
client.push(future.command)
|
172
|
+
idx += 1
|
173
|
+
end
|
174
|
+
|
175
|
+
client.commit.each_with_index do |reply, i|
|
176
|
+
next unless rev_index[i]
|
177
|
+
|
178
|
+
future = futures[rev_index[i]]
|
179
|
+
future.value = reply
|
180
|
+
|
181
|
+
err, url = scan_reply(reply)
|
182
|
+
next unless err
|
183
|
+
|
184
|
+
moved ||= err == :moved
|
185
|
+
future.asking = err == :ask
|
186
|
+
future.url = url
|
187
|
+
leftover << future
|
188
|
+
end
|
189
|
+
|
190
|
+
return [leftover, moved]
|
191
|
+
rescue Redis::CannotConnectError
|
192
|
+
# reset url and asking when connection refused
|
193
|
+
futures.each{ |f| f.url = nil; f.asking = false }
|
194
|
+
|
195
|
+
return [futures, true]
|
196
|
+
end
|
197
|
+
|
198
|
+
def scan_reply(reply)
|
199
|
+
if reply.is_a?(Redis::CommandError)
|
200
|
+
err, _slot, url = reply.to_s.split
|
201
|
+
raise reply if err != 'MOVED' && err != 'ASK'
|
202
|
+
|
203
|
+
[err.downcase.to_sym, url]
|
204
|
+
elsif reply.is_a?(::RuntimeError)
|
205
|
+
raise reply
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'redis'
|
3
|
+
|
4
|
+
require_relative 'version'
|
5
|
+
|
6
|
+
class RedisCluster
|
7
|
+
|
8
|
+
# Client is a decorator object for Redis::Client. It add queue to support pipelining and another
|
9
|
+
# useful addition
|
10
|
+
class Client
|
11
|
+
attr_reader :client, :queue, :url
|
12
|
+
|
13
|
+
def initialize(opts)
|
14
|
+
@client = Redis::Client.new(opts)
|
15
|
+
@queue = []
|
16
|
+
@url = "#{client.host}:#{client.port}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def inspect
|
20
|
+
"#<RedisCluster client v#{RedisCluster::VERSION} for #{url}>"
|
21
|
+
end
|
22
|
+
|
23
|
+
def connected?
|
24
|
+
client.connected?
|
25
|
+
end
|
26
|
+
|
27
|
+
def close
|
28
|
+
client.disconnect
|
29
|
+
end
|
30
|
+
|
31
|
+
def call(command)
|
32
|
+
push(command)
|
33
|
+
commit.last
|
34
|
+
end
|
35
|
+
|
36
|
+
def push(command)
|
37
|
+
queue << command
|
38
|
+
end
|
39
|
+
|
40
|
+
def commit
|
41
|
+
return nil if queue.empty?
|
42
|
+
|
43
|
+
result = Array.new(queue.size)
|
44
|
+
client.process(queue) do
|
45
|
+
queue.size.times do |i|
|
46
|
+
result[i] = client.read
|
47
|
+
end
|
48
|
+
end
|
49
|
+
@queue = []
|
50
|
+
|
51
|
+
return result
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative 'client'
|
3
|
+
|
4
|
+
class RedisCluster
|
5
|
+
|
6
|
+
# Cluster implement redis cluster logic for client.
|
7
|
+
class Cluster
|
8
|
+
attr_reader :options, :slots, :clients, :replicas
|
9
|
+
|
10
|
+
HASH_SLOTS = 16_384
|
11
|
+
|
12
|
+
def initialize(seeds, options = {})
|
13
|
+
@options = options
|
14
|
+
@slots = []
|
15
|
+
@clients = {}
|
16
|
+
@replicas = nil
|
17
|
+
|
18
|
+
init_client(seeds)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Return Redis::Client for a given key.
|
22
|
+
# Modified from https://github.com/antirez/redis-rb-cluster/blob/master/cluster.rb#L104-L117
|
23
|
+
def slot_for(key)
|
24
|
+
key = key.to_s
|
25
|
+
if (s = key.index('{'))
|
26
|
+
if (e = key.index('}', s + 1)) && e != s+1
|
27
|
+
key = key[s+1..e-1]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
crc16(key) % HASH_SLOTS
|
31
|
+
end
|
32
|
+
|
33
|
+
def master(slot)
|
34
|
+
slots[slot].first
|
35
|
+
end
|
36
|
+
|
37
|
+
def slave(slot)
|
38
|
+
slots[slot][1..-1].sample || slots[slot].first
|
39
|
+
end
|
40
|
+
|
41
|
+
def master_slave(slot)
|
42
|
+
slots[slot].sample
|
43
|
+
end
|
44
|
+
|
45
|
+
def close
|
46
|
+
clients.values.each(&:close)
|
47
|
+
end
|
48
|
+
|
49
|
+
def connected?
|
50
|
+
clients.values.all?(&:connected?)
|
51
|
+
end
|
52
|
+
|
53
|
+
def random
|
54
|
+
clients.values.sample
|
55
|
+
end
|
56
|
+
|
57
|
+
def reset
|
58
|
+
try = 3
|
59
|
+
begin
|
60
|
+
try -= 1
|
61
|
+
client = random
|
62
|
+
slots_and_clients(client)
|
63
|
+
rescue StandardError => e
|
64
|
+
clients.delete(client.url)
|
65
|
+
try.positive? ? retry : ( raise e )
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def [](url)
|
70
|
+
clients[url] ||= create_client(url)
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def slots_and_clients(client)
|
76
|
+
replicas = ::Hash.new{ |h, k| h[k] = [] }
|
77
|
+
|
78
|
+
client.call([:cluster, :slots]).tap do |result|
|
79
|
+
result.each do |arr|
|
80
|
+
arr[2..-1].each_with_index do |a, i|
|
81
|
+
cli = self["#{a[0]}:#{a[1]}"]
|
82
|
+
replicas[arr[0]] << cli
|
83
|
+
cli.call([:readonly]) if i.nonzero?
|
84
|
+
end
|
85
|
+
|
86
|
+
(arr[0]..arr[1]).each do |slot|
|
87
|
+
slots[slot] = replicas[arr[0]]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
@replicas = replicas
|
93
|
+
end
|
94
|
+
|
95
|
+
def init_client(seeds)
|
96
|
+
try = seeds.count
|
97
|
+
err = nil
|
98
|
+
|
99
|
+
while try.positive?
|
100
|
+
try -= 1
|
101
|
+
begin
|
102
|
+
client = create_client(seeds[try])
|
103
|
+
slots_and_clients(client)
|
104
|
+
return
|
105
|
+
rescue StandardError => e
|
106
|
+
err = e
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
raise err
|
111
|
+
end
|
112
|
+
|
113
|
+
def create_client(url)
|
114
|
+
host, port = url.split(':', 2)
|
115
|
+
Client.new(options.merge(host: host, port: port))
|
116
|
+
end
|
117
|
+
|
118
|
+
# -----------------------------------------------------------------------------
|
119
|
+
#
|
120
|
+
# This is the CRC16 algorithm used by Redis Cluster to hash keys.
|
121
|
+
# Implementation according to CCITT standards.
|
122
|
+
# Copied from https://github.com/antirez/redis-rb-cluster/blob/master/crc16.rb
|
123
|
+
#
|
124
|
+
# This is actually the XMODEM CRC 16 algorithm, using the
|
125
|
+
# following parameters:
|
126
|
+
#
|
127
|
+
# Name : "XMODEM", also known as "ZMODEM", "CRC-16/ACORN"
|
128
|
+
# Width : 16 bit
|
129
|
+
# Poly : 1021 (That is actually x^16 + x^12 + x^5 + 1)
|
130
|
+
# Initialization : 0000
|
131
|
+
# Reflect Input byte : False
|
132
|
+
# Reflect Output CRC : False
|
133
|
+
# Xor constant to output CRC : 0000
|
134
|
+
# Output for "123456789" : 31C3
|
135
|
+
|
136
|
+
def crc16(bytes)
|
137
|
+
crc = 0
|
138
|
+
bytes.each_byte do |b|
|
139
|
+
crc = ((crc<<8) & 0xffff) ^ XMODEM_CRC16_LOOKUP[((crc>>8)^b) & 0xff]
|
140
|
+
end
|
141
|
+
crc
|
142
|
+
end
|
143
|
+
|
144
|
+
XMODEM_CRC16_LOOKUP = [
|
145
|
+
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
|
146
|
+
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
|
147
|
+
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
|
148
|
+
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
|
149
|
+
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
|
150
|
+
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
|
151
|
+
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
|
152
|
+
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
|
153
|
+
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
|
154
|
+
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
|
155
|
+
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
|
156
|
+
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
|
157
|
+
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
|
158
|
+
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
|
159
|
+
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
|
160
|
+
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
|
161
|
+
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
|
162
|
+
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
|
163
|
+
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
|
164
|
+
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
|
165
|
+
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
|
166
|
+
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
|
167
|
+
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
|
168
|
+
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
|
169
|
+
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
|
170
|
+
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
|
171
|
+
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
|
172
|
+
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
|
173
|
+
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
|
174
|
+
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
|
175
|
+
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
|
176
|
+
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
|
177
|
+
]
|
178
|
+
end
|
179
|
+
end
|