em_redis_cluster 0.5.1
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/History.txt +22 -0
- data/lib/em_redis_cluster.rb +3 -0
- data/lib/em_redis_cluster/cluster.rb +335 -0
- data/lib/em_redis_cluster/crc16.rb +85 -0
- data/lib/em_redis_cluster/redis_error.rb +10 -0
- data/lib/em_redis_cluster/redis_protocol.rb +491 -0
- data/lib/em_redis_cluster/version.rb +3 -0
- metadata +80 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f700c069e9d3c1188c82873779fefc78900a0937
|
4
|
+
data.tar.gz: bd97730d324bdb476a8f55cb79bc7ac785e9d346
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 044bdd943741a8b2abf92da8837231789a8257d39bd3448e854578891d1c6a0578e787f59a84ca90380390540d73f1f3ef680511c8198f8dd6c1eeda7bbb2591
|
7
|
+
data.tar.gz: a0f46fb192cbae4209fbdfbef23efa6883afecabd6e314464e9006d531d320f031866c50a9ba6ae61ebb573ae4e8e18092d1990c104c8f876d53f64b3e07fc0f
|
data/History.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
== 0.2.2 / 2009-12-29
|
2
|
+
* reselect database after reconnecting
|
3
|
+
|
4
|
+
== 0.2.1 / 2009-12-15
|
5
|
+
* updated gem dependencies
|
6
|
+
|
7
|
+
== 0.2 / 2009-12-15
|
8
|
+
* rip off stock redis gem
|
9
|
+
* sort is no longer compatible with 0.1 version
|
10
|
+
* response of exists, sismember, sadd, srem, smove, zadd, zrem, move, setnx, del, renamenx, and expire is either true or false, not 0 or 1 as in 0.1
|
11
|
+
* info returns hash of symbols now
|
12
|
+
* lrem has different argument order
|
13
|
+
|
14
|
+
== 0.1.1 / 2009-05-01
|
15
|
+
|
16
|
+
* added a number of aliases to redis-based method names
|
17
|
+
* refactored process_cmd method for greater clarity
|
18
|
+
|
19
|
+
== 0.1.0 / 2009-04-28
|
20
|
+
|
21
|
+
* initial release
|
22
|
+
* compatible with Redis 0.093
|
@@ -0,0 +1,335 @@
|
|
1
|
+
# Copyright (C) 2013 Salvatore Sanfilippo <antirez@gmail.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
# a copy of this software and associated documentation files (the
|
5
|
+
# "Software"), to deal in the Software without restriction, including
|
6
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
# the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be
|
12
|
+
# included in all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
22
|
+
require 'rubygems'
|
23
|
+
require_relative 'redis_protocol'
|
24
|
+
require_relative 'redis_error'
|
25
|
+
require_relative 'crc16'
|
26
|
+
|
27
|
+
module EventMachine
|
28
|
+
module Protocols
|
29
|
+
class RedisCluster
|
30
|
+
|
31
|
+
RedisClusterHashSlots = 16384
|
32
|
+
RedisClusterRequestTTL = 16
|
33
|
+
RedisClusterDefaultTimeout = 1
|
34
|
+
|
35
|
+
def initialize(startup_nodes, opt={})
|
36
|
+
@startup_nodes = startup_nodes
|
37
|
+
|
38
|
+
@connections = {}
|
39
|
+
@opt = opt
|
40
|
+
|
41
|
+
# Redis Cluster does not support multiple databases like the stand alone version of Redis.
|
42
|
+
# There is just database 0 and the SELECT command is not allowed
|
43
|
+
@opt.delete(:db)
|
44
|
+
@logger = @opt[:logger]
|
45
|
+
|
46
|
+
@refresh_table_asap = false
|
47
|
+
@slots_initialized = false
|
48
|
+
@command_before_init = []
|
49
|
+
initialize_slots_cache {|c| yield(c) if block_given?}
|
50
|
+
end
|
51
|
+
|
52
|
+
def log(severity, msg)
|
53
|
+
@logger && @logger.send(severity, "em_redis_cluster: #{msg}")
|
54
|
+
end
|
55
|
+
|
56
|
+
def ready?
|
57
|
+
@slots_initialized
|
58
|
+
end
|
59
|
+
|
60
|
+
def any_error?
|
61
|
+
ready? && all_connections.any?{|c| c.error?}
|
62
|
+
end
|
63
|
+
|
64
|
+
def conn_status
|
65
|
+
status = {}
|
66
|
+
@connections.each {|k,c| status[k] = !c.error?}
|
67
|
+
status
|
68
|
+
end
|
69
|
+
|
70
|
+
def get_redis_link(host, port)
|
71
|
+
EM::Protocols::Redis.connect({:host => host, :port => port}.merge @opt)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Given a node (that is just a Ruby hash) give it a name just
|
75
|
+
# concatenating the host and port. We use the node name as a key
|
76
|
+
# to cache connections to that node.
|
77
|
+
def set_node_name!(n)
|
78
|
+
n[:name] ||= "#{n[:host]}:#{n[:port]}"
|
79
|
+
n
|
80
|
+
end
|
81
|
+
|
82
|
+
# Contact the startup nodes and try to fetch the hash slots -> instances
|
83
|
+
# map in order to initialize the @slots hash.
|
84
|
+
def initialize_slots_cache
|
85
|
+
@slots = Array.new(RedisClusterHashSlots)
|
86
|
+
|
87
|
+
fiber = Fiber.new do
|
88
|
+
|
89
|
+
@startup_nodes.each do |n|
|
90
|
+
@nodes = []
|
91
|
+
|
92
|
+
r = get_redis_link(n[:host], n[:port])
|
93
|
+
|
94
|
+
r.errback {|e| fiber.resume(nil)}
|
95
|
+
|
96
|
+
r.cluster("slots") {|rsp| fiber.resume(rsp)}
|
97
|
+
|
98
|
+
rsp = Fiber.yield
|
99
|
+
r.close_connection
|
100
|
+
|
101
|
+
if rsp.is_a?(Array)
|
102
|
+
rsp.each do |r|
|
103
|
+
|
104
|
+
ip, port = r[2]
|
105
|
+
# somehow redis return "" for the node it's querying
|
106
|
+
ip = n[:host] if ip == ""
|
107
|
+
|
108
|
+
node = set_node_name!(host: ip, port: port)
|
109
|
+
@nodes << node
|
110
|
+
|
111
|
+
(r[0]..r[1]).each {|slot| @slots[slot] = node}
|
112
|
+
end
|
113
|
+
|
114
|
+
populate_startup_nodes
|
115
|
+
@refresh_table_asap = false
|
116
|
+
@slots_initialized = true
|
117
|
+
|
118
|
+
# Exit the loop as long as the first node replies
|
119
|
+
break
|
120
|
+
else
|
121
|
+
next
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
log :debug, "RedisCluster: #{@nodes}"
|
126
|
+
|
127
|
+
yield(self) if block_given?
|
128
|
+
|
129
|
+
# run cached commands before initialization
|
130
|
+
if ready?
|
131
|
+
@command_before_init.each do |argv|
|
132
|
+
argv.respond_to?(:call) ? argv.call : send_cluster_command(argv)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
@command_before_init = []
|
136
|
+
end
|
137
|
+
|
138
|
+
fiber.resume
|
139
|
+
end
|
140
|
+
|
141
|
+
# Use @nodes to populate @startup_nodes, so that we have more chances
|
142
|
+
# if a subset of the cluster fails.
|
143
|
+
def populate_startup_nodes
|
144
|
+
# Make sure every node has already a name, so that later the
|
145
|
+
# Array uniq! method will work reliably.
|
146
|
+
@startup_nodes.each{|n| set_node_name!(n)}
|
147
|
+
@nodes.each{|n| @startup_nodes << n}
|
148
|
+
@startup_nodes.uniq!
|
149
|
+
end
|
150
|
+
|
151
|
+
# Flush the cache, mostly useful for debugging when we want to force
|
152
|
+
# redirection.
|
153
|
+
def flush_slots_cache
|
154
|
+
@slots = Array.new(RedisClusterHashSlots)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Return the hash slot from the key.
|
158
|
+
def keyslot(key)
|
159
|
+
# Only hash what is inside {...} if there is such a pattern in the key.
|
160
|
+
# Note that the specification requires the content that is between
|
161
|
+
# the first { and the first } after the first {. If we found {} without
|
162
|
+
# nothing in the middle, the whole key is hashed as usually.
|
163
|
+
s = key.index "{"
|
164
|
+
if s
|
165
|
+
e = key.index "}",s+1
|
166
|
+
if e && e != s+1
|
167
|
+
key = key[s+1..e-1]
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
RedisClusterCRC16.crc16(key) % RedisClusterHashSlots
|
172
|
+
end
|
173
|
+
|
174
|
+
# Return the first key in the command arguments.
|
175
|
+
#
|
176
|
+
# Currently we just return argv[1], that is, the first argument
|
177
|
+
# after the command name.
|
178
|
+
#
|
179
|
+
# This is indeed the key for most commands, and when it is not true
|
180
|
+
# the cluster redirection will point us to the right node anyway.
|
181
|
+
#
|
182
|
+
# For commands we want to explicitly bad as they don't make sense
|
183
|
+
# in the context of cluster, nil is returned.
|
184
|
+
def get_key_from_command(argv)
|
185
|
+
case argv[0].to_s.downcase
|
186
|
+
when "info","multi","exec","slaveof","config","shutdown","select"
|
187
|
+
nil
|
188
|
+
else
|
189
|
+
# Unknown commands, and all the commands having the key
|
190
|
+
# as first argument are handled here:
|
191
|
+
# set, get, ...
|
192
|
+
argv[1]
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def get_random_connection
|
197
|
+
n = @startup_nodes.shuffle.first
|
198
|
+
@connections[n[:name]] ||= get_redis_link(n[:host], n[:port])
|
199
|
+
end
|
200
|
+
|
201
|
+
# Given a slot return the link (Redis instance) to the mapped node.
|
202
|
+
# Make sure to create a connection with the node if we don't have
|
203
|
+
# one.
|
204
|
+
def get_connection_by_key(key)
|
205
|
+
if n = @slots[keyslot(key)]
|
206
|
+
@connections[n[:name]] ||= get_redis_link(n[:host], n[:port])
|
207
|
+
else
|
208
|
+
# If we don't know what the mapping is, return a random node.
|
209
|
+
get_random_connection
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def get_connection_by_node(n)
|
214
|
+
set_node_name!(n)
|
215
|
+
@connections[n[:name]] ||= get_redis_link(n[:host], n[:port])
|
216
|
+
end
|
217
|
+
|
218
|
+
def all_connections
|
219
|
+
@startup_nodes.each do |n|
|
220
|
+
@connections[n[:name]] ||= get_redis_link(n[:host], n[:port])
|
221
|
+
end
|
222
|
+
@connections.values
|
223
|
+
end
|
224
|
+
|
225
|
+
# Dispatch commands.
|
226
|
+
def send_cluster_command(argv)
|
227
|
+
|
228
|
+
callback = argv.pop
|
229
|
+
callback = nil unless callback.respond_to?(:call)
|
230
|
+
|
231
|
+
ttl = RedisClusterRequestTTL
|
232
|
+
asking = false
|
233
|
+
conn_for_next_cmd = nil
|
234
|
+
try_random_node = false
|
235
|
+
|
236
|
+
|
237
|
+
fiber = Fiber.new do
|
238
|
+
while ttl > 0 do
|
239
|
+
key = get_key_from_command(argv)
|
240
|
+
|
241
|
+
# raise Redis::ParserError.new("No way to dispatch this command to Redis Cluster.") unless key
|
242
|
+
|
243
|
+
# The full semantics of ASK redirection from the point of view of the client is as follows:
|
244
|
+
# If ASK redirection is received, send only the query that was redirected to the specified node but continue sending subsequent queries to the old node.
|
245
|
+
# Start the redirected query with the ASKING command.
|
246
|
+
# Don't yet update local client tables to map hash slot 8 to B.
|
247
|
+
conn_for_next_cmd ||= (key ? get_connection_by_key(key) : get_random_connection)
|
248
|
+
conn_for_next_cmd.asking if asking
|
249
|
+
conn_for_next_cmd.send(argv[0].to_sym, *argv[1..-1]) {|rsp| fiber.resume(rsp) }
|
250
|
+
|
251
|
+
rsp = Fiber.yield
|
252
|
+
|
253
|
+
conn_for_next_cmd = nil
|
254
|
+
asking = false
|
255
|
+
|
256
|
+
if rsp.is_a?(RedisError)
|
257
|
+
errv = rsp.to_s.split
|
258
|
+
if errv[0] == "MOVED" || errv[0] == "ASK"
|
259
|
+
log :debug, rsp.to_s
|
260
|
+
|
261
|
+
newslot = errv[1].to_i
|
262
|
+
node_ip, node_port = errv[2].split(":").map{|x|x.strip}
|
263
|
+
|
264
|
+
if errv[0] == "ASK"
|
265
|
+
asking = true
|
266
|
+
conn_for_next_cmd = get_connection_by_node(host: node_ip, port: node_port)
|
267
|
+
else
|
268
|
+
# Serve replied with MOVED. It's better for us to ask for CLUSTER NODES the next time.
|
269
|
+
@refresh_table_asap = true
|
270
|
+
@slots[newslot] = set_node_name!(host: node_ip, port: node_port.to_i)
|
271
|
+
end
|
272
|
+
else
|
273
|
+
callback && callback.call(rsp)
|
274
|
+
break
|
275
|
+
end
|
276
|
+
else
|
277
|
+
callback && callback.call(rsp)
|
278
|
+
break
|
279
|
+
end
|
280
|
+
|
281
|
+
ttl -= 1
|
282
|
+
end
|
283
|
+
|
284
|
+
callback && callback.call(rsp) if ttl == 0
|
285
|
+
|
286
|
+
initialize_slots_cache if @refresh_table_asap
|
287
|
+
end
|
288
|
+
|
289
|
+
fiber.resume
|
290
|
+
end
|
291
|
+
|
292
|
+
# Currently we handle all the commands using method_missing for
|
293
|
+
# simplicity. For a Cluster client actually it will be better to have
|
294
|
+
# every single command as a method with the right arity and possibly
|
295
|
+
# additional checks (example: RPOPLPUSH with same src/dst key, SORT
|
296
|
+
# without GET or BY, and so forth).
|
297
|
+
def method_missing(*argv, &blk)
|
298
|
+
argv << blk
|
299
|
+
if ready?
|
300
|
+
send_cluster_command(argv)
|
301
|
+
else
|
302
|
+
@command_before_init << argv
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
[:flushdb, :flushall].each do |fn|
|
307
|
+
define_method fn do
|
308
|
+
if ready?
|
309
|
+
all_connections.each {|c| c.send(fn)}
|
310
|
+
else
|
311
|
+
@command_before_init << proc {self.send(fn)}
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
# used for testing connection auto-recovery
|
317
|
+
def conn_close(key)
|
318
|
+
c = get_connection_by_key(key)
|
319
|
+
c && c.close_connection_after_writing
|
320
|
+
end
|
321
|
+
|
322
|
+
# used for testing connection auto-recovery
|
323
|
+
def conn_error?(key)
|
324
|
+
c = get_connection_by_key(key)
|
325
|
+
c && c.error?
|
326
|
+
end
|
327
|
+
|
328
|
+
# used for testing connection auto-recovery
|
329
|
+
def get_slotname_by_key(key)
|
330
|
+
@slots[keyslot(key)][:name] rescue nil
|
331
|
+
end
|
332
|
+
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# Copyright (C) 2013 Salvatore Sanfilippo <antirez@gmail.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
# a copy of this software and associated documentation files (the
|
5
|
+
# "Software"), to deal in the Software without restriction, including
|
6
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
# the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be
|
12
|
+
# included in all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
#
|
22
|
+
# -----------------------------------------------------------------------------
|
23
|
+
#
|
24
|
+
# This is the CRC16 algorithm used by Redis Cluster to hash keys.
|
25
|
+
# Implementation according to CCITT standards.
|
26
|
+
#
|
27
|
+
# This is actually the XMODEM CRC 16 algorithm, using the
|
28
|
+
# following parameters:
|
29
|
+
#
|
30
|
+
# Name : "XMODEM", also known as "ZMODEM", "CRC-16/ACORN"
|
31
|
+
# Width : 16 bit
|
32
|
+
# Poly : 1021 (That is actually x^16 + x^12 + x^5 + 1)
|
33
|
+
# Initialization : 0000
|
34
|
+
# Reflect Input byte : False
|
35
|
+
# Reflect Output CRC : False
|
36
|
+
# Xor constant to output CRC : 0000
|
37
|
+
# Output for "123456789" : 31C3
|
38
|
+
|
39
|
+
module RedisClusterCRC16
|
40
|
+
|
41
|
+
def RedisClusterCRC16.crc16(bytes)
|
42
|
+
crc = 0
|
43
|
+
bytes.each_byte{|b|
|
44
|
+
crc = ((crc<<8) & 0xffff) ^ XMODEMCRC16Lookup[((crc>>8)^b) & 0xff]
|
45
|
+
}
|
46
|
+
crc
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
XMODEMCRC16Lookup = [
|
52
|
+
0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7,
|
53
|
+
0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef,
|
54
|
+
0x1231,0x0210,0x3273,0x2252,0x52b5,0x4294,0x72f7,0x62d6,
|
55
|
+
0x9339,0x8318,0xb37b,0xa35a,0xd3bd,0xc39c,0xf3ff,0xe3de,
|
56
|
+
0x2462,0x3443,0x0420,0x1401,0x64e6,0x74c7,0x44a4,0x5485,
|
57
|
+
0xa56a,0xb54b,0x8528,0x9509,0xe5ee,0xf5cf,0xc5ac,0xd58d,
|
58
|
+
0x3653,0x2672,0x1611,0x0630,0x76d7,0x66f6,0x5695,0x46b4,
|
59
|
+
0xb75b,0xa77a,0x9719,0x8738,0xf7df,0xe7fe,0xd79d,0xc7bc,
|
60
|
+
0x48c4,0x58e5,0x6886,0x78a7,0x0840,0x1861,0x2802,0x3823,
|
61
|
+
0xc9cc,0xd9ed,0xe98e,0xf9af,0x8948,0x9969,0xa90a,0xb92b,
|
62
|
+
0x5af5,0x4ad4,0x7ab7,0x6a96,0x1a71,0x0a50,0x3a33,0x2a12,
|
63
|
+
0xdbfd,0xcbdc,0xfbbf,0xeb9e,0x9b79,0x8b58,0xbb3b,0xab1a,
|
64
|
+
0x6ca6,0x7c87,0x4ce4,0x5cc5,0x2c22,0x3c03,0x0c60,0x1c41,
|
65
|
+
0xedae,0xfd8f,0xcdec,0xddcd,0xad2a,0xbd0b,0x8d68,0x9d49,
|
66
|
+
0x7e97,0x6eb6,0x5ed5,0x4ef4,0x3e13,0x2e32,0x1e51,0x0e70,
|
67
|
+
0xff9f,0xefbe,0xdfdd,0xcffc,0xbf1b,0xaf3a,0x9f59,0x8f78,
|
68
|
+
0x9188,0x81a9,0xb1ca,0xa1eb,0xd10c,0xc12d,0xf14e,0xe16f,
|
69
|
+
0x1080,0x00a1,0x30c2,0x20e3,0x5004,0x4025,0x7046,0x6067,
|
70
|
+
0x83b9,0x9398,0xa3fb,0xb3da,0xc33d,0xd31c,0xe37f,0xf35e,
|
71
|
+
0x02b1,0x1290,0x22f3,0x32d2,0x4235,0x5214,0x6277,0x7256,
|
72
|
+
0xb5ea,0xa5cb,0x95a8,0x8589,0xf56e,0xe54f,0xd52c,0xc50d,
|
73
|
+
0x34e2,0x24c3,0x14a0,0x0481,0x7466,0x6447,0x5424,0x4405,
|
74
|
+
0xa7db,0xb7fa,0x8799,0x97b8,0xe75f,0xf77e,0xc71d,0xd73c,
|
75
|
+
0x26d3,0x36f2,0x0691,0x16b0,0x6657,0x7676,0x4615,0x5634,
|
76
|
+
0xd94c,0xc96d,0xf90e,0xe92f,0x99c8,0x89e9,0xb98a,0xa9ab,
|
77
|
+
0x5844,0x4865,0x7806,0x6827,0x18c0,0x08e1,0x3882,0x28a3,
|
78
|
+
0xcb7d,0xdb5c,0xeb3f,0xfb1e,0x8bf9,0x9bd8,0xabbb,0xbb9a,
|
79
|
+
0x4a75,0x5a54,0x6a37,0x7a16,0x0af1,0x1ad0,0x2ab3,0x3a92,
|
80
|
+
0xfd2e,0xed0f,0xdd6c,0xcd4d,0xbdaa,0xad8b,0x9de8,0x8dc9,
|
81
|
+
0x7c26,0x6c07,0x5c64,0x4c45,0x3ca2,0x2c83,0x1ce0,0x0cc1,
|
82
|
+
0xef1f,0xff3e,0xcf5d,0xdf7c,0xaf9b,0xbfba,0x8fd9,0x9ff8,
|
83
|
+
0x6e17,0x7e36,0x4e55,0x5e74,0x2e93,0x3eb2,0x0ed1,0x1ef0
|
84
|
+
]
|
85
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module Protocols
|
3
|
+
# errors
|
4
|
+
class ParserError < StandardError; end
|
5
|
+
class ProtocolError < StandardError; end
|
6
|
+
class TimeoutError < StandardError; end
|
7
|
+
class ConnectError < StandardError; end
|
8
|
+
class RedisError < StandardError; end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,491 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'eventmachine'
|
3
|
+
|
4
|
+
require_relative 'redis_error'
|
5
|
+
|
6
|
+
module EventMachine
|
7
|
+
module Protocols
|
8
|
+
module Redis
|
9
|
+
|
10
|
+
include EM::Deferrable
|
11
|
+
|
12
|
+
##
|
13
|
+
# constants
|
14
|
+
#########################
|
15
|
+
|
16
|
+
OK = "OK".freeze
|
17
|
+
MINUS = "-".freeze
|
18
|
+
PLUS = "+".freeze
|
19
|
+
COLON = ":".freeze
|
20
|
+
DOLLAR = "$".freeze
|
21
|
+
ASTERISK = "*".freeze
|
22
|
+
DELIM = "\r\n".freeze
|
23
|
+
|
24
|
+
BOOLEAN_PROCESSOR = lambda{|r| r == 1 }
|
25
|
+
|
26
|
+
REPLY_PROCESSOR = {
|
27
|
+
"exists" => BOOLEAN_PROCESSOR,
|
28
|
+
"sismember" => BOOLEAN_PROCESSOR,
|
29
|
+
# "sadd" => BOOLEAN_PROCESSOR,
|
30
|
+
# "srem" => BOOLEAN_PROCESSOR,
|
31
|
+
"smove" => BOOLEAN_PROCESSOR,
|
32
|
+
"zadd" => BOOLEAN_PROCESSOR,
|
33
|
+
"zrem" => BOOLEAN_PROCESSOR,
|
34
|
+
"move" => BOOLEAN_PROCESSOR,
|
35
|
+
"setnx" => BOOLEAN_PROCESSOR,
|
36
|
+
"del" => BOOLEAN_PROCESSOR,
|
37
|
+
"renamenx" => BOOLEAN_PROCESSOR,
|
38
|
+
"expire" => BOOLEAN_PROCESSOR,
|
39
|
+
"select" => BOOLEAN_PROCESSOR, # not in redis gem
|
40
|
+
"hset" => BOOLEAN_PROCESSOR,
|
41
|
+
"hdel" => BOOLEAN_PROCESSOR,
|
42
|
+
"hexists" => BOOLEAN_PROCESSOR,
|
43
|
+
"keys" => lambda {|r|
|
44
|
+
if r.is_a?(Array)
|
45
|
+
r
|
46
|
+
else
|
47
|
+
r.split(" ")
|
48
|
+
end
|
49
|
+
},
|
50
|
+
"info" => lambda{|r|
|
51
|
+
info = {}
|
52
|
+
r.each_line {|kv|
|
53
|
+
k,v = kv.split(":",2).map{|x| x.chomp}
|
54
|
+
info[k.to_sym] = v
|
55
|
+
}
|
56
|
+
info
|
57
|
+
},
|
58
|
+
"hgetall" => lambda{|r|
|
59
|
+
Hash[*r]
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
ALIASES = {
|
64
|
+
"flush_db" => "flushdb",
|
65
|
+
"flush_all" => "flushall",
|
66
|
+
"last_save" => "lastsave",
|
67
|
+
"key?" => "exists",
|
68
|
+
"delete" => "del",
|
69
|
+
"randkey" => "randomkey",
|
70
|
+
"list_length" => "llen",
|
71
|
+
"push_tail" => "rpush",
|
72
|
+
"push_head" => "lpush",
|
73
|
+
"pop_tail" => "rpop",
|
74
|
+
"pop_head" => "lpop",
|
75
|
+
"list_set" => "lset",
|
76
|
+
"list_range" => "lrange",
|
77
|
+
"list_trim" => "ltrim",
|
78
|
+
"list_index" => "lindex",
|
79
|
+
"list_rm" => "lrem",
|
80
|
+
"set_add" => "sadd",
|
81
|
+
"set_delete" => "srem",
|
82
|
+
"set_count" => "scard",
|
83
|
+
"set_member?" => "sismember",
|
84
|
+
"set_members" => "smembers",
|
85
|
+
"set_intersect" => "sinter",
|
86
|
+
"set_intersect_store" => "sinterstore",
|
87
|
+
"set_inter_store" => "sinterstore",
|
88
|
+
"set_union" => "sunion",
|
89
|
+
"set_union_store" => "sunionstore",
|
90
|
+
"set_diff" => "sdiff",
|
91
|
+
"set_diff_store" => "sdiffstore",
|
92
|
+
"set_move" => "smove",
|
93
|
+
"set_unless_exists" => "setnx",
|
94
|
+
"rename_unless_exists" => "renamenx",
|
95
|
+
"type?" => "type",
|
96
|
+
"zset_add" => "zadd",
|
97
|
+
"zset_count" => "zcard",
|
98
|
+
"zset_range_by_score" => "zrangebyscore",
|
99
|
+
"zset_reverse_range" => "zrevrange",
|
100
|
+
"zset_range" => "zrange",
|
101
|
+
"zset_delete" => "zrem",
|
102
|
+
"zset_score" => "zscore",
|
103
|
+
"zset_incr_by" => "zincrby",
|
104
|
+
"zset_increment_by" => "zincrby",
|
105
|
+
# these aliases aren't in redis gem
|
106
|
+
"background_save" => 'bgsave',
|
107
|
+
"async_save" => 'bgsave',
|
108
|
+
"members" => 'smembers',
|
109
|
+
"decrement_by" => "decrby",
|
110
|
+
"decrement" => "decr",
|
111
|
+
"increment_by" => "incrby",
|
112
|
+
"increment" => "incr",
|
113
|
+
"set_if_nil" => "setnx",
|
114
|
+
"multi_get" => "mget",
|
115
|
+
"random_key" => "randomkey",
|
116
|
+
"random" => "randomkey",
|
117
|
+
"rename_if_nil" => "renamenx",
|
118
|
+
"tail_pop" => "rpop",
|
119
|
+
"pop" => "rpop",
|
120
|
+
"head_pop" => "lpop",
|
121
|
+
"shift" => "lpop",
|
122
|
+
"list_remove" => "lrem",
|
123
|
+
"index" => "lindex",
|
124
|
+
"trim" => "ltrim",
|
125
|
+
"range" => "lrange",
|
126
|
+
"list_len" => "llen",
|
127
|
+
"len" => "llen",
|
128
|
+
"head_push" => "lpush",
|
129
|
+
"unshift" => "lpush",
|
130
|
+
"tail_push" => "rpush",
|
131
|
+
"push" => "rpush",
|
132
|
+
"add" => "sadd",
|
133
|
+
"set_remove" => "srem",
|
134
|
+
"set_size" => "scard",
|
135
|
+
"member?" => "sismember",
|
136
|
+
"intersect" => "sinter",
|
137
|
+
"intersect_and_store" => "sinterstore",
|
138
|
+
"exists?" => "exists"
|
139
|
+
}
|
140
|
+
|
141
|
+
DISABLED_COMMANDS = {
|
142
|
+
"monitor" => true,
|
143
|
+
"sync" => true
|
144
|
+
}
|
145
|
+
|
146
|
+
def []=(key,value)
|
147
|
+
set(key,value)
|
148
|
+
end
|
149
|
+
|
150
|
+
def set(key, value, expiry=nil)
|
151
|
+
call_command([:set, key, value]) do |s|
|
152
|
+
yield s if block_given?
|
153
|
+
end
|
154
|
+
expire(key, expiry) if expiry
|
155
|
+
end
|
156
|
+
|
157
|
+
def sort(key, options={}, &blk)
|
158
|
+
cmd = ["SORT"]
|
159
|
+
cmd << key
|
160
|
+
cmd << ["BY", options[:by]] if options[:by]
|
161
|
+
cmd << [options[:get]].flatten.map { |key| ["GET", key] } if options[:get]
|
162
|
+
cmd << options[:order].split(/\s+/) if options[:order]
|
163
|
+
cmd << ["LIMIT", options[:limit]] if options[:limit]
|
164
|
+
call_command(cmd.flatten, &blk)
|
165
|
+
end
|
166
|
+
|
167
|
+
def incr(key, increment = nil, &blk)
|
168
|
+
call_command(increment ? ["incrby",key,increment] : ["incr",key], &blk)
|
169
|
+
end
|
170
|
+
|
171
|
+
def decr(key, decrement = nil, &blk)
|
172
|
+
call_command(decrement ? ["decrby",key,decrement] : ["decr",key], &blk)
|
173
|
+
end
|
174
|
+
|
175
|
+
def select(db, &blk)
|
176
|
+
@db = db.to_i
|
177
|
+
call_command(['select', @db], &blk)
|
178
|
+
end
|
179
|
+
|
180
|
+
def auth(password, &blk)
|
181
|
+
@password = password
|
182
|
+
call_command(['auth', password], &blk)
|
183
|
+
end
|
184
|
+
|
185
|
+
def sadd(key, member)
|
186
|
+
call_command([:sadd, key, *member]) do |s|
|
187
|
+
yield s if block_given?
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def srem(key, member)
|
192
|
+
call_command([:srem, key, *member]) do |s|
|
193
|
+
yield s if block_given?
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def hmset(key, value)
|
198
|
+
call_command([:hmset, key, *value.to_a.flatten]) do |s|
|
199
|
+
yield s if block_given?
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def mapped_hmset(key, hash)
|
204
|
+
hmset(key, hash.to_a.flatten) do |s|
|
205
|
+
yield s if block_given?
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# Similar to memcache.rb's #get_multi, returns a hash mapping
|
210
|
+
# keys to values.
|
211
|
+
def mapped_mget(*keys)
|
212
|
+
mget(*keys) do |response|
|
213
|
+
result = {}
|
214
|
+
response.each do |value|
|
215
|
+
key = keys.shift
|
216
|
+
result.merge!(key => value) unless value.nil?
|
217
|
+
end
|
218
|
+
yield result if block_given?
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
# Ruby defines a now deprecated type method so we need to override it here
|
223
|
+
# since it will never hit method_missing
|
224
|
+
def type(key, &blk)
|
225
|
+
call_command(['type', key], &blk)
|
226
|
+
end
|
227
|
+
|
228
|
+
def quit(&blk)
|
229
|
+
call_command(['quit'], &blk)
|
230
|
+
end
|
231
|
+
|
232
|
+
def exec(&blk)
|
233
|
+
call_command(['exec'], &blk)
|
234
|
+
end
|
235
|
+
|
236
|
+
# I'm not sure autocommit is a good idea.
|
237
|
+
# For example:
|
238
|
+
# r.multi { r.set('a', 'b') { raise "kaboom" } }
|
239
|
+
# will commit "a" and will stop EM
|
240
|
+
def multi
|
241
|
+
call_command(['multi'])
|
242
|
+
if block_given?
|
243
|
+
begin
|
244
|
+
yield self
|
245
|
+
exec
|
246
|
+
rescue Exception => e
|
247
|
+
discard
|
248
|
+
raise e
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def mset(*args, &blk)
|
254
|
+
hsh = args.pop if Hash === args.last
|
255
|
+
if hsh
|
256
|
+
call_command(hsh.to_a.flatten.unshift(:mset), &blk)
|
257
|
+
else
|
258
|
+
call_command(args.unshift(:mset), &blk)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def msetnx(*args, &blk)
|
263
|
+
hsh = args.pop if Hash === args.last
|
264
|
+
if hsh
|
265
|
+
call_command(hsh.to_a.flatten.unshift(:msetnx), &blk)
|
266
|
+
else
|
267
|
+
call_command(args.unshift(:msetnx), &blk)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def errback(&blk)
|
272
|
+
@error_callback = blk
|
273
|
+
end
|
274
|
+
alias_method :on_error, :errback
|
275
|
+
|
276
|
+
def method_missing(*argv, &blk)
|
277
|
+
call_command(argv, &blk)
|
278
|
+
end
|
279
|
+
|
280
|
+
def maybe_lock(&blk)
|
281
|
+
if !EM.reactor_thread?
|
282
|
+
EM.schedule { maybe_lock(&blk) }
|
283
|
+
elsif @connected
|
284
|
+
yield
|
285
|
+
else
|
286
|
+
callback { yield }
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def call_command(argv, &blk)
|
291
|
+
argv = argv.dup
|
292
|
+
|
293
|
+
argv[0] = argv[0].to_s.downcase
|
294
|
+
argv[0] = ALIASES[argv[0]] if ALIASES[argv[0]]
|
295
|
+
raise "#{argv[0]} command is disabled" if DISABLED_COMMANDS[argv[0]]
|
296
|
+
|
297
|
+
command = ""
|
298
|
+
command << "*#{argv.size}\r\n"
|
299
|
+
argv.each do |a|
|
300
|
+
a = a.to_s
|
301
|
+
command << "$#{get_size(a)}\r\n"
|
302
|
+
command << a
|
303
|
+
command << "\r\n"
|
304
|
+
end
|
305
|
+
|
306
|
+
# log :debug, "sending: #{command}"
|
307
|
+
|
308
|
+
maybe_lock do
|
309
|
+
@redis_callbacks << [REPLY_PROCESSOR[argv[0]], blk]
|
310
|
+
send_data command
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
##
|
315
|
+
# em hooks
|
316
|
+
#########################
|
317
|
+
|
318
|
+
def self.connect(*args)
|
319
|
+
case args.length
|
320
|
+
when 0
|
321
|
+
options = {}
|
322
|
+
when 1
|
323
|
+
arg = args.shift
|
324
|
+
case arg
|
325
|
+
when Hash then options = arg
|
326
|
+
when String then options = {:host => arg}
|
327
|
+
else raise ArgumentError, 'first argument must be Hash or String'
|
328
|
+
end
|
329
|
+
when 2
|
330
|
+
options = {:host => args[0], :port => args[1]}
|
331
|
+
else
|
332
|
+
raise ArgumentError, "wrong number of arguments (#{args.length} for 1)"
|
333
|
+
end
|
334
|
+
options[:host] ||= '127.0.0.1'
|
335
|
+
options[:port] = (options[:port] || 6379).to_i
|
336
|
+
EM.connect options[:host], options[:port], self, options
|
337
|
+
end
|
338
|
+
|
339
|
+
def initialize(options = {})
|
340
|
+
@host = options[:host]
|
341
|
+
@port = options[:port]
|
342
|
+
@db = (options[:db] || 0).to_i
|
343
|
+
@password = options[:password]
|
344
|
+
@logger = options[:logger]
|
345
|
+
@reconn_timer = options[:reconn_timer] || 0.2 # reconnect after second
|
346
|
+
@redis_callbacks = []
|
347
|
+
@error_callback = nil
|
348
|
+
|
349
|
+
# These commands should be first
|
350
|
+
auth_and_select_db
|
351
|
+
end
|
352
|
+
|
353
|
+
def log(severity, msg)
|
354
|
+
@logger && @logger.send(severity, "em_redis: #{msg}")
|
355
|
+
end
|
356
|
+
|
357
|
+
def auth_and_select_db
|
358
|
+
call_command(["auth", @password]) if @password
|
359
|
+
call_command(["select", @db]) unless @db == 0
|
360
|
+
end
|
361
|
+
private :auth_and_select_db
|
362
|
+
|
363
|
+
def connection_completed
|
364
|
+
log :debug, "Connected to #{@host}:#{@port}"
|
365
|
+
dispatch_on_conn_err
|
366
|
+
|
367
|
+
@previous_multibulks = []
|
368
|
+
@multibulk_n = false
|
369
|
+
@connected = true
|
370
|
+
|
371
|
+
succeed
|
372
|
+
end
|
373
|
+
|
374
|
+
# 19Feb09 Switched to a custom parser, LineText2 is recursive and can cause
|
375
|
+
# stack overflows when there is too much data.
|
376
|
+
# include EM::P::LineText2
|
377
|
+
def receive_data(data)
|
378
|
+
(@buffer ||= '') << data
|
379
|
+
while index = @buffer.index(DELIM)
|
380
|
+
begin
|
381
|
+
line = @buffer.slice!(0, index+2)
|
382
|
+
process_cmd line
|
383
|
+
rescue ParserError
|
384
|
+
@buffer[0...0] = line
|
385
|
+
break
|
386
|
+
end
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
def process_cmd(line)
|
391
|
+
# log :debug, "processing #{line}"
|
392
|
+
# first character of buffer will always be the response type
|
393
|
+
reply_type = line[0, 1]
|
394
|
+
reply_args = line.slice(1..-3) # remove type character and \r\n
|
395
|
+
case reply_type
|
396
|
+
|
397
|
+
#e.g. -MISSING
|
398
|
+
when MINUS
|
399
|
+
excep = RedisError.new(reply_args)
|
400
|
+
dispatch_response(excep)
|
401
|
+
@error_callback && @error_callback.call(excep)
|
402
|
+
# e.g. +OK
|
403
|
+
when PLUS
|
404
|
+
dispatch_response(reply_args)
|
405
|
+
# e.g. $3\r\nabc\r\n
|
406
|
+
# 'bulk' is more complex because it could be part of multi-bulk
|
407
|
+
when DOLLAR
|
408
|
+
data_len = Integer(reply_args)
|
409
|
+
if data_len == -1 # expect no data; return nil
|
410
|
+
dispatch_response(nil)
|
411
|
+
elsif @buffer.size >= data_len + 2 # buffer is full of expected data
|
412
|
+
dispatch_response(@buffer.slice!(0, data_len))
|
413
|
+
@buffer.slice!(0,2) # tossing \r\n
|
414
|
+
else # buffer isn't full or nil
|
415
|
+
# TODO: don't control execution with exceptions
|
416
|
+
raise ParserError
|
417
|
+
end
|
418
|
+
#e.g. :8
|
419
|
+
when COLON
|
420
|
+
dispatch_response(Integer(reply_args))
|
421
|
+
#e.g. *2\r\n$1\r\na\r\n$1\r\nb\r\n
|
422
|
+
when ASTERISK
|
423
|
+
multibulk_count = Integer(reply_args)
|
424
|
+
if multibulk_count == -1 || multibulk_count == 0
|
425
|
+
dispatch_response([])
|
426
|
+
else
|
427
|
+
if @multibulk_n
|
428
|
+
@previous_multibulks << [@multibulk_n, @multibulk_values]
|
429
|
+
end
|
430
|
+
@multibulk_n = multibulk_count
|
431
|
+
@multibulk_values = []
|
432
|
+
end
|
433
|
+
# Whu?
|
434
|
+
else
|
435
|
+
excep = ProtocolError.new("unknown reply_type: #{reply_type}")
|
436
|
+
dispatch_response(excep)
|
437
|
+
@error_callback && @error_callback.call(excep)
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
def dispatch_response(value)
|
442
|
+
if @multibulk_n
|
443
|
+
@multibulk_values << value
|
444
|
+
@multibulk_n -= 1
|
445
|
+
|
446
|
+
if @multibulk_n == 0
|
447
|
+
value = @multibulk_values
|
448
|
+
@multibulk_n, @multibulk_values = @previous_multibulks.pop
|
449
|
+
if @multibulk_n
|
450
|
+
dispatch_response(value)
|
451
|
+
return
|
452
|
+
end
|
453
|
+
else
|
454
|
+
return
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
processor, blk = @redis_callbacks.shift
|
459
|
+
value = processor.call(value) if processor
|
460
|
+
blk.call(value) if blk
|
461
|
+
end
|
462
|
+
|
463
|
+
def unbind(reason)
|
464
|
+
log :debug, "Disconnected from #{@host}:#{@port}: #{reason}"
|
465
|
+
dispatch_on_conn_err
|
466
|
+
|
467
|
+
# keep re-connecting
|
468
|
+
EM.add_timer(@reconn_timer) do
|
469
|
+
@logger.debug { "Reconnecting to #{@host}:#{@port}" } if @logger
|
470
|
+
reconnect @host, @port
|
471
|
+
auth_and_select_db
|
472
|
+
end
|
473
|
+
@connected = false
|
474
|
+
@deferred_status = nil
|
475
|
+
end
|
476
|
+
|
477
|
+
private
|
478
|
+
def dispatch_on_conn_err
|
479
|
+
while @redis_callbacks.size > 0
|
480
|
+
processor, blk = @redis_callbacks.shift
|
481
|
+
blk && blk.call(ConnectError.new("Connection error"))
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
def get_size(string)
|
486
|
+
string.respond_to?(:bytesize) ? string.bytesize : string.size
|
487
|
+
end
|
488
|
+
|
489
|
+
end
|
490
|
+
end
|
491
|
+
end
|
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: em_redis_cluster
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jonathan Broad
|
8
|
+
- Eugene Pimenov
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2017-05-09 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: eventmachine
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: bundler
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 1.0.rc.6
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: 1.0.rc.6
|
42
|
+
description: An eventmachine-based implementation of the Redis protocol and Redis
|
43
|
+
cluster support
|
44
|
+
email: lxz.tty@gmail.com
|
45
|
+
executables: []
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- History.txt
|
50
|
+
- lib/em_redis_cluster.rb
|
51
|
+
- lib/em_redis_cluster/cluster.rb
|
52
|
+
- lib/em_redis_cluster/crc16.rb
|
53
|
+
- lib/em_redis_cluster/redis_error.rb
|
54
|
+
- lib/em_redis_cluster/redis_protocol.rb
|
55
|
+
- lib/em_redis_cluster/version.rb
|
56
|
+
homepage: https://github.com/xiuzhong/em_redis_cluster
|
57
|
+
licenses: []
|
58
|
+
metadata: {}
|
59
|
+
post_install_message:
|
60
|
+
rdoc_options: []
|
61
|
+
require_paths:
|
62
|
+
- lib
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
requirements: []
|
74
|
+
rubyforge_project:
|
75
|
+
rubygems_version: 2.6.8
|
76
|
+
signing_key:
|
77
|
+
specification_version: 4
|
78
|
+
summary: An eventmachine-based implementation of the Redis protocol and Redis cluster
|
79
|
+
support
|
80
|
+
test_files: []
|