redis 3.3.5 → 4.0.3
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 +4 -4
- data/.gitignore +3 -0
- data/.travis/Gemfile +8 -1
- data/.travis.yml +34 -62
- data/CHANGELOG.md +45 -2
- data/Gemfile +5 -1
- data/README.md +32 -76
- data/benchmarking/logging.rb +1 -1
- data/bin/build +71 -0
- data/bors.toml +14 -0
- data/lib/redis/client.rb +38 -20
- data/lib/redis/cluster/command.rb +81 -0
- data/lib/redis/cluster/command_loader.rb +32 -0
- data/lib/redis/cluster/key_slot_converter.rb +72 -0
- data/lib/redis/cluster/node.rb +104 -0
- data/lib/redis/cluster/node_key.rb +35 -0
- data/lib/redis/cluster/node_loader.rb +35 -0
- data/lib/redis/cluster/option.rb +76 -0
- data/lib/redis/cluster/slot.rb +69 -0
- data/lib/redis/cluster/slot_loader.rb +47 -0
- data/lib/redis/cluster.rb +285 -0
- data/lib/redis/connection/command_helper.rb +2 -8
- data/lib/redis/connection/hiredis.rb +2 -2
- data/lib/redis/connection/ruby.rb +13 -30
- data/lib/redis/connection/synchrony.rb +12 -4
- data/lib/redis/connection.rb +2 -2
- data/lib/redis/distributed.rb +29 -8
- data/lib/redis/errors.rb +46 -0
- data/lib/redis/hash_ring.rb +20 -64
- data/lib/redis/pipeline.rb +9 -7
- data/lib/redis/version.rb +1 -1
- data/lib/redis.rb +287 -52
- data/makefile +74 -0
- data/redis.gemspec +9 -10
- data/test/bitpos_test.rb +13 -19
- data/test/blocking_commands_test.rb +3 -5
- data/test/client_test.rb +18 -1
- data/test/cluster_abnormal_state_test.rb +38 -0
- data/test/cluster_blocking_commands_test.rb +15 -0
- data/test/cluster_client_internals_test.rb +77 -0
- data/test/cluster_client_key_hash_tags_test.rb +88 -0
- data/test/cluster_client_options_test.rb +147 -0
- data/test/cluster_client_pipelining_test.rb +59 -0
- data/test/cluster_client_replicas_test.rb +36 -0
- data/test/cluster_client_slots_test.rb +94 -0
- data/test/cluster_client_transactions_test.rb +71 -0
- data/test/cluster_commands_on_cluster_test.rb +165 -0
- data/test/cluster_commands_on_connection_test.rb +40 -0
- data/test/cluster_commands_on_geo_test.rb +74 -0
- data/test/cluster_commands_on_hashes_test.rb +11 -0
- data/test/cluster_commands_on_hyper_log_log_test.rb +17 -0
- data/test/cluster_commands_on_keys_test.rb +134 -0
- data/test/cluster_commands_on_lists_test.rb +15 -0
- data/test/cluster_commands_on_pub_sub_test.rb +101 -0
- data/test/cluster_commands_on_scripting_test.rb +56 -0
- data/test/cluster_commands_on_server_test.rb +221 -0
- data/test/cluster_commands_on_sets_test.rb +39 -0
- data/test/cluster_commands_on_sorted_sets_test.rb +35 -0
- data/test/cluster_commands_on_streams_test.rb +196 -0
- data/test/cluster_commands_on_strings_test.rb +15 -0
- data/test/cluster_commands_on_transactions_test.rb +41 -0
- data/test/cluster_commands_on_value_types_test.rb +14 -0
- data/test/command_map_test.rb +3 -5
- data/test/commands_on_geo_test.rb +116 -0
- data/test/commands_on_hashes_test.rb +2 -16
- data/test/commands_on_hyper_log_log_test.rb +3 -17
- data/test/commands_on_lists_test.rb +2 -15
- data/test/commands_on_sets_test.rb +2 -72
- data/test/commands_on_sorted_sets_test.rb +2 -132
- data/test/commands_on_strings_test.rb +2 -96
- data/test/commands_on_value_types_test.rb +80 -6
- data/test/connection_handling_test.rb +5 -7
- data/test/distributed_blocking_commands_test.rb +10 -4
- data/test/distributed_commands_on_hashes_test.rb +16 -5
- data/test/distributed_commands_on_hyper_log_log_test.rb +8 -15
- data/test/distributed_commands_on_lists_test.rb +4 -7
- data/test/distributed_commands_on_sets_test.rb +58 -36
- data/test/distributed_commands_on_sorted_sets_test.rb +51 -10
- data/test/distributed_commands_on_strings_test.rb +30 -10
- data/test/distributed_commands_on_value_types_test.rb +38 -4
- data/test/distributed_commands_requiring_clustering_test.rb +1 -3
- data/test/distributed_connection_handling_test.rb +1 -3
- data/test/distributed_internals_test.rb +8 -19
- data/test/distributed_key_tags_test.rb +4 -6
- data/test/distributed_persistence_control_commands_test.rb +1 -3
- data/test/distributed_publish_subscribe_test.rb +1 -3
- data/test/distributed_remote_server_control_commands_test.rb +1 -3
- data/test/distributed_scripting_test.rb +1 -3
- data/test/distributed_sorting_test.rb +1 -3
- data/test/distributed_test.rb +12 -14
- data/test/distributed_transactions_test.rb +1 -3
- data/test/encoding_test.rb +4 -8
- data/test/error_replies_test.rb +2 -4
- data/test/fork_safety_test.rb +1 -6
- data/test/helper.rb +179 -66
- data/test/helper_test.rb +1 -3
- data/test/internals_test.rb +47 -56
- data/test/lint/blocking_commands.rb +40 -16
- data/test/lint/hashes.rb +41 -0
- data/test/lint/hyper_log_log.rb +15 -1
- data/test/lint/lists.rb +16 -0
- data/test/lint/sets.rb +142 -0
- data/test/lint/sorted_sets.rb +183 -2
- data/test/lint/strings.rb +108 -20
- data/test/lint/value_types.rb +8 -0
- data/test/persistence_control_commands_test.rb +1 -3
- data/test/pipelining_commands_test.rb +12 -8
- data/test/publish_subscribe_test.rb +1 -3
- data/test/remote_server_control_commands_test.rb +60 -3
- data/test/scanning_test.rb +1 -7
- data/test/scripting_test.rb +1 -3
- data/test/sentinel_command_test.rb +1 -3
- data/test/sentinel_test.rb +1 -3
- data/test/sorting_test.rb +1 -3
- data/test/ssl_test.rb +45 -49
- data/test/support/cluster/orchestrator.rb +199 -0
- data/test/support/connection/hiredis.rb +1 -1
- data/test/support/connection/ruby.rb +1 -1
- data/test/support/connection/synchrony.rb +1 -1
- data/test/support/redis_mock.rb +1 -1
- data/test/synchrony_driver.rb +6 -9
- data/test/thread_safety_test.rb +1 -3
- data/test/transactions_test.rb +11 -3
- data/test/unknown_commands_test.rb +1 -3
- data/test/url_param_test.rb +44 -46
- metadata +109 -16
- 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
|
-
|
34
|
-
|
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,6 +1,6 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
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
|
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
|
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
|
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
|
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
|
-
|
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[:
|
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
|
-
|
2
|
-
|
3
|
-
|
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
|
-
|
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
|
data/lib/redis/connection.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
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
|
-
|
9
|
+
require_relative "connection/ruby" if Redis::Connection.drivers.empty?
|
data/lib/redis/distributed.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
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
|
-
|
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
|
-
|
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,
|
683
|
-
node_for(key).hdel(key,
|
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
|
data/lib/redis/hash_ring.rb
CHANGED
@@ -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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|