redis 3.3.5 → 4.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (127) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.travis/Gemfile +8 -1
  4. data/.travis.yml +34 -62
  5. data/CHANGELOG.md +45 -2
  6. data/Gemfile +5 -1
  7. data/README.md +32 -76
  8. data/benchmarking/logging.rb +1 -1
  9. data/bin/build +71 -0
  10. data/bors.toml +14 -0
  11. data/lib/redis/client.rb +38 -20
  12. data/lib/redis/cluster/command.rb +81 -0
  13. data/lib/redis/cluster/command_loader.rb +32 -0
  14. data/lib/redis/cluster/key_slot_converter.rb +72 -0
  15. data/lib/redis/cluster/node.rb +104 -0
  16. data/lib/redis/cluster/node_key.rb +35 -0
  17. data/lib/redis/cluster/node_loader.rb +35 -0
  18. data/lib/redis/cluster/option.rb +76 -0
  19. data/lib/redis/cluster/slot.rb +69 -0
  20. data/lib/redis/cluster/slot_loader.rb +47 -0
  21. data/lib/redis/cluster.rb +285 -0
  22. data/lib/redis/connection/command_helper.rb +2 -8
  23. data/lib/redis/connection/hiredis.rb +2 -2
  24. data/lib/redis/connection/ruby.rb +13 -30
  25. data/lib/redis/connection/synchrony.rb +12 -4
  26. data/lib/redis/connection.rb +2 -2
  27. data/lib/redis/distributed.rb +29 -8
  28. data/lib/redis/errors.rb +46 -0
  29. data/lib/redis/hash_ring.rb +20 -64
  30. data/lib/redis/pipeline.rb +9 -7
  31. data/lib/redis/version.rb +1 -1
  32. data/lib/redis.rb +287 -52
  33. data/makefile +74 -0
  34. data/redis.gemspec +9 -10
  35. data/test/bitpos_test.rb +13 -19
  36. data/test/blocking_commands_test.rb +3 -5
  37. data/test/client_test.rb +18 -1
  38. data/test/cluster_abnormal_state_test.rb +38 -0
  39. data/test/cluster_blocking_commands_test.rb +15 -0
  40. data/test/cluster_client_internals_test.rb +77 -0
  41. data/test/cluster_client_key_hash_tags_test.rb +88 -0
  42. data/test/cluster_client_options_test.rb +147 -0
  43. data/test/cluster_client_pipelining_test.rb +59 -0
  44. data/test/cluster_client_replicas_test.rb +36 -0
  45. data/test/cluster_client_slots_test.rb +94 -0
  46. data/test/cluster_client_transactions_test.rb +71 -0
  47. data/test/cluster_commands_on_cluster_test.rb +165 -0
  48. data/test/cluster_commands_on_connection_test.rb +40 -0
  49. data/test/cluster_commands_on_geo_test.rb +74 -0
  50. data/test/cluster_commands_on_hashes_test.rb +11 -0
  51. data/test/cluster_commands_on_hyper_log_log_test.rb +17 -0
  52. data/test/cluster_commands_on_keys_test.rb +134 -0
  53. data/test/cluster_commands_on_lists_test.rb +15 -0
  54. data/test/cluster_commands_on_pub_sub_test.rb +101 -0
  55. data/test/cluster_commands_on_scripting_test.rb +56 -0
  56. data/test/cluster_commands_on_server_test.rb +221 -0
  57. data/test/cluster_commands_on_sets_test.rb +39 -0
  58. data/test/cluster_commands_on_sorted_sets_test.rb +35 -0
  59. data/test/cluster_commands_on_streams_test.rb +196 -0
  60. data/test/cluster_commands_on_strings_test.rb +15 -0
  61. data/test/cluster_commands_on_transactions_test.rb +41 -0
  62. data/test/cluster_commands_on_value_types_test.rb +14 -0
  63. data/test/command_map_test.rb +3 -5
  64. data/test/commands_on_geo_test.rb +116 -0
  65. data/test/commands_on_hashes_test.rb +2 -16
  66. data/test/commands_on_hyper_log_log_test.rb +3 -17
  67. data/test/commands_on_lists_test.rb +2 -15
  68. data/test/commands_on_sets_test.rb +2 -72
  69. data/test/commands_on_sorted_sets_test.rb +2 -132
  70. data/test/commands_on_strings_test.rb +2 -96
  71. data/test/commands_on_value_types_test.rb +80 -6
  72. data/test/connection_handling_test.rb +5 -7
  73. data/test/distributed_blocking_commands_test.rb +10 -4
  74. data/test/distributed_commands_on_hashes_test.rb +16 -5
  75. data/test/distributed_commands_on_hyper_log_log_test.rb +8 -15
  76. data/test/distributed_commands_on_lists_test.rb +4 -7
  77. data/test/distributed_commands_on_sets_test.rb +58 -36
  78. data/test/distributed_commands_on_sorted_sets_test.rb +51 -10
  79. data/test/distributed_commands_on_strings_test.rb +30 -10
  80. data/test/distributed_commands_on_value_types_test.rb +38 -4
  81. data/test/distributed_commands_requiring_clustering_test.rb +1 -3
  82. data/test/distributed_connection_handling_test.rb +1 -3
  83. data/test/distributed_internals_test.rb +8 -19
  84. data/test/distributed_key_tags_test.rb +4 -6
  85. data/test/distributed_persistence_control_commands_test.rb +1 -3
  86. data/test/distributed_publish_subscribe_test.rb +1 -3
  87. data/test/distributed_remote_server_control_commands_test.rb +1 -3
  88. data/test/distributed_scripting_test.rb +1 -3
  89. data/test/distributed_sorting_test.rb +1 -3
  90. data/test/distributed_test.rb +12 -14
  91. data/test/distributed_transactions_test.rb +1 -3
  92. data/test/encoding_test.rb +4 -8
  93. data/test/error_replies_test.rb +2 -4
  94. data/test/fork_safety_test.rb +1 -6
  95. data/test/helper.rb +179 -66
  96. data/test/helper_test.rb +1 -3
  97. data/test/internals_test.rb +47 -56
  98. data/test/lint/blocking_commands.rb +40 -16
  99. data/test/lint/hashes.rb +41 -0
  100. data/test/lint/hyper_log_log.rb +15 -1
  101. data/test/lint/lists.rb +16 -0
  102. data/test/lint/sets.rb +142 -0
  103. data/test/lint/sorted_sets.rb +183 -2
  104. data/test/lint/strings.rb +108 -20
  105. data/test/lint/value_types.rb +8 -0
  106. data/test/persistence_control_commands_test.rb +1 -3
  107. data/test/pipelining_commands_test.rb +12 -8
  108. data/test/publish_subscribe_test.rb +1 -3
  109. data/test/remote_server_control_commands_test.rb +60 -3
  110. data/test/scanning_test.rb +1 -7
  111. data/test/scripting_test.rb +1 -3
  112. data/test/sentinel_command_test.rb +1 -3
  113. data/test/sentinel_test.rb +1 -3
  114. data/test/sorting_test.rb +1 -3
  115. data/test/ssl_test.rb +45 -49
  116. data/test/support/cluster/orchestrator.rb +199 -0
  117. data/test/support/connection/hiredis.rb +1 -1
  118. data/test/support/connection/ruby.rb +1 -1
  119. data/test/support/connection/synchrony.rb +1 -1
  120. data/test/support/redis_mock.rb +1 -1
  121. data/test/synchrony_driver.rb +6 -9
  122. data/test/thread_safety_test.rb +1 -3
  123. data/test/transactions_test.rb +11 -3
  124. data/test/unknown_commands_test.rb +1 -3
  125. data/test/url_param_test.rb +44 -46
  126. metadata +109 -16
  127. data/Rakefile +0 -87
