em_redis_cluster 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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,3 @@
1
+ require File.expand_path('../em_redis_cluster/version', __FILE__)
2
+ require File.expand_path('../em_redis_cluster/redis_protocol', __FILE__)
3
+ require File.expand_path('../em_redis_cluster/cluster', __FILE__)
@@ -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
@@ -0,0 +1,3 @@
1
+ module EMRedis
2
+ VERSION = '0.5.1' unless defined? ::EMRedis::VERSION
3
+ 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: []