redis 3.2.2 → 4.4.0

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.
Files changed (118) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +175 -13
  3. data/README.md +223 -76
  4. data/lib/redis.rb +1360 -445
  5. data/lib/redis/client.rb +183 -103
  6. data/lib/redis/cluster.rb +291 -0
  7. data/lib/redis/cluster/command.rb +81 -0
  8. data/lib/redis/cluster/command_loader.rb +34 -0
  9. data/lib/redis/cluster/key_slot_converter.rb +72 -0
  10. data/lib/redis/cluster/node.rb +108 -0
  11. data/lib/redis/cluster/node_key.rb +31 -0
  12. data/lib/redis/cluster/node_loader.rb +37 -0
  13. data/lib/redis/cluster/option.rb +93 -0
  14. data/lib/redis/cluster/slot.rb +86 -0
  15. data/lib/redis/cluster/slot_loader.rb +49 -0
  16. data/lib/redis/connection.rb +4 -2
  17. data/lib/redis/connection/command_helper.rb +5 -10
  18. data/lib/redis/connection/hiredis.rb +9 -6
  19. data/lib/redis/connection/registry.rb +2 -1
  20. data/lib/redis/connection/ruby.rb +168 -63
  21. data/lib/redis/connection/synchrony.rb +29 -7
  22. data/lib/redis/distributed.rb +156 -74
  23. data/lib/redis/errors.rb +48 -0
  24. data/lib/redis/hash_ring.rb +30 -73
  25. data/lib/redis/pipeline.rb +55 -15
  26. data/lib/redis/subscribe.rb +20 -13
  27. data/lib/redis/version.rb +3 -1
  28. metadata +41 -170
  29. data/.gitignore +0 -16
  30. data/.travis.yml +0 -59
  31. data/.travis/Gemfile +0 -11
  32. data/.yardopts +0 -3
  33. data/Gemfile +0 -4
  34. data/Rakefile +0 -87
  35. data/benchmarking/logging.rb +0 -71
  36. data/benchmarking/pipeline.rb +0 -51
  37. data/benchmarking/speed.rb +0 -21
  38. data/benchmarking/suite.rb +0 -24
  39. data/benchmarking/worker.rb +0 -71
  40. data/examples/basic.rb +0 -15
  41. data/examples/consistency.rb +0 -114
  42. data/examples/dist_redis.rb +0 -43
  43. data/examples/incr-decr.rb +0 -17
  44. data/examples/list.rb +0 -26
  45. data/examples/pubsub.rb +0 -37
  46. data/examples/sentinel.rb +0 -41
  47. data/examples/sentinel/sentinel.conf +0 -9
  48. data/examples/sentinel/start +0 -49
  49. data/examples/sets.rb +0 -36
  50. data/examples/unicorn/config.ru +0 -3
  51. data/examples/unicorn/unicorn.rb +0 -20
  52. data/redis.gemspec +0 -44
  53. data/test/bitpos_test.rb +0 -69
  54. data/test/blocking_commands_test.rb +0 -42
  55. data/test/command_map_test.rb +0 -30
  56. data/test/commands_on_hashes_test.rb +0 -21
  57. data/test/commands_on_hyper_log_log_test.rb +0 -21
  58. data/test/commands_on_lists_test.rb +0 -20
  59. data/test/commands_on_sets_test.rb +0 -77
  60. data/test/commands_on_sorted_sets_test.rb +0 -137
  61. data/test/commands_on_strings_test.rb +0 -101
  62. data/test/commands_on_value_types_test.rb +0 -133
  63. data/test/connection_handling_test.rb +0 -250
  64. data/test/db/.gitkeep +0 -0
  65. data/test/distributed_blocking_commands_test.rb +0 -46
  66. data/test/distributed_commands_on_hashes_test.rb +0 -10
  67. data/test/distributed_commands_on_hyper_log_log_test.rb +0 -33
  68. data/test/distributed_commands_on_lists_test.rb +0 -22
  69. data/test/distributed_commands_on_sets_test.rb +0 -83
  70. data/test/distributed_commands_on_sorted_sets_test.rb +0 -18
  71. data/test/distributed_commands_on_strings_test.rb +0 -59
  72. data/test/distributed_commands_on_value_types_test.rb +0 -95
  73. data/test/distributed_commands_requiring_clustering_test.rb +0 -164
  74. data/test/distributed_connection_handling_test.rb +0 -23
  75. data/test/distributed_internals_test.rb +0 -79
  76. data/test/distributed_key_tags_test.rb +0 -52
  77. data/test/distributed_persistence_control_commands_test.rb +0 -26
  78. data/test/distributed_publish_subscribe_test.rb +0 -92
  79. data/test/distributed_remote_server_control_commands_test.rb +0 -66
  80. data/test/distributed_scripting_test.rb +0 -102
  81. data/test/distributed_sorting_test.rb +0 -20
  82. data/test/distributed_test.rb +0 -58
  83. data/test/distributed_transactions_test.rb +0 -32
  84. data/test/encoding_test.rb +0 -18
  85. data/test/error_replies_test.rb +0 -59
  86. data/test/fork_safety_test.rb +0 -65
  87. data/test/helper.rb +0 -232
  88. data/test/helper_test.rb +0 -24
  89. data/test/internals_test.rb +0 -437
  90. data/test/lint/blocking_commands.rb +0 -150
  91. data/test/lint/hashes.rb +0 -162
  92. data/test/lint/hyper_log_log.rb +0 -60
  93. data/test/lint/lists.rb +0 -143
  94. data/test/lint/sets.rb +0 -125
  95. data/test/lint/sorted_sets.rb +0 -316
  96. data/test/lint/strings.rb +0 -260
  97. data/test/lint/value_types.rb +0 -122
  98. data/test/persistence_control_commands_test.rb +0 -26
  99. data/test/pipelining_commands_test.rb +0 -242
  100. data/test/publish_subscribe_test.rb +0 -254
  101. data/test/remote_server_control_commands_test.rb +0 -118
  102. data/test/scanning_test.rb +0 -413
  103. data/test/scripting_test.rb +0 -78
  104. data/test/sentinel_command_test.rb +0 -80
  105. data/test/sentinel_test.rb +0 -255
  106. data/test/sorting_test.rb +0 -59
  107. data/test/support/connection/hiredis.rb +0 -1
  108. data/test/support/connection/ruby.rb +0 -1
  109. data/test/support/connection/synchrony.rb +0 -17
  110. data/test/support/redis_mock.rb +0 -119
  111. data/test/support/wire/synchrony.rb +0 -24
  112. data/test/support/wire/thread.rb +0 -5
  113. data/test/synchrony_driver.rb +0 -88
  114. data/test/test.conf.erb +0 -9
  115. data/test/thread_safety_test.rb +0 -32
  116. data/test/transactions_test.rb +0 -264
  117. data/test/unknown_commands_test.rb +0 -14
  118. data/test/url_param_test.rb +0 -138
