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.
@@ -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
@@ -0,0 +1,36 @@
1
+ # redis-cluster
2
+ [![CircleCI](https://circleci.com/gh/bukalapak/redis-cluster.svg?style=shield&circle-token=5ebe750ce74100b7bc18768395ec3e4ebd9f1a43)](https://circleci.com/gh/bukalapak/redis-cluster)
3
+ [![Coverage](https://codecov.io/gh/bukalapak/redis-cluster/branch/master/graph/badge.svg?token=cPZvgDYAft)](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