rb-cluster-mm 0.0.2
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/lib/crc16.rb +85 -0
- data/lib/rediscluster.rb +278 -0
- metadata +45 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 297f654807597a7363bcbd0de01ce2ebb2e64c67
|
|
4
|
+
data.tar.gz: 996a10d446ecde7c892b7a5a43325487b7399d5d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8d4dfd14942c627ca5c78ff78626cf8edd7f0bb311f0126c45b6371668f26c2e3870a39fa2e02dc14b76e7d92edd7cfa767e88061081a3ce2734b5eccaf8a1ab
|
|
7
|
+
data.tar.gz: b575a4b90634b14f10b41a4730fff5efcc2d1d49cc91d6d666e32503665b8411586054ea02ae6817c266c6eb45c03e60b8f5a098e11821d969c1a3bb7013193e
|
data/lib/crc16.rb
ADDED
|
@@ -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
|
data/lib/rediscluster.rb
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
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 'redis'
|
|
24
|
+
require File.expand_path('../../../support/redis_library/crc16.rb', __FILE__)
|
|
25
|
+
|
|
26
|
+
class RedisCluster
|
|
27
|
+
|
|
28
|
+
RedisClusterHashSlots = 16384
|
|
29
|
+
RedisClusterRequestTTL = 16
|
|
30
|
+
RedisClusterDefaultTimeout = 1
|
|
31
|
+
|
|
32
|
+
def initialize(startup_nodes,connections,opt={})
|
|
33
|
+
@startup_nodes = startup_nodes
|
|
34
|
+
@max_connections = connections
|
|
35
|
+
@connections = {}
|
|
36
|
+
@opt = opt
|
|
37
|
+
@refresh_table_asap = false
|
|
38
|
+
initialize_slots_cache
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def get_redis_link(host,port)
|
|
42
|
+
timeout = @opt[:timeout] or RedisClusterDefaultTimeout
|
|
43
|
+
Redis.new(:host => host, :port => port, :timeout => timeout)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Given a node (that is just a Ruby hash) give it a name just
|
|
47
|
+
# concatenating the host and port. We use the node name as a key
|
|
48
|
+
# to cache connections to that node.
|
|
49
|
+
def set_node_name!(n)
|
|
50
|
+
if !n[:name]
|
|
51
|
+
n[:name] = "#{n[:host]}:#{n[:port]}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Contact the startup nodes and try to fetch the hash slots -> instances
|
|
56
|
+
# map in order to initialize the @slots hash.
|
|
57
|
+
def initialize_slots_cache
|
|
58
|
+
@startup_nodes.each{|n|
|
|
59
|
+
begin
|
|
60
|
+
@slots = {}
|
|
61
|
+
@nodes = []
|
|
62
|
+
|
|
63
|
+
r = get_redis_link(n[:host],n[:port])
|
|
64
|
+
r.cluster("slots").each {|r|
|
|
65
|
+
(r[0]..r[1]).each{|slot|
|
|
66
|
+
ip,port = r[2]
|
|
67
|
+
name = "#{ip}:#{port}"
|
|
68
|
+
node = {
|
|
69
|
+
:host => ip, :port => port,
|
|
70
|
+
:name => name
|
|
71
|
+
}
|
|
72
|
+
@nodes << node
|
|
73
|
+
@slots[slot] = node
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
populate_startup_nodes
|
|
77
|
+
@refresh_table_asap = false
|
|
78
|
+
rescue
|
|
79
|
+
# Try with the next node on error.
|
|
80
|
+
next
|
|
81
|
+
end
|
|
82
|
+
# Exit the loop as long as the first node replies
|
|
83
|
+
break
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Use @nodes to populate @startup_nodes, so that we have more chances
|
|
88
|
+
# if a subset of the cluster fails.
|
|
89
|
+
def populate_startup_nodes
|
|
90
|
+
# Make sure every node has already a name, so that later the
|
|
91
|
+
# Array uniq! method will work reliably.
|
|
92
|
+
@startup_nodes.each{|n| set_node_name! n}
|
|
93
|
+
@nodes.each{|n| @startup_nodes << n}
|
|
94
|
+
@startup_nodes.uniq!
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Flush the cache, mostly useful for debugging when we want to force
|
|
98
|
+
# redirection.
|
|
99
|
+
def flush_slots_cache
|
|
100
|
+
@slots = {}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Return the hash slot from the key.
|
|
104
|
+
def keyslot(key)
|
|
105
|
+
# Only hash what is inside {...} if there is such a pattern in the key.
|
|
106
|
+
# Note that the specification requires the content that is between
|
|
107
|
+
# the first { and the first } after the first {. If we found {} without
|
|
108
|
+
# nothing in the middle, the whole key is hashed as usually.
|
|
109
|
+
s = key.index "{"
|
|
110
|
+
if s
|
|
111
|
+
e = key.index "}",s+1
|
|
112
|
+
if e && e != s+1
|
|
113
|
+
key = key[s+1..e-1]
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
RedisClusterCRC16.crc16(key) % RedisClusterHashSlots
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Return the first key in the command arguments.
|
|
120
|
+
#
|
|
121
|
+
# Currently we just return argv[1], that is, the first argument
|
|
122
|
+
# after the command name.
|
|
123
|
+
#
|
|
124
|
+
# This is indeed the key for most commands, and when it is not true
|
|
125
|
+
# the cluster redirection will point us to the right node anyway.
|
|
126
|
+
#
|
|
127
|
+
# For commands we want to explicitly bad as they don't make sense
|
|
128
|
+
# in the context of cluster, nil is returned.
|
|
129
|
+
def get_key_from_command(argv)
|
|
130
|
+
case argv[0].to_s.downcase
|
|
131
|
+
when "info","multi","exec","slaveof","config","shutdown"
|
|
132
|
+
return nil
|
|
133
|
+
else
|
|
134
|
+
# Unknown commands, and all the commands having the key
|
|
135
|
+
# as first argument are handled here:
|
|
136
|
+
# set, get, ...
|
|
137
|
+
return argv[1]
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# If the current number of connections is already the maximum number
|
|
142
|
+
# allowed, close a random connection. This should be called every time
|
|
143
|
+
# we cache a new connection in the @connections hash.
|
|
144
|
+
def close_existing_connection
|
|
145
|
+
while @connections.length >= @max_connections
|
|
146
|
+
@connections.each{|n,r|
|
|
147
|
+
@connections.delete(n)
|
|
148
|
+
begin
|
|
149
|
+
r.client.disconnect
|
|
150
|
+
rescue
|
|
151
|
+
end
|
|
152
|
+
break
|
|
153
|
+
}
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Return a link to a random node, or raise an error if no node can be
|
|
158
|
+
# contacted. This function is only called when we can't reach the node
|
|
159
|
+
# associated with a given hash slot, or when we don't know the right
|
|
160
|
+
# mapping.
|
|
161
|
+
#
|
|
162
|
+
# The function will try to get a successful reply to the PING command,
|
|
163
|
+
# otherwise the next node is tried.
|
|
164
|
+
def get_random_connection
|
|
165
|
+
e = ""
|
|
166
|
+
@startup_nodes.shuffle.each{|n|
|
|
167
|
+
begin
|
|
168
|
+
set_node_name!(n)
|
|
169
|
+
conn = @connections[n[:name]]
|
|
170
|
+
|
|
171
|
+
if !conn
|
|
172
|
+
# Connect the node if it is not connected
|
|
173
|
+
conn = get_redis_link(n[:host],n[:port])
|
|
174
|
+
if conn.ping == "PONG"
|
|
175
|
+
close_existing_connection
|
|
176
|
+
@connections[n[:name]] = conn
|
|
177
|
+
return conn
|
|
178
|
+
else
|
|
179
|
+
# If the connection is not good close it ASAP in order
|
|
180
|
+
# to avoid waiting for the GC finalizer. File
|
|
181
|
+
# descriptors are a rare resource.
|
|
182
|
+
conn.client.disconnect
|
|
183
|
+
end
|
|
184
|
+
else
|
|
185
|
+
# The node was already connected, test the connection.
|
|
186
|
+
return conn if conn.ping == "PONG"
|
|
187
|
+
end
|
|
188
|
+
rescue => e
|
|
189
|
+
# Just try with the next node.
|
|
190
|
+
end
|
|
191
|
+
}
|
|
192
|
+
raise "Can't reach a single startup node. #{e}"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Given a slot return the link (Redis instance) to the mapped node.
|
|
196
|
+
# Make sure to create a connection with the node if we don't have
|
|
197
|
+
# one.
|
|
198
|
+
def get_connection_by_slot(slot)
|
|
199
|
+
node = @slots[slot]
|
|
200
|
+
# If we don't know what the mapping is, return a random node.
|
|
201
|
+
return get_random_connection if !node
|
|
202
|
+
set_node_name!(node)
|
|
203
|
+
if not @connections[node[:name]]
|
|
204
|
+
begin
|
|
205
|
+
close_existing_connection
|
|
206
|
+
@connections[node[:name]] =
|
|
207
|
+
get_redis_link(node[:host],node[:port])
|
|
208
|
+
rescue
|
|
209
|
+
# This will probably never happen with recent redis-rb
|
|
210
|
+
# versions because the connection is enstablished in a lazy
|
|
211
|
+
# way only when a command is called. However it is wise to
|
|
212
|
+
# handle an instance creation error of some kind.
|
|
213
|
+
return get_random_connection
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
@connections[node[:name]]
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Dispatch commands.
|
|
220
|
+
def send_cluster_command(argv)
|
|
221
|
+
initialize_slots_cache if @refresh_table_asap
|
|
222
|
+
ttl = RedisClusterRequestTTL; # Max number of redirections
|
|
223
|
+
e = ""
|
|
224
|
+
asking = false
|
|
225
|
+
try_random_node = false
|
|
226
|
+
while ttl > 0
|
|
227
|
+
ttl -= 1
|
|
228
|
+
key = get_key_from_command(argv)
|
|
229
|
+
raise "No way to dispatch this command to Redis Cluster." if !key
|
|
230
|
+
slot = keyslot(key)
|
|
231
|
+
if try_random_node
|
|
232
|
+
r = get_random_connection
|
|
233
|
+
try_random_node = false
|
|
234
|
+
else
|
|
235
|
+
r = get_connection_by_slot(slot)
|
|
236
|
+
end
|
|
237
|
+
begin
|
|
238
|
+
# TODO: use pipelining to send asking and save a rtt.
|
|
239
|
+
r.asking if asking
|
|
240
|
+
asking = false
|
|
241
|
+
return r.send(argv[0].to_sym,*argv[1..-1])
|
|
242
|
+
rescue Errno::ECONNREFUSED, Redis::TimeoutError, Redis::CannotConnectError, Errno::EACCES
|
|
243
|
+
try_random_node = true
|
|
244
|
+
sleep(0.1) if ttl < RedisClusterRequestTTL/2
|
|
245
|
+
rescue => e
|
|
246
|
+
errv = e.to_s.split
|
|
247
|
+
if errv[0] == "MOVED" || errv[0] == "ASK"
|
|
248
|
+
if errv[0] == "ASK"
|
|
249
|
+
asking = true
|
|
250
|
+
else
|
|
251
|
+
# Serve replied with MOVED. It's better for us to
|
|
252
|
+
# ask for CLUSTER NODES the next time.
|
|
253
|
+
@refresh_table_asap = true
|
|
254
|
+
end
|
|
255
|
+
newslot = errv[1].to_i
|
|
256
|
+
node_ip,node_port = errv[2].split(":")
|
|
257
|
+
if !asking
|
|
258
|
+
@slots[newslot] = {:host => node_ip,
|
|
259
|
+
:port => node_port.to_i}
|
|
260
|
+
end
|
|
261
|
+
else
|
|
262
|
+
raise e
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
raise "Too many Cluster redirections? (last error: #{e})"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Currently we handle all the commands using method_missing for
|
|
270
|
+
# simplicity. For a Cluster client actually it will be better to have
|
|
271
|
+
# every single command as a method with the right arity and possibly
|
|
272
|
+
# additional checks (example: RPOPLPUSH with same src/dst key, SORT
|
|
273
|
+
# without GET or BY, and so forth).
|
|
274
|
+
def method_missing(*argv)
|
|
275
|
+
send_cluster_command(argv)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
metadata
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rb-cluster-mm
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ysh1986
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2017-06-27 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: redis cluster connect
|
|
14
|
+
email: ysh198606@gmail.com
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- lib/crc16.rb
|
|
20
|
+
- lib/rediscluster.rb
|
|
21
|
+
homepage: https://rubygems.org/profiles/ysh1986
|
|
22
|
+
licenses:
|
|
23
|
+
- MIT
|
|
24
|
+
metadata: {}
|
|
25
|
+
post_install_message:
|
|
26
|
+
rdoc_options: []
|
|
27
|
+
require_paths:
|
|
28
|
+
- lib
|
|
29
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '0'
|
|
39
|
+
requirements: []
|
|
40
|
+
rubyforge_project:
|
|
41
|
+
rubygems_version: 2.6.11
|
|
42
|
+
signing_key:
|
|
43
|
+
specification_version: 4
|
|
44
|
+
summary: rb-cluster-mm
|
|
45
|
+
test_files: []
|