@@ -0,0 +1,291 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'errors'
4
+ require_relative 'client'
5
+ require_relative 'cluster/command'
6
+ require_relative 'cluster/command_loader'
7
+ require_relative 'cluster/key_slot_converter'
8
+ require_relative 'cluster/node'
9
+ require_relative 'cluster/node_key'
10
+ require_relative 'cluster/node_loader'
11
+ require_relative 'cluster/option'
12
+ require_relative 'cluster/slot'
13
+ require_relative 'cluster/slot_loader'
14
+
15
+ class Redis
16
+ # Redis Cluster client
17
+ #
18
+ # @see https://github.com/antirez/redis-rb-cluster POC implementation
19
+ # @see https://redis.io/topics/cluster-spec Redis Cluster specification
20
+ # @see https://redis.io/topics/cluster-tutorial Redis Cluster tutorial
21
+ #
22
+ # Copyright (C) 2013 Salvatore Sanfilippo <antirez@gmail.com>
23
+ class Cluster
24
+ def initialize(options = {})
25
+ @option = Option.new(options)
26
+ @node, @slot = fetch_cluster_info!(@option)
27
+ @command = fetch_command_details(@node)
28
+ end
29
+
30
+ def id
31
+ @node.map(&:id).sort.join(' ')
32
+ end
33
+
34
+ # db feature is disabled in cluster mode
35
+ def db
36
+ 0
37
+ end
38
+
39
+ # db feature is disabled in cluster mode
40
+ def db=(_db); end
41
+
42
+ def timeout
43
+ @node.first.timeout
44
+ end
45
+
46
+ def connected?
47
+ @node.any?(&:connected?)
48
+ end
49
+
50
+ def disconnect
51
+ @node.each(&:disconnect)
52
+ true
53
+ end
54
+
55
+ def connection_info
56
+ @node.sort_by(&:id).map do |client|
57
+ {
58
+ host: client.host,
59
+ port: client.port,
60
+ db: client.db,
61
+ id: client.id,
62
+ location: client.location
63
+ }
64
+ end
65
+ end
66
+
67
+ def with_reconnect(val = true, &block)
68
+ try_send(@node.sample, :with_reconnect, val, &block)
69
+ end
70
+
71
+ def call(command, &block)
72
+ send_command(command, &block)
73
+ end
74
+
75
+ def call_loop(command, timeout = 0, &block)
76
+ node = assign_node(command)
77
+ try_send(node, :call_loop, command, timeout, &block)
78
+ end
79
+
80
+ def call_pipeline(pipeline)
81
+ node_keys = pipeline.commands.map { |cmd| find_node_key(cmd, primary_only: true) }.compact.uniq
82
+ if node_keys.size > 1
83
+ raise(CrossSlotPipeliningError,
84
+ pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?).uniq)
85
+ end
86
+
87
+ try_send(find_node(node_keys.first), :call_pipeline, pipeline)
88
+ end
89
+
90
+ def call_with_timeout(command, timeout, &block)
91
+ node = assign_node(command)
92
+ try_send(node, :call_with_timeout, command, timeout, &block)
93
+ end
94
+
95
+ def call_without_timeout(command, &block)
96
+ call_with_timeout(command, 0, &block)
97
+ end
98
+
99
+ def process(commands, &block)
100
+ if commands.size == 1 &&
101
+ %w[unsubscribe punsubscribe].include?(commands.first.first.to_s.downcase) &&
102
+ commands.first.size == 1
103
+
104
+ # Node is indeterminate. We do just a best-effort try here.
105
+ @node.process_all(commands, &block)
106
+ else
107
+ node = assign_node(commands.first)
108
+ try_send(node, :process, commands, &block)
109
+ end
110
+ end
111
+
112
+ private
113
+
114
+ def fetch_cluster_info!(option)
115
+ node = Node.new(option.per_node_key)
116
+ available_slots = SlotLoader.load(node)
117
+ node_flags = NodeLoader.load_flags(node)
118
+ option.update_node(available_slots.keys.map { |k| NodeKey.optionize(k) })
119
+ [Node.new(option.per_node_key, node_flags, option.use_replica?),
120
+ Slot.new(available_slots, node_flags, option.use_replica?)]
121
+ ensure
122
+ node&.each(&:disconnect)
123
+ end
124
+
125
+ def fetch_command_details(nodes)
126
+ details = CommandLoader.load(nodes)
127
+ Command.new(details)
128
+ end
129
+
130
+ def send_command(command, &block)
131
+ cmd = command.first.to_s.downcase
132
+ case cmd
133
+ when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
134
+ @node.call_all(command, &block).first
135
+ when 'flushall', 'flushdb'
136
+ @node.call_master(command, &block).first
137
+ when 'wait' then @node.call_master(command, &block).reduce(:+)
138
+ when 'keys' then @node.call_slave(command, &block).flatten.sort
139
+ when 'dbsize' then @node.call_slave(command, &block).reduce(:+)
140
+ when 'lastsave' then @node.call_all(command, &block).sort
141
+ when 'role' then @node.call_all(command, &block)
142
+ when 'config' then send_config_command(command, &block)
143
+ when 'client' then send_client_command(command, &block)
144
+ when 'cluster' then send_cluster_command(command, &block)
145
+ when 'readonly', 'readwrite', 'shutdown'
146
+ raise OrchestrationCommandNotSupported, cmd
147
+ when 'memory' then send_memory_command(command, &block)
148
+ when 'script' then send_script_command(command, &block)
149
+ when 'pubsub' then send_pubsub_command(command, &block)
150
+ when 'discard', 'exec', 'multi', 'unwatch'
151
+ raise AmbiguousNodeError, cmd
152
+ else
153
+ node = assign_node(command)
154
+ try_send(node, :call, command, &block)
155
+ end
156
+ end
157
+
158
+ def send_config_command(command, &block)
159
+ case command[1].to_s.downcase
160
+ when 'resetstat', 'rewrite', 'set'
161
+ @node.call_all(command, &block).first
162
+ else assign_node(command).call(command, &block)
163
+ end
164
+ end
165
+
166
+ def send_memory_command(command, &block)
167
+ case command[1].to_s.downcase
168
+ when 'stats' then @node.call_all(command, &block)
169
+ when 'purge' then @node.call_all(command, &block).first
170
+ else assign_node(command).call(command, &block)
171
+ end
172
+ end
173
+
174
+ def send_client_command(command, &block)
175
+ case command[1].to_s.downcase
176
+ when 'list' then @node.call_all(command, &block).flatten
177
+ when 'pause', 'reply', 'setname'
178
+ @node.call_all(command, &block).first
179
+ else assign_node(command).call(command, &block)
180
+ end
181
+ end
182
+
183
+ def send_cluster_command(command, &block)
184
+ subcommand = command[1].to_s.downcase
185
+ case subcommand
186
+ when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
187
+ 'reset', 'set-config-epoch', 'setslot'
188
+ raise OrchestrationCommandNotSupported, 'cluster', subcommand
189
+ when 'saveconfig' then @node.call_all(command, &block).first
190
+ else assign_node(command).call(command, &block)
191
+ end
192
+ end
193
+
194
+ def send_script_command(command, &block)
195
+ case command[1].to_s.downcase
196
+ when 'debug', 'kill'
197
+ @node.call_all(command, &block).first
198
+ when 'flush', 'load'
199
+ @node.call_master(command, &block).first
200
+ else assign_node(command).call(command, &block)
201
+ end
202
+ end
203
+
204
+ def send_pubsub_command(command, &block)
205
+ case command[1].to_s.downcase
206
+ when 'channels' then @node.call_all(command, &block).flatten.uniq.sort
207
+ when 'numsub'
208
+ @node.call_all(command, &block).reject(&:empty?).map { |e| Hash[*e] }
209
+ .reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }
210
+ when 'numpat' then @node.call_all(command, &block).reduce(:+)
211
+ else assign_node(command).call(command, &block)
212
+ end
213
+ end
214
+
215
+ # @see https://redis.io/topics/cluster-spec#redirection-and-resharding
216
+ # Redirection and resharding
217
+ def try_send(node, method_name, *args, retry_count: 3, &block)
218
+ node.public_send(method_name, *args, &block)
219
+ rescue CommandError => err
220
+ if err.message.start_with?('MOVED')
221
+ raise if retry_count <= 0
222
+
223
+ node = assign_redirection_node(err.message)
224
+ retry_count -= 1
225
+ retry
226
+ elsif err.message.start_with?('ASK')
227
+ raise if retry_count <= 0
228
+
229
+ node = assign_asking_node(err.message)
230
+ node.call(%i[asking])
231
+ retry_count -= 1
232
+ retry
233
+ else
234
+ raise
235
+ end
236
+ rescue CannotConnectError
237
+ update_cluster_info!
238
+ raise
239
+ end
240
+
241
+ def assign_redirection_node(err_msg)
242
+ _, slot, node_key = err_msg.split(' ')
243
+ slot = slot.to_i
244
+ @slot.put(slot, node_key)
245
+ find_node(node_key)
246
+ end
247
+
248
+ def assign_asking_node(err_msg)
249
+ _, _, node_key = err_msg.split(' ')
250
+ find_node(node_key)
251
+ end
252
+
253
+ def assign_node(command)
254
+ node_key = find_node_key(command)
255
+ find_node(node_key)
256
+ end
257
+
258
+ def find_node_key(command, primary_only: false)
259
+ key = @command.extract_first_key(command)
260
+ return if key.empty?
261
+
262
+ slot = KeySlotConverter.convert(key)
263
+ return unless @slot.exists?(slot)
264
+
265
+ if @command.should_send_to_master?(command) || primary_only
266
+ @slot.find_node_key_of_master(slot)
267
+ else
268
+ @slot.find_node_key_of_slave(slot)
269
+ end
270
+ end
271
+
272
+ def find_node(node_key)
273
+ return @node.sample if node_key.nil?
274
+
275
+ @node.find_by(node_key)
276
+ rescue Node::ReloadNeeded
277
+ update_cluster_info!(node_key)
278
+ @node.find_by(node_key)
279
+ end
280
+
281
+ def update_cluster_info!(node_key = nil)
282
+ unless node_key.nil?
283
+ host, port = NodeKey.split(node_key)
284
+ @option.add_node(host, port)
285
+ end
286
+
287
+ @node.map(&:disconnect)
288
+ @node, @slot = fetch_cluster_info!(@option)
289
+ end
290
+ end
291
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors'
4
+
5
+ class Redis
6
+ class Cluster
7
+ # Keep details about Redis commands for Redis Cluster Client.
8
+ # @see https://redis.io/commands/command
9
+ class Command
10
+ def initialize(details)
11
+ @details = pick_details(details)
12
+ end
13
+
14
+ def extract_first_key(command)
15
+ i = determine_first_key_position(command)
16
+ return '' if i == 0
17
+
18
+ key = command[i].to_s
19
+ hash_tag = extract_hash_tag(key)
20
+ hash_tag.empty? ? key : hash_tag
21
+ end
22
+
23
+ def should_send_to_master?(command)
24
+ dig_details(command, :write)
25
+ end
26
+
27
+ def should_send_to_slave?(command)
28
+ dig_details(command, :readonly)
29
+ end
30
+
31
+ private
32
+
33
+ def pick_details(details)
34
+ details.map do |command, detail|
35
+ [command, {
36
+ first_key_position: detail[:first],
37
+ write: detail[:flags].include?('write'),
38
+ readonly: detail[:flags].include?('readonly')
39
+ }]
40
+ end.to_h
41
+ end
42
+
43
+ def dig_details(command, key)
44
+ name = command.first.to_s
45
+ return unless @details.key?(name)
46
+
47
+ @details.fetch(name).fetch(key)
48
+ end
49
+
50
+ def determine_first_key_position(command)
51
+ case command.first.to_s.downcase
52
+ when 'eval', 'evalsha', 'migrate', 'zinterstore', 'zunionstore' then 3
53
+ when 'object' then 2
54
+ when 'memory'
55
+ command[1].to_s.casecmp('usage').zero? ? 2 : 0
56
+ when 'scan', 'sscan', 'hscan', 'zscan'
57
+ determine_optional_key_position(command, 'match')
58
+ when 'xread', 'xreadgroup'
59
+ determine_optional_key_position(command, 'streams')
60
+ else
61
+ dig_details(command, :first_key_position).to_i
62
+ end
63
+ end
64
+
65
+ def determine_optional_key_position(command, option_name)
66
+ idx = command.map(&:to_s).map(&:downcase).index(option_name)
67
+ idx.nil? ? 0 : idx + 1
68
+ end
69
+
70
+ # @see https://redis.io/topics/cluster-spec#keys-hash-tags Keys hash tags
71
+ def extract_hash_tag(key)
72
+ s = key.index('{')
73
+ e = key.index('}', s.to_i + 1)
74
+
75
+ return '' if s.nil? || e.nil?
76
+
77
+ key[s + 1..e - 1]
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors'
4
+
5
+ class Redis
6
+ class Cluster
7
+ # Load details about Redis commands for Redis Cluster Client
8
+ # @see https://redis.io/commands/command
9
+ module CommandLoader
10
+ module_function
11
+
12
+ def load(nodes)
13
+ details = {}
14
+
15
+ nodes.each do |node|
16
+ details = fetch_command_details(node)
17
+ details.empty? ? next : break
18
+ end
19
+
20
+ details
21
+ end
22
+
23
+ def fetch_command_details(node)
24
+ node.call(%i[command]).map do |reply|
25
+ [reply[0], { arity: reply[1], flags: reply[2], first: reply[3], last: reply[4], step: reply[5] }]
26
+ end.to_h
27
+ rescue CannotConnectError, ConnectionError, CommandError
28
+ {} # can retry on another node
29
+ end
30
+
31
+ private_class_method :fetch_command_details
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Redis
4
+ class Cluster
5
+ # Key to slot converter for Redis Cluster Client
6
+ #
7
+ # We can test it by `CLUSTER KEYSLOT` command.
8
+ #
9
+ # @see https://github.com/antirez/redis-rb-cluster
10
+ # Reference implementation in Ruby
11
+ # @see https://redis.io/topics/cluster-spec#appendix
12
+ # Reference implementation in ANSI C
13
+ # @see https://redis.io/commands/cluster-keyslot
14
+ # CLUSTER KEYSLOT command reference
15
+ #
16
+ # Copyright (C) 2013 Salvatore Sanfilippo <antirez@gmail.com>
17
+ module KeySlotConverter
18
+ XMODEM_CRC16_LOOKUP = [
19
+ 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
20
+ 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
21
+ 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
22
+ 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
23
+ 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
24
+ 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
25
+ 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
26
+ 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
27
+ 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
28
+ 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
29
+ 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
30
+ 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
31
+ 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
32
+ 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
33
+ 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
34
+ 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
35
+ 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
36
+ 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
37
+ 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
38
+ 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
39
+ 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
40
+ 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
41
+ 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
42
+ 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
43
+ 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
44
+ 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
45
+ 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
46
+ 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
47
+ 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
48
+ 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
49
+ 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
50
+ 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
51
+ ].freeze
52
+
53
+ HASH_SLOTS = 16_384
54
+
55
+ module_function
56
+
57
+ # Convert key into slot.
58
+ #
59
+ # @param key [String] the key of the redis command
60
+ #
61
+ # @return [Integer] slot number
62
+ def convert(key)
63
+ crc = 0
64
+ key.each_byte do |b|
65
+ crc = ((crc << 8) & 0xffff) ^ XMODEM_CRC16_LOOKUP[((crc >> 8) ^ b) & 0xff]
66
+ end
67
+
68
+ crc % HASH_SLOTS
69
+ end
70
+ end
71
+ end
72
+ end