@@ -0,0 +1,285 @@
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, command_keys = extract_keys_in_pipeline(pipeline)
82
+ raise CrossSlotPipeliningError, command_keys if node_keys.size > 1
83
+ node = find_node(node_keys.first)
84
+ try_send(node, :call_pipeline, pipeline)
85
+ end
86
+
87
+ def call_with_timeout(command, timeout, &block)
88
+ node = assign_node(command)
89
+ try_send(node, :call_with_timeout, command, timeout, &block)
90
+ end
91
+
92
+ def call_without_timeout(command, &block)
93
+ call_with_timeout(command, 0, &block)
94
+ end
95
+
96
+ def process(commands, &block)
97
+ if commands.size == 1 &&
98
+ %w[unsubscribe punsubscribe].include?(commands.first.first.to_s.downcase) &&
99
+ commands.first.size == 1
100
+
101
+ # Node is indeterminate. We do just a best-effort try here.
102
+ @node.process_all(commands, &block)
103
+ else
104
+ node = assign_node(commands.first)
105
+ try_send(node, :process, commands, &block)
106
+ end
107
+ end
108
+
109
+ private
110
+
111
+ def fetch_cluster_info!(option)
112
+ node = Node.new(option.per_node_key)
113
+ available_slots = SlotLoader.load(node)
114
+ node_flags = NodeLoader.load_flags(node)
115
+ available_node_urls = NodeKey.to_node_urls(available_slots.keys, secure: option.secure?)
116
+ option.update_node(available_node_urls)
117
+ [Node.new(option.per_node_key, node_flags, option.use_replica?),
118
+ Slot.new(available_slots, node_flags, option.use_replica?)]
119
+ ensure
120
+ node.map(&:disconnect)
121
+ end
122
+
123
+ def fetch_command_details(nodes)
124
+ details = CommandLoader.load(nodes)
125
+ Command.new(details)
126
+ end
127
+
128
+ def send_command(command, &block)
129
+ cmd = command.first.to_s.downcase
130
+ case cmd
131
+ when 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
132
+ @node.call_all(command, &block).first
133
+ when 'flushall', 'flushdb'
134
+ @node.call_master(command, &block).first
135
+ when 'keys' then @node.call_slave(command, &block).flatten.sort
136
+ when 'dbsize' then @node.call_slave(command, &block).reduce(:+)
137
+ when 'lastsave' then @node.call_all(command, &block).sort
138
+ when 'role' then @node.call_all(command, &block)
139
+ when 'config' then send_config_command(command, &block)
140
+ when 'client' then send_client_command(command, &block)
141
+ when 'cluster' then send_cluster_command(command, &block)
142
+ when 'readonly', 'readwrite', 'shutdown'
143
+ raise OrchestrationCommandNotSupported, cmd
144
+ when 'memory' then send_memory_command(command, &block)
145
+ when 'script' then send_script_command(command, &block)
146
+ when 'pubsub' then send_pubsub_command(command, &block)
147
+ when 'discard', 'exec', 'multi', 'unwatch'
148
+ raise AmbiguousNodeError, cmd
149
+ else
150
+ node = assign_node(command)
151
+ try_send(node, :call, command, &block)
152
+ end
153
+ end
154
+
155
+ def send_config_command(command, &block)
156
+ case command[1].to_s.downcase
157
+ when 'resetstat', 'rewrite', 'set'
158
+ @node.call_all(command, &block).first
159
+ else assign_node(command).call(command, &block)
160
+ end
161
+ end
162
+
163
+ def send_memory_command(command, &block)
164
+ case command[1].to_s.downcase
165
+ when 'stats' then @node.call_all(command, &block)
166
+ when 'purge' then @node.call_all(command, &block).first
167
+ else assign_node(command).call(command, &block)
168
+ end
169
+ end
170
+
171
+ def send_client_command(command, &block)
172
+ case command[1].to_s.downcase
173
+ when 'list' then @node.call_all(command, &block).flatten
174
+ when 'pause', 'reply', 'setname'
175
+ @node.call_all(command, &block).first
176
+ else assign_node(command).call(command, &block)
177
+ end
178
+ end
179
+
180
+ def send_cluster_command(command, &block)
181
+ subcommand = command[1].to_s.downcase
182
+ case subcommand
183
+ when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
184
+ 'reset', 'set-config-epoch', 'setslot'
185
+ raise OrchestrationCommandNotSupported, 'cluster', subcommand
186
+ when 'saveconfig' then @node.call_all(command, &block).first
187
+ else assign_node(command).call(command, &block)
188
+ end
189
+ end
190
+
191
+ def send_script_command(command, &block)
192
+ case command[1].to_s.downcase
193
+ when 'debug', 'kill'
194
+ @node.call_all(command, &block).first
195
+ when 'flush', 'load'
196
+ @node.call_master(command, &block).first
197
+ else assign_node(command).call(command, &block)
198
+ end
199
+ end
200
+
201
+ def send_pubsub_command(command, &block)
202
+ case command[1].to_s.downcase
203
+ when 'channels' then @node.call_all(command, &block).flatten.uniq.sort
204
+ when 'numsub'
205
+ @node.call_all(command, &block).reject(&:empty?).map { |e| Hash[*e] }
206
+ .reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }
207
+ when 'numpat' then @node.call_all(command, &block).reduce(:+)
208
+ else assign_node(command).call(command, &block)
209
+ end
210
+ end
211
+
212
+ # @see https://redis.io/topics/cluster-spec#redirection-and-resharding
213
+ # Redirection and resharding
214
+ def try_send(node, method_name, *args, retry_count: 3, &block)
215
+ node.public_send(method_name, *args, &block)
216
+ rescue CommandError => err
217
+ if err.message.start_with?('MOVED')
218
+ assign_redirection_node(err.message).public_send(method_name, *args, &block)
219
+ elsif err.message.start_with?('ASK')
220
+ raise if retry_count <= 0
221
+ node = assign_asking_node(err.message)
222
+ node.call(%i[asking])
223
+ retry_count -= 1
224
+ retry
225
+ else
226
+ raise
227
+ end
228
+ end
229
+
230
+ def assign_redirection_node(err_msg)
231
+ _, slot, node_key = err_msg.split(' ')
232
+ slot = slot.to_i
233
+ @slot.put(slot, node_key)
234
+ find_node(node_key)
235
+ end
236
+
237
+ def assign_asking_node(err_msg)
238
+ _, _, node_key = err_msg.split(' ')
239
+ find_node(node_key)
240
+ end
241
+
242
+ def assign_node(command)
243
+ node_key = find_node_key(command)
244
+ find_node(node_key)
245
+ end
246
+
247
+ def find_node_key(command)
248
+ key = @command.extract_first_key(command)
249
+ return if key.empty?
250
+
251
+ slot = KeySlotConverter.convert(key)
252
+ return unless @slot.exists?(slot)
253
+
254
+ if @command.should_send_to_master?(command)
255
+ @slot.find_node_key_of_master(slot)
256
+ else
257
+ @slot.find_node_key_of_slave(slot)
258
+ end
259
+ end
260
+
261
+ def find_node(node_key)
262
+ return @node.sample if node_key.nil?
263
+ @node.find_by(node_key)
264
+ rescue Node::ReloadNeeded
265
+ update_cluster_info!(node_key)
266
+ @node.find_by(node_key)
267
+ end
268
+
269
+ def update_cluster_info!(node_key = nil)
270
+ unless node_key.nil?
271
+ host, port = NodeKey.split(node_key)
272
+ @option.add_node(host, port)
273
+ end
274
+
275
+ @node.map(&:disconnect)
276
+ @node, @slot = fetch_cluster_info!(@option)
277
+ end
278
+
279
+ def extract_keys_in_pipeline(pipeline)
280
+ node_keys = pipeline.commands.map { |cmd| find_node_key(cmd) }.compact.uniq
281
+ command_keys = pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?)
282
+ [node_keys, command_keys]
283
+ end
284
+ end
285
+ end
@@ -30,14 +30,8 @@ class Redis
30
30
 
31
31
  protected
32
32
 
33
- if defined?(Encoding::default_external)
34
- def encode(string)
35
- string.force_encoding(Encoding::default_external)
36
- end
37
- else
38
- def encode(string)
39
- string
40
- end
33
+ def encode(string)
34
+ string.force_encoding(Encoding.default_external)
41
35
  end
42
36
  end
43
37
  end
@@ -1,5 +1,5 @@
1
- require "redis/connection/registry"
2
- require "redis/errors"
1
+ require_relative "registry"
2
+ require_relative "../errors"
3
3
  require "hiredis/connection"
4
4
  require "timeout"
5
5
 
@@ -1,6 +1,6 @@
1
- require "redis/connection/registry"
2
- require "redis/connection/command_helper"
3
- require "redis/errors"
1
+ require_relative "registry"
2
+ require_relative "command_helper"
3
+ require_relative "../errors"
4
4
  require "socket"
5
5
  require "timeout"
6
6
 
@@ -10,36 +10,17 @@ rescue LoadError
10
10
  # Not all systems have OpenSSL support
11
11
  end
12
12
 
13
- if RUBY_VERSION < "1.9.3"
14
- class String
15
- # Ruby 1.8.7 does not have byteslice, but it handles encodings differently anyway.
16
- # We can simply slice the string, which is a byte array there.
17
- def byteslice(*args)
18
- slice(*args)
19
- end
20
- end
21
- end
22
-
23
13
  class Redis
