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.
@@ -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: []