redis 3.0.0 → 4.5.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 (106) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +315 -0
  3. data/README.md +301 -58
  4. data/lib/redis/client.rb +383 -88
  5. data/lib/redis/cluster/command.rb +81 -0
  6. data/lib/redis/cluster/command_loader.rb +33 -0
  7. data/lib/redis/cluster/key_slot_converter.rb +72 -0
  8. data/lib/redis/cluster/node.rb +108 -0
  9. data/lib/redis/cluster/node_key.rb +31 -0
  10. data/lib/redis/cluster/node_loader.rb +37 -0
  11. data/lib/redis/cluster/option.rb +93 -0
  12. data/lib/redis/cluster/slot.rb +86 -0
  13. data/lib/redis/cluster/slot_loader.rb +49 -0
  14. data/lib/redis/cluster.rb +291 -0
  15. data/lib/redis/connection/command_helper.rb +7 -10
  16. data/lib/redis/connection/hiredis.rb +12 -8
  17. data/lib/redis/connection/registry.rb +2 -1
  18. data/lib/redis/connection/ruby.rb +266 -74
  19. data/lib/redis/connection/synchrony.rb +41 -14
  20. data/lib/redis/connection.rb +4 -2
  21. data/lib/redis/distributed.rb +258 -76
  22. data/lib/redis/errors.rb +48 -0
  23. data/lib/redis/hash_ring.rb +31 -73
  24. data/lib/redis/pipeline.rb +74 -18
  25. data/lib/redis/subscribe.rb +24 -13
  26. data/lib/redis/version.rb +3 -1
  27. data/lib/redis.rb +2068 -464
  28. metadata +63 -160
  29. data/.gitignore +0 -10
  30. data/.order +0 -169
  31. data/.travis/Gemfile +0 -11
  32. data/.travis.yml +0 -50
  33. data/.yardopts +0 -3
  34. data/Rakefile +0 -392
  35. data/benchmarking/logging.rb +0 -62
  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/dist_redis.rb +0 -43
  42. data/examples/incr-decr.rb +0 -17
  43. data/examples/list.rb +0 -26
  44. data/examples/pubsub.rb +0 -31
  45. data/examples/sets.rb +0 -36
  46. data/examples/unicorn/config.ru +0 -3
  47. data/examples/unicorn/unicorn.rb +0 -20
  48. data/redis.gemspec +0 -41
  49. data/test/blocking_commands_test.rb +0 -42
  50. data/test/command_map_test.rb +0 -30
  51. data/test/commands_on_hashes_test.rb +0 -21
  52. data/test/commands_on_lists_test.rb +0 -20
  53. data/test/commands_on_sets_test.rb +0 -77
  54. data/test/commands_on_sorted_sets_test.rb +0 -109
  55. data/test/commands_on_strings_test.rb +0 -83
  56. data/test/commands_on_value_types_test.rb +0 -99
  57. data/test/connection_handling_test.rb +0 -189
  58. data/test/db/.gitignore +0 -1
  59. data/test/distributed_blocking_commands_test.rb +0 -46
  60. data/test/distributed_commands_on_hashes_test.rb +0 -10
  61. data/test/distributed_commands_on_lists_test.rb +0 -22
  62. data/test/distributed_commands_on_sets_test.rb +0 -83
  63. data/test/distributed_commands_on_sorted_sets_test.rb +0 -18
  64. data/test/distributed_commands_on_strings_test.rb +0 -48
  65. data/test/distributed_commands_on_value_types_test.rb +0 -87
  66. data/test/distributed_commands_requiring_clustering_test.rb +0 -148
  67. data/test/distributed_connection_handling_test.rb +0 -23
  68. data/test/distributed_internals_test.rb +0 -15
  69. data/test/distributed_key_tags_test.rb +0 -52
  70. data/test/distributed_persistence_control_commands_test.rb +0 -26
  71. data/test/distributed_publish_subscribe_test.rb +0 -92
  72. data/test/distributed_remote_server_control_commands_test.rb +0 -53
  73. data/test/distributed_scripting_test.rb +0 -102
  74. data/test/distributed_sorting_test.rb +0 -20
  75. data/test/distributed_test.rb +0 -58
  76. data/test/distributed_transactions_test.rb +0 -32
  77. data/test/encoding_test.rb +0 -18
  78. data/test/error_replies_test.rb +0 -59
  79. data/test/helper.rb +0 -188
  80. data/test/helper_test.rb +0 -22
  81. data/test/internals_test.rb +0 -214
  82. data/test/lint/blocking_commands.rb +0 -124
  83. data/test/lint/hashes.rb +0 -162
  84. data/test/lint/lists.rb +0 -143
  85. data/test/lint/sets.rb +0 -96
  86. data/test/lint/sorted_sets.rb +0 -201
  87. data/test/lint/strings.rb +0 -157
  88. data/test/lint/value_types.rb +0 -106
  89. data/test/persistence_control_commands_test.rb +0 -26
  90. data/test/pipelining_commands_test.rb +0 -195
  91. data/test/publish_subscribe_test.rb +0 -153
  92. data/test/remote_server_control_commands_test.rb +0 -104
  93. data/test/scripting_test.rb +0 -78
  94. data/test/sorting_test.rb +0 -45
  95. data/test/support/connection/hiredis.rb +0 -1
  96. data/test/support/connection/ruby.rb +0 -1
  97. data/test/support/connection/synchrony.rb +0 -17
  98. data/test/support/redis_mock.rb +0 -92
  99. data/test/support/wire/synchrony.rb +0 -24
  100. data/test/support/wire/thread.rb +0 -5
  101. data/test/synchrony_driver.rb +0 -57
  102. data/test/test.conf +0 -9
  103. data/test/thread_safety_test.rb +0 -32
  104. data/test/transactions_test.rb +0 -244
  105. data/test/unknown_commands_test.rb +0 -14
  106. data/test/url_param_test.rb +0 -64
@@ -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
@@ -1,7 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Redis
2
4
  module Connection
3
5
  module CommandHelper
4
-
5
6
  COMMAND_DELIMITER = "\r\n"
6
7
 
7
8
  def build_command(args)
@@ -11,11 +12,13 @@ class Redis
11
12
  if i.is_a? Array
12
13
  i.each do |j|
13
14
  j = j.to_s
15
+ j = j.encoding == Encoding::BINARY ? j : j.b
14
16
  command << "$#{j.bytesize}"
15
17
  command << j
16
18
  end
17
19
  else
18
20
  i = i.to_s
21
+ i = i.encoding == Encoding::BINARY ? i : i.b
19
22
  command << "$#{i.bytesize}"
20
23
  command << i
21
24
  end
@@ -28,16 +31,10 @@ class Redis
28
31
  command.join(COMMAND_DELIMITER)
29
32
  end
30
33
 
31
- protected
34
+ protected
32
35
 
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
36
+ def encode(string)
37
+ string.force_encoding(Encoding.default_external)
41
38
  end
42
39
  end
43
40
  end
@@ -1,23 +1,27 @@
1
- require "redis/connection/registry"
2
- require "redis/errors"
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "registry"
4
+ require_relative "../errors"
3
5
  require "hiredis/connection"
4
6
  require "timeout"
5
7
 
6
8
  class Redis
7
9
  module Connection
8
10
  class Hiredis
9
-
10
11
  def self.connect(config)
11
12
  connection = ::Hiredis::Connection.new
13
+ connect_timeout = (config.fetch(:connect_timeout, 0) * 1_000_000).to_i
12
14
 
13
15
  if config[:scheme] == "unix"
14
- connection.connect_unix(config[:path], Integer(config[:timeout] * 1_000_000))
16
+ connection.connect_unix(config[:path], connect_timeout)
17
+ elsif config[:scheme] == "rediss" || config[:ssl]
18
+ raise NotImplementedError, "SSL not supported by hiredis driver"
15
19
  else
16
- connection.connect(config[:host], config[:port], Integer(config[:timeout] * 1_000_000))
20
+ connection.connect(config[:host], config[:port], connect_timeout)
17
21
  end
18
22
 
19
23
  instance = new(connection)
20
- instance.timeout = config[:timeout]
24
+ instance.timeout = config[:read_timeout]
21
25
  instance
22
26
  rescue Errno::ETIMEDOUT
23
27
  raise TimeoutError
@@ -28,7 +32,7 @@ class Redis
28
32
  end
29
33
 
30
34
  def connected?
31
- @connection && @connection.connected?
35
+ @connection&.connected?
32
36
  end
33
37
 
34
38
  def timeout=(timeout)
@@ -54,7 +58,7 @@ class Redis
54
58
  rescue Errno::EAGAIN
55
59
  raise TimeoutError
56
60
  rescue RuntimeError => err
57
- raise ProtocolError.new(err.message)
61
+ raise ProtocolError, err.message
58
62
  end
59
63
  end
60
64
  end
@@ -1,6 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Redis
2
4
  module Connection
3
-
4
5
  # Store a list of loaded connection drivers in the Connection module.
5
6
  # Redis::Client uses the last required driver by default, and will be aware
6
7
  # of the loaded connection drivers if the user chooses to override the