24
14
  module Connection
25
15
  module SocketMixin
26
16
 
27
17
  CRLF = "\r\n".freeze
28
18
 
29
- # Exceptions raised during non-blocking I/O ops that require retrying the op
30
- if RUBY_VERSION >= "1.9.3"
31
- NBIO_READ_EXCEPTIONS = [IO::WaitReadable]
32
- NBIO_WRITE_EXCEPTIONS = [IO::WaitWritable]
33
- else
34
- NBIO_READ_EXCEPTIONS = [Errno::EWOULDBLOCK, Errno::EAGAIN]
35
- NBIO_WRITE_EXCEPTIONS = [Errno::EWOULDBLOCK, Errno::EAGAIN]
36
- end
37
-
38
19
  def initialize(*args)
39
20
  super(*args)
40
21
 
41
22
  @timeout = @write_timeout = nil
42
- @buffer = ""
23
+ @buffer = "".dup
43
24
  end
44
25
 
45
26
  def timeout=(timeout)
@@ -83,13 +64,13 @@ class Redis
83
64
  begin
84
65
  read_nonblock(nbytes)
85
66
 
86
- rescue *NBIO_READ_EXCEPTIONS
67
+ rescue IO::WaitReadable
87
68
  if IO.select([self], nil, nil, @timeout)
88
69
  retry
89
70
  else
90
71
  raise Redis::TimeoutError
91
72
  end
92
- rescue *NBIO_WRITE_EXCEPTIONS
73
+ rescue IO::WaitWritable
93
74
  if IO.select(nil, [self], nil, @timeout)
94
75
  retry
95
76
  else
@@ -105,13 +86,13 @@ class Redis
105
86
  begin
106
87
  write_nonblock(data)
107
88
 
108
- rescue *NBIO_WRITE_EXCEPTIONS
89
+ rescue IO::WaitWritable
109
90
  if IO.select(nil, [self], nil, @write_timeout)
110
91
  retry
111
92
  else
112
93
  raise Redis::TimeoutError
113
94
  end
114
- rescue *NBIO_READ_EXCEPTIONS
95
+ rescue IO::WaitReadable
115
96
  if IO.select([self], nil, nil, @write_timeout)
116
97
  retry
117
98
  else
@@ -286,7 +267,10 @@ class Redis
286
267
  ssl_sock = new(tcp_sock, ctx)
287
268
  ssl_sock.hostname = host
288
269
  ssl_sock.connect
289
- ssl_sock.post_connection_check(host)
270
+
271
+ unless ctx.verify_mode == OpenSSL::SSL::VERIFY_NONE
272
+ ssl_sock.post_connection_check(host)
273
+ end
290
274
 
291
275
  ssl_sock
292
276
  end
@@ -307,14 +291,13 @@ class Redis
307
291
  raise ArgumentError, "SSL incompatible with unix sockets" if config[:ssl]
308
292
  sock = UNIXSocket.connect(config[:path], config[:connect_timeout])
309
293
  elsif config[:scheme] == "rediss" || config[:ssl]
310
- raise ArgumentError, "This library does not support SSL on Ruby < 1.9" if RUBY_VERSION < "1.9.3"
311
294
  sock = SSLSocket.connect(config[:host], config[:port], config[:connect_timeout], config[:ssl_params])
312
295
  else
313
296
  sock = TCPSocket.connect(config[:host], config[:port], config[:connect_timeout])
314
297
  end
315
298
 
316
299
  instance = new(sock)
317
- instance.timeout = config[:timeout]
300
+ instance.timeout = config[:read_timeout]
318
301
  instance.write_timeout = config[:write_timeout]
319
302
  instance.set_tcp_keepalive config[:tcp_keepalive]
320
303
  instance
@@ -1,6 +1,6 @@
1
- require "redis/connection/command_helper"
2
- require "redis/connection/registry"
3
- require "redis/errors"
1
+ require_relative "command_helper"
2
+ require_relative "registry"
3
+ require_relative "../errors"
4
4
  require "em-synchrony"
5
5
  require "hiredis/reader"
6
6
 
@@ -72,7 +72,15 @@ class Redis
72
72
 
73
73
  def self.connect(config)
74
74
  if config[:scheme] == "unix"
75
- conn = EventMachine.connect_unix_domain(config[:path], RedisClient)
75
+ begin
76
+ conn = EventMachine.connect_unix_domain(config[:path], RedisClient)
77
+ rescue RuntimeError => e
78
+ if e.message == "no connection"
79
+ raise Errno::ECONNREFUSED
80
+ else
81
+ raise e
82
+ end
83
+ end
76
84
  elsif config[:scheme] == "rediss" || config[:ssl]
77
85
  raise NotImplementedError, "SSL not supported by synchrony driver"
78
86
  else
@@ -1,4 +1,4 @@
1
- require "redis/connection/registry"
1
+ require_relative "connection/registry"
2
2
 
3
3
  # If a connection driver was required before this file, the array
4
4
  # Redis::Connection.drivers will contain one or more classes. The last driver
@@ -6,4 +6,4 @@ require "redis/connection/registry"
6
6
  # the plain Ruby driver as our default. Another driver can be required at a
7
7
  # later point in time, causing it to be the last element of the #drivers array
8
8
  # and therefore be chosen by default.
9
- require "redis/connection/ruby" if Redis::Connection.drivers.empty?
9
+ require_relative "connection/ruby" if Redis::Connection.drivers.empty?
@@ -1,4 +1,4 @@
1
- require "redis/hash_ring"
1
+ require_relative "hash_ring"
2
2
 
3
3
  class Redis
4
4
  class Distributed
@@ -144,8 +144,8 @@ class Redis
144
144
  end
145
145
 
146
146
  # Create a key using the serialized value, previously obtained using DUMP.
147
- def restore(key, ttl, serialized_value)
148
- node_for(key).restore(key, ttl, serialized_value)
147
+ def restore(key, ttl, serialized_value, options = {})
148
+ node_for(key).restore(key, ttl, serialized_value, options)
149
149
  end
150
150
 
151
151
  # Transfer a key from the connected instance to another instance.
@@ -161,6 +161,14 @@ class Redis
161
161
  end
162
162
  end
163
163
 
164
+ # Unlink keys.
165
+ def unlink(*args)
166
+ keys_per_node = args.group_by { |key| node_for(key) }
167
+ keys_per_node.inject(0) do |sum, (node, keys)|
168
+ sum + node.unlink(*keys)
169
+ end
170
+ end
171
+
164
172
  # Determine if a key exists.
165
173
  def exists(key)
166
174
  node_for(key).exists(key)
@@ -277,13 +285,16 @@ class Redis
277
285
  node_for(key).get(key)
278
286
  end
279
287
 
280
- # Get the values of all the given keys.
288
+ # Get the values of all the given keys as an Array.
281
289
  def mget(*keys)
282
- raise CannotDistribute, :mget
290
+ mapped_mget(*keys).values_at(*keys)
283
291
  end
284
292
 
293
+ # Get the values of all the given keys as a Hash.
285
294
  def mapped_mget(*keys)
286
- raise CannotDistribute, :mapped_mget
295
+ keys.group_by { |k| node_for k }.inject({}) do |results, (node, subkeys)|
296
+ results.merge! node.mapped_mget(*subkeys)
297
+ end
287
298
  end
288
299
 
289
300
  # Overwrite part of a string at key starting at the specified offset.
@@ -509,6 +520,16 @@ class Redis
509
520
  node_for(key).smembers(key)
510
521
  end
511
522
 
523
+ # Scan a set
524
+ def sscan(key, cursor, options={})
525
+ node_for(key).sscan(key, cursor, options)
526
+ end
527
+
528
+ # Scan a set and return an enumerator
529
+ def sscan_each(key, options={}, &block)
530
+ node_for(key).sscan_each(key, options, &block)
531
+ end
532
+
512
533
  # Subtract multiple sets.
513
534
  def sdiff(*keys)
514
535
  ensure_same_node(:sdiff, keys) do |node|
@@ -679,8 +700,8 @@ class Redis
679
700
  end
680
701
 
681
702
  # Delete one or more hash fields.
682
- def hdel(key, field)
683
- node_for(key).hdel(key, field)
703
+ def hdel(key, *fields)
704
+ node_for(key).hdel(key, *fields)
684
705
  end
685
706
 
686
707
  # Determine if a hash field exists.
data/lib/redis/errors.rb CHANGED
@@ -37,4 +37,50 @@ class Redis
37
37
  # Raised when the connection was inherited by a child process.
38
38
  class InheritedError < BaseConnectionError
39
39
  end
40
+
41
+ # Raised when client options are invalid.
42
+ class InvalidClientOptionError < BaseError
43
+ end
44
+
45
+ class Cluster
46
+ # Raised when client connected to redis as cluster mode
47
+ # and some cluster subcommands were called.
48
+ class OrchestrationCommandNotSupported < BaseError
49
+ def initialize(command, subcommand = '')
50
+ str = [command, subcommand].map(&:to_s).reject(&:empty?).join(' ').upcase
51
+ msg = "#{str} command should be used with care "\
52
+ 'only by applications orchestrating Redis Cluster, like redis-trib, '\
53
+ 'and the command if used out of the right context can leave the cluster '\
54
+ 'in a wrong state or cause data loss.'
55
+ super(msg)
56
+ end
57
+ end
58
+
59
+ # Raised when error occurs on any node of cluster.
60
+ class CommandErrorCollection < BaseError
61
+ attr_reader :errors
62
+
63
+ # @param errors [Hash{String => Redis::CommandError}]
64
+ # @param error_message [String]
65
+ def initialize(errors, error_message = 'Command errors were replied on any node')
66
+ @errors = errors
67
+ super(error_message)
68
+ end
69
+ end
70
+
71
+ # Raised when cluster client can't select node.
72
+ class AmbiguousNodeError < BaseError
73
+ def initialize(command)
74
+ super("Cluster client doesn't know which node the #{command} command should be sent to.")
75
+ end
76
+ end
77
+
78
+ # Raised when commands in pipelining include cross slot keys.
79
+ class CrossSlotPipeliningError < BaseError
80
+ def initialize(keys)
81
+ super("Cluster client couldn't send pipelining to single node. "\
82
+ "The commands include cross slot keys. #{keys}")
83
+ end
84
+ end
85
+ end
40
86
  end
@@ -25,7 +25,6 @@ class Redis
25
25
  @nodes << node
26
26
  @replicas.times do |i|
27
27
  key = Zlib.crc32("#{node.id}:#{i}")
28
- raise "Node ID collision" if @ring.has_key?(key)
29
28
  @ring[key] = node
30
29
  @sorted_keys << key
31
30
  end
@@ -61,72 +60,29 @@ class Redis
61
60
  end
62
61
  end
63
62
 
64
- class << self
65
-
66
- # gem install RubyInline to use this code
67
- # Native extension to perform the binary search within the hashring.
68
- # There's a pure ruby version below so this is purely optional
69
- # for performance. In testing 20k gets and sets, the native
70
- # binary search shaved about 12% off the runtime (9sec -> 8sec).
71
- begin
72
- require 'inline'
73
- inline do |builder|
74
- builder.c <<-EOM
75
- int binary_search(VALUE ary, unsigned int r) {
76
- int upper = RARRAY_LEN(ary) - 1;
77
- int lower = 0;
78
- int idx = 0;
79
-
80
- while (lower <= upper) {
81
- idx = (lower + upper) / 2;
82
-
83
- VALUE continuumValue = RARRAY_PTR(ary)[idx];
84
- unsigned int l = NUM2UINT(continuumValue);
85
- if (l == r) {
86
- return idx;
87
- }
88
- else if (l > r) {
89
- upper = idx - 1;
90
- }
91
- else {
92
- lower = idx + 1;
93
- }
94
- }
95
- if (upper < 0) {
96
- upper = RARRAY_LEN(ary) - 1;
97
- }
98
- return upper;
99
- }
100
- EOM
101
- end
102
- rescue Exception
103
- # Find the closest index in HashRing with value <= the given value
104
- def binary_search(ary, value, &block)
105
- upper = ary.size - 1
106
- lower = 0
107
- idx = 0
108
-
109
- while(lower <= upper) do
110
- idx = (lower + upper) / 2
111
- comp = ary[idx] <=> value
112
-
113
- if comp == 0
114
- return idx
115
- elsif comp > 0
116
- upper = idx - 1
117
- else
118
- lower = idx + 1
119
- end
120
- end
121
-
122
- if upper < 0
123
- upper = ary.size - 1
124
- end
125
- return upper
63
+ # Find the closest index in HashRing with value <= the given value
64
+ def self.binary_search(ary, value, &block)
65
+ upper = ary.size - 1
66
+ lower = 0
67
+ idx = 0
68
+
69
+ while(lower <= upper) do
70
+ idx = (lower + upper) / 2
71
+ comp = ary[idx] <=> value
72
+
73
+ if comp == 0
74
+ return idx
75
+ elsif comp > 0
76
+ upper = idx - 1
77
+ else
78
+ lower = idx + 1
126
79
  end
80
+ end
127
81
 
82
+ if upper < 0
83
+ upper = ary.size - 1
128
84
  end
85
+ return upper
129
86
  end
130
-
131
87
  end
132
88
  end