redis 4.8.1 → 6.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +138 -1
- data/README.md +285 -169
- data/lib/redis/client.rb +116 -611
- data/lib/redis/commands/bitmaps.rb +14 -4
- data/lib/redis/commands/cluster.rb +1 -18
- data/lib/redis/commands/connection.rb +5 -10
- data/lib/redis/commands/geo.rb +109 -7
- data/lib/redis/commands/hashes.rb +179 -8
- data/lib/redis/commands/hyper_log_log.rb +1 -1
- data/lib/redis/commands/keys.rb +32 -24
- data/lib/redis/commands/lists.rb +167 -25
- data/lib/redis/commands/modules/json.rb +530 -0
- data/lib/redis/commands/modules/search/aggregation.rb +418 -0
- data/lib/redis/commands/modules/search/dialect.rb +14 -0
- data/lib/redis/commands/modules/search/field.rb +306 -0
- data/lib/redis/commands/modules/search/hybrid.rb +359 -0
- data/lib/redis/commands/modules/search/index.rb +351 -0
- data/lib/redis/commands/modules/search/index_definition.rb +114 -0
- data/lib/redis/commands/modules/search/miscellaneous.rb +607 -0
- data/lib/redis/commands/modules/search/query.rb +738 -0
- data/lib/redis/commands/modules/search/result.rb +488 -0
- data/lib/redis/commands/modules/search/schema.rb +211 -0
- data/lib/redis/commands/modules/search.rb +19 -0
- data/lib/redis/commands/pubsub.rb +34 -25
- data/lib/redis/commands/server.rb +15 -15
- data/lib/redis/commands/sets.rb +76 -40
- data/lib/redis/commands/sorted_sets.rb +128 -19
- data/lib/redis/commands/streams.rb +75 -28
- data/lib/redis/commands/strings.rb +18 -17
- data/lib/redis/commands/transactions.rb +7 -31
- data/lib/redis/commands.rb +39 -20
- data/lib/redis/distributed.rb +407 -73
- data/lib/redis/errors.rb +20 -50
- data/lib/redis/hash_ring.rb +26 -26
- data/lib/redis/lib_identity.rb +105 -0
- data/lib/redis/pipeline.rb +47 -222
- data/lib/redis/subscribe.rb +51 -15
- data/lib/redis/version.rb +1 -1
- data/lib/redis.rb +213 -188
- metadata +25 -59
- data/lib/redis/cluster/command.rb +0 -79
- data/lib/redis/cluster/command_loader.rb +0 -33
- data/lib/redis/cluster/key_slot_converter.rb +0 -72
- data/lib/redis/cluster/node.rb +0 -120
- data/lib/redis/cluster/node_key.rb +0 -31
- data/lib/redis/cluster/node_loader.rb +0 -34
- data/lib/redis/cluster/option.rb +0 -100
- data/lib/redis/cluster/slot.rb +0 -86
- data/lib/redis/cluster/slot_loader.rb +0 -46
- data/lib/redis/cluster.rb +0 -315
- data/lib/redis/connection/command_helper.rb +0 -41
- data/lib/redis/connection/hiredis.rb +0 -68
- data/lib/redis/connection/registry.rb +0 -13
- data/lib/redis/connection/ruby.rb +0 -437
- data/lib/redis/connection/synchrony.rb +0 -148
- data/lib/redis/connection.rb +0 -11
data/lib/redis/errors.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
class Redis
|
|
4
4
|
# Base error for all redis-rb errors.
|
|
5
|
-
class BaseError <
|
|
5
|
+
class BaseError < StandardError
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
# Raised by the connection when a protocol error occurs.
|
|
@@ -20,6 +20,20 @@ class Redis
|
|
|
20
20
|
class CommandError < BaseError
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
class PermissionError < CommandError
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class WrongTypeError < CommandError
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class OutOfMemoryError < CommandError
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if defined?(RedisClient::NoScriptError)
|
|
33
|
+
class NoScriptError < CommandError
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
23
37
|
# Base error for connection related errors.
|
|
24
38
|
class BaseConnectionError < BaseError
|
|
25
39
|
end
|
|
@@ -40,58 +54,14 @@ class Redis
|
|
|
40
54
|
class InheritedError < BaseConnectionError
|
|
41
55
|
end
|
|
42
56
|
|
|
57
|
+
# Generally raised during Redis failover scenarios
|
|
58
|
+
class ReadOnlyError < BaseConnectionError
|
|
59
|
+
end
|
|
60
|
+
|
|
43
61
|
# Raised when client options are invalid.
|
|
44
62
|
class InvalidClientOptionError < BaseError
|
|
45
63
|
end
|
|
46
64
|
|
|
47
|
-
class
|
|
48
|
-
# Raised when client connected to redis as cluster mode
|
|
49
|
-
# and failed to fetch cluster state information by commands.
|
|
50
|
-
class InitialSetupError < BaseError
|
|
51
|
-
# @param errors [Array<Redis::BaseError>]
|
|
52
|
-
def initialize(errors)
|
|
53
|
-
super("Redis client could not fetch cluster information: #{errors.map(&:message).uniq.join(',')}")
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Raised when client connected to redis as cluster mode
|
|
58
|
-
# and some cluster subcommands were called.
|
|
59
|
-
class OrchestrationCommandNotSupported < BaseError
|
|
60
|
-
def initialize(command, subcommand = '')
|
|
61
|
-
str = [command, subcommand].map(&:to_s).reject(&:empty?).join(' ').upcase
|
|
62
|
-
msg = "#{str} command should be used with care "\
|
|
63
|
-
'only by applications orchestrating Redis Cluster, like redis-trib, '\
|
|
64
|
-
'and the command if used out of the right context can leave the cluster '\
|
|
65
|
-
'in a wrong state or cause data loss.'
|
|
66
|
-
super(msg)
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Raised when error occurs on any node of cluster.
|
|
71
|
-
class CommandErrorCollection < BaseError
|
|
72
|
-
attr_reader :errors
|
|
73
|
-
|
|
74
|
-
# @param errors [Hash{String => Redis::CommandError}]
|
|
75
|
-
# @param error_message [String]
|
|
76
|
-
def initialize(errors, error_message = 'Command errors were replied on any node')
|
|
77
|
-
@errors = errors
|
|
78
|
-
super(error_message)
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Raised when cluster client can't select node.
|
|
83
|
-
class AmbiguousNodeError < BaseError
|
|
84
|
-
def initialize(command)
|
|
85
|
-
super("Cluster client doesn't know which node the #{command} command should be sent to.")
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# Raised when commands in pipelining include cross slot keys.
|
|
90
|
-
class CrossSlotPipeliningError < BaseError
|
|
91
|
-
def initialize(keys)
|
|
92
|
-
super("Cluster client couldn't send pipelining to single node. "\
|
|
93
|
-
"The commands include cross slot keys. #{keys}")
|
|
94
|
-
end
|
|
95
|
-
end
|
|
65
|
+
class SubscriptionError < BaseError
|
|
96
66
|
end
|
|
97
67
|
end
|
data/lib/redis/hash_ring.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'zlib'
|
|
4
|
+
require 'digest/md5'
|
|
4
5
|
|
|
5
6
|
class Redis
|
|
6
7
|
class HashRing
|
|
@@ -25,7 +26,7 @@ class Redis
|
|
|
25
26
|
def add_node(node)
|
|
26
27
|
@nodes << node
|
|
27
28
|
@replicas.times do |i|
|
|
28
|
-
key =
|
|
29
|
+
key = server_hash_for("#{node.id}:#{i}")
|
|
29
30
|
@ring[key] = node
|
|
30
31
|
@sorted_keys << key
|
|
31
32
|
end
|
|
@@ -35,7 +36,7 @@ class Redis
|
|
|
35
36
|
def remove_node(node)
|
|
36
37
|
@nodes.reject! { |n| n.id == node.id }
|
|
37
38
|
@replicas.times do |i|
|
|
38
|
-
key =
|
|
39
|
+
key = server_hash_for("#{node.id}:#{i}")
|
|
39
40
|
@ring.delete(key)
|
|
40
41
|
@sorted_keys.reject! { |k| k == key }
|
|
41
42
|
end
|
|
@@ -43,47 +44,46 @@ class Redis
|
|
|
43
44
|
|
|
44
45
|
# get the node in the hash ring for this key
|
|
45
46
|
def get_node(key)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def get_node_pos(key)
|
|
50
|
-
return [nil, nil] if @ring.empty?
|
|
51
|
-
|
|
52
|
-
crc = Zlib.crc32(key)
|
|
53
|
-
idx = HashRing.binary_search(@sorted_keys, crc)
|
|
54
|
-
[@ring[@sorted_keys[idx]], idx]
|
|
47
|
+
hash = hash_for(key)
|
|
48
|
+
idx = binary_search(@sorted_keys, hash)
|
|
49
|
+
@ring[@sorted_keys[idx]]
|
|
55
50
|
end
|
|
56
51
|
|
|
57
52
|
def iter_nodes(key)
|
|
58
53
|
return [nil, nil] if @ring.empty?
|
|
59
54
|
|
|
60
|
-
|
|
55
|
+
crc = hash_for(key)
|
|
56
|
+
pos = binary_search(@sorted_keys, crc)
|
|
61
57
|
@ring.size.times do |n|
|
|
62
58
|
yield @ring[@sorted_keys[(pos + n) % @ring.size]]
|
|
63
59
|
end
|
|
64
60
|
end
|
|
65
61
|
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def hash_for(key)
|
|
65
|
+
Zlib.crc32(key)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def server_hash_for(key)
|
|
69
|
+
Digest::MD5.digest(key).unpack1("L>")
|
|
70
|
+
end
|
|
71
|
+
|
|
66
72
|
# Find the closest index in HashRing with value <= the given value
|
|
67
|
-
def
|
|
68
|
-
upper = ary.size
|
|
73
|
+
def binary_search(ary, value)
|
|
74
|
+
upper = ary.size
|
|
69
75
|
lower = 0
|
|
70
|
-
idx = 0
|
|
71
|
-
|
|
72
|
-
while lower <= upper
|
|
73
|
-
idx = (lower + upper) / 2
|
|
74
|
-
comp = ary[idx] <=> value
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
upper =
|
|
77
|
+
while lower < upper
|
|
78
|
+
mid = (lower + upper) / 2
|
|
79
|
+
if ary[mid] > value
|
|
80
|
+
upper = mid
|
|
80
81
|
else
|
|
81
|
-
lower =
|
|
82
|
+
lower = mid + 1
|
|
82
83
|
end
|
|
83
84
|
end
|
|
84
85
|
|
|
85
|
-
upper
|
|
86
|
-
upper
|
|
86
|
+
upper - 1
|
|
87
87
|
end
|
|
88
88
|
end
|
|
89
89
|
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "redis-client"
|
|
4
|
+
require "redis/version"
|
|
5
|
+
|
|
6
|
+
class Redis
|
|
7
|
+
# Reports `redis-rb` as the connecting library via `CLIENT SETINFO`.
|
|
8
|
+
#
|
|
9
|
+
# `redis-client` builds its own `LIB-NAME` as `redis-client(<driver_info>)` and always
|
|
10
|
+
# reports its own version as `LIB-VER`, which would identify `redis-client` rather than
|
|
11
|
+
# `redis-rb` to the server. We append a second pair of `CLIENT SETINFO` commands, which wins
|
|
12
|
+
# because the command is last-write-wins, so that `CLIENT LIST` / `CLIENT INFO` report
|
|
13
|
+
# `lib-name=redis-rb` and `lib-ver=<Redis::VERSION>`.
|
|
14
|
+
#
|
|
15
|
+
# Libraries built on top of `redis-rb` can still identify themselves by passing `driver_info`,
|
|
16
|
+
# in which case they are reported inside the parentheses, e.g. `lib-name=redis-rb(my-gem_v1.0.0)`.
|
|
17
|
+
# The official convention for such suffixes is `<name>_v<version>` (matching
|
|
18
|
+
# `(?<custom-name>[ -~]+)[ -~]v(?<custom-version>[\d\.]+)`), with `;` delimiting multiple
|
|
19
|
+
# suffixes and parentheses avoided inside them — see
|
|
20
|
+
# https://redis.io/docs/latest/commands/client-setinfo/. Passing `driver_info: false` disables
|
|
21
|
+
# `CLIENT SETINFO` entirely, for peers that can't tolerate unknown commands during the handshake
|
|
22
|
+
# (e.g. some proxies).
|
|
23
|
+
#
|
|
24
|
+
# Applications that use `redis-client` directly are left untouched: the override only applies
|
|
25
|
+
# to configurations whose `driver_info` was built by this module.
|
|
26
|
+
module LibIdentity
|
|
27
|
+
# Follows the official suffix convention, so that the transient pair `redis-client` itself
|
|
28
|
+
# builds from this value (`lib-name=redis-client(redis-rb_v<version>)`) is well-formed too.
|
|
29
|
+
MARKER = "redis-rb_v"
|
|
30
|
+
LIB_NAME = "redis-rb"
|
|
31
|
+
SEPARATOR = ";"
|
|
32
|
+
# Interpolated strings are not frozen by the magic comment; explicit freezes keep these
|
|
33
|
+
# constants Ractor-shareable (lazy module ivars here would raise Ractor::IsolationError).
|
|
34
|
+
OWN_INFO = "#{MARKER}#{Redis::VERSION}".freeze
|
|
35
|
+
OWN_PREFIX = "#{OWN_INFO}#{SEPARATOR}".freeze
|
|
36
|
+
# `CLIENT SETINFO` values must be printable ASCII with no spaces — the server rejects anything
|
|
37
|
+
# else, and `redis-client` swallows that error, which would silently leave `lib-name` empty.
|
|
38
|
+
# Parentheses are excluded as well: they delimit the suffix in `lib-name=redis-rb(<suffix>)`,
|
|
39
|
+
# so a suffix containing them would corrupt the reported name.
|
|
40
|
+
INVALID_CHARS = /(?:[^!-~]|[()])+/
|
|
41
|
+
|
|
42
|
+
class << self
|
|
43
|
+
# Combines redis-rb's own identity with the `driver_info` a downstream library passed in,
|
|
44
|
+
# so that both can be recovered when the connection prelude is built. `false` is passed
|
|
45
|
+
# through untouched and, being falsy, turns `CLIENT SETINFO` off wholesale (`redis-client`
|
|
46
|
+
# gates its own commands on `driver_info` too). Any other non-String, non-Array value is
|
|
47
|
+
# also passed through, leaving `redis-client` to reject it.
|
|
48
|
+
def driver_info(downstream = nil)
|
|
49
|
+
case downstream
|
|
50
|
+
when nil then OWN_INFO
|
|
51
|
+
when String then combine(downstream)
|
|
52
|
+
when Array then combine(downstream.join(SEPARATOR))
|
|
53
|
+
else downstream
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# True only for values `driver_info` produced: exactly our own identity, or our identity
|
|
58
|
+
# followed by a downstream library's. Matching on the bare `MARKER` prefix instead would
|
|
59
|
+
# also capture a direct `redis-client` user whose own `driver_info` merely starts with
|
|
60
|
+
# `redis-rb_v` (say `redis-rb_viewer-1.0`) and would silently discard their identity.
|
|
61
|
+
def ours?(info)
|
|
62
|
+
info.is_a?(String) && (info == OWN_INFO || info.start_with?(OWN_PREFIX))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def setinfo_commands(info)
|
|
66
|
+
[
|
|
67
|
+
["CLIENT", "SETINFO", "LIB-NAME", lib_name(info)].freeze,
|
|
68
|
+
["CLIENT", "SETINFO", "LIB-VER", Redis::VERSION].freeze,
|
|
69
|
+
].freeze
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def combine(downstream)
|
|
75
|
+
downstream = downstream.scrub(" ") # invalid byte sequences would make gsub raise
|
|
76
|
+
.gsub(/\A#{INVALID_CHARS}|#{INVALID_CHARS}\z/o, "")
|
|
77
|
+
.gsub(INVALID_CHARS, "_")
|
|
78
|
+
downstream.empty? ? OWN_INFO : "#{OWN_PREFIX}#{downstream}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def lib_name(info)
|
|
82
|
+
downstream = info == OWN_INFO ? nil : info.delete_prefix(OWN_PREFIX)
|
|
83
|
+
downstream.nil? || downstream.empty? ? LIB_NAME : "#{LIB_NAME}(#{downstream})".freeze
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def connection_prelude
|
|
88
|
+
prelude = super
|
|
89
|
+
info = driver_info
|
|
90
|
+
return prelude unless LibIdentity.ours?(info)
|
|
91
|
+
|
|
92
|
+
# `CLIENT SETINFO` is last-write-wins, so appending after the commands `redis-client`
|
|
93
|
+
# already built overrides them, without having to recognise or remove them.
|
|
94
|
+
(prelude + LibIdentity.setinfo_commands(info)).freeze
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Prepended to the concrete config classes rather than to `RedisClient::Config::Common`, so that
|
|
100
|
+
# the two `redis-client` entry points are covered without altering the shared module for anything
|
|
101
|
+
# else that includes it. `RedisClient::SentinelConfig` is NOT a `RedisClient::Config` subclass, so
|
|
102
|
+
# both lines are required. `RedisClient::Cluster::Node::Config` does subclass `RedisClient::Config`
|
|
103
|
+
# and calls `super` in its own `connection_prelude`, so cluster nodes are covered by the first.
|
|
104
|
+
RedisClient::Config.prepend(Redis::LibIdentity)
|
|
105
|
+
RedisClient::SentinelConfig.prepend(Redis::LibIdentity)
|
data/lib/redis/pipeline.rb
CHANGED
|
@@ -4,27 +4,31 @@ require "delegate"
|
|
|
4
4
|
|
|
5
5
|
class Redis
|
|
6
6
|
class PipelinedConnection
|
|
7
|
-
|
|
7
|
+
attr_accessor :db
|
|
8
|
+
|
|
9
|
+
def initialize(pipeline, futures = [], exception: true)
|
|
8
10
|
@pipeline = pipeline
|
|
11
|
+
@futures = futures
|
|
12
|
+
@exception = exception
|
|
9
13
|
end
|
|
10
14
|
|
|
11
15
|
include Commands
|
|
12
16
|
|
|
13
|
-
def db
|
|
14
|
-
@pipeline.db
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def db=(db)
|
|
18
|
-
@pipeline.db = db
|
|
19
|
-
end
|
|
20
|
-
|
|
21
17
|
def pipelined
|
|
22
18
|
yield self
|
|
23
19
|
end
|
|
24
20
|
|
|
25
|
-
def
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
def multi
|
|
22
|
+
transaction = MultiConnection.new(@pipeline, @futures)
|
|
23
|
+
send_command([:multi])
|
|
24
|
+
size = @futures.size
|
|
25
|
+
yield transaction
|
|
26
|
+
multi_future = MultiFuture.new(@futures[size..-1])
|
|
27
|
+
@pipeline.call_v([:exec]) do |result|
|
|
28
|
+
multi_future._set(result)
|
|
29
|
+
end
|
|
30
|
+
@futures << multi_future
|
|
31
|
+
multi_future
|
|
28
32
|
end
|
|
29
33
|
|
|
30
34
|
private
|
|
@@ -34,204 +38,36 @@ class Redis
|
|
|
34
38
|
end
|
|
35
39
|
|
|
36
40
|
def send_command(command, &block)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def send_blocking_command(command, timeout, &block)
|
|
41
|
-
@pipeline.call_with_timeout(command, timeout, &block)
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
class Pipeline
|
|
46
|
-
REDIS_INTERNAL_PATH = File.expand_path("..", __dir__).freeze
|
|
47
|
-
# Redis use MonitorMixin#synchronize and this class use DelegateClass which we want to filter out.
|
|
48
|
-
# Both are in the stdlib so we can simply filter the entire stdlib out.
|
|
49
|
-
STDLIB_PATH = File.expand_path("..", MonitorMixin.instance_method(:synchronize).source_location.first).freeze
|
|
50
|
-
|
|
51
|
-
class << self
|
|
52
|
-
def deprecation_warning(method, caller_locations) # :nodoc:
|
|
53
|
-
callsite = caller_locations.find { |l| !l.path.start_with?(REDIS_INTERNAL_PATH, STDLIB_PATH) }
|
|
54
|
-
callsite ||= caller_locations.last # The caller_locations should be large enough, but just in case.
|
|
55
|
-
::Redis.deprecate! <<~MESSAGE
|
|
56
|
-
Pipelining commands on a Redis instance is deprecated and will be removed in Redis 5.0.0.
|
|
57
|
-
|
|
58
|
-
redis.#{method} do
|
|
59
|
-
redis.get("key")
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
should be replaced by
|
|
63
|
-
|
|
64
|
-
redis.#{method} do |pipeline|
|
|
65
|
-
pipeline.get("key")
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
(called from #{callsite}}
|
|
69
|
-
MESSAGE
|
|
41
|
+
future = Future.new(command, block, @exception)
|
|
42
|
+
@pipeline.call_v(command) do |result|
|
|
43
|
+
future._set(result)
|
|
70
44
|
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
attr_accessor :db
|
|
74
|
-
attr_reader :client
|
|
75
|
-
|
|
76
|
-
attr :futures
|
|
77
|
-
alias materialized_futures futures
|
|
78
|
-
|
|
79
|
-
def initialize(client)
|
|
80
|
-
@client = client.is_a?(Pipeline) ? client.client : client
|
|
81
|
-
@with_reconnect = true
|
|
82
|
-
@shutdown = false
|
|
83
|
-
@futures = []
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def timeout
|
|
87
|
-
client.timeout
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def with_reconnect?
|
|
91
|
-
@with_reconnect
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def without_reconnect?
|
|
95
|
-
!@with_reconnect
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def shutdown?
|
|
99
|
-
@shutdown
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def empty?
|
|
103
|
-
@futures.empty?
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def call(command, timeout: nil, &block)
|
|
107
|
-
# A pipeline that contains a shutdown should not raise ECONNRESET when
|
|
108
|
-
# the connection is gone.
|
|
109
|
-
@shutdown = true if command.first == :shutdown
|
|
110
|
-
future = Future.new(command, block, timeout)
|
|
111
45
|
@futures << future
|
|
112
46
|
future
|
|
113
47
|
end
|
|
114
48
|
|
|
115
|
-
def
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def call_pipeline(pipeline)
|
|
120
|
-
@shutdown = true if pipeline.shutdown?
|
|
121
|
-
@futures.concat(pipeline.materialized_futures)
|
|
122
|
-
@db = pipeline.db
|
|
123
|
-
nil
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def commands
|
|
127
|
-
@futures.map(&:_command)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def timeouts
|
|
131
|
-
@futures.map(&:timeout)
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def with_reconnect(val = true)
|
|
135
|
-
@with_reconnect = false unless val
|
|
136
|
-
yield
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def without_reconnect(&blk)
|
|
140
|
-
with_reconnect(false, &blk)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def finish(replies, &blk)
|
|
144
|
-
if blk
|
|
145
|
-
futures.each_with_index.map do |future, i|
|
|
146
|
-
future._set(blk.call(replies[i]))
|
|
147
|
-
end
|
|
148
|
-
else
|
|
149
|
-
futures.each_with_index.map do |future, i|
|
|
150
|
-
future._set(replies[i])
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
class Multi < self
|
|
156
|
-
def finish(replies)
|
|
157
|
-
exec = replies.last
|
|
158
|
-
|
|
159
|
-
return if exec.nil? # The transaction failed because of WATCH.
|
|
160
|
-
|
|
161
|
-
# EXEC command failed.
|
|
162
|
-
raise exec if exec.is_a?(CommandError)
|
|
163
|
-
|
|
164
|
-
if exec.size < futures.size
|
|
165
|
-
# Some command wasn't recognized by Redis.
|
|
166
|
-
command_error = replies.detect { |r| r.is_a?(CommandError) }
|
|
167
|
-
raise command_error
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
super(exec) do |reply|
|
|
171
|
-
# Because an EXEC returns nested replies, hiredis won't be able to
|
|
172
|
-
# convert an error reply to a CommandError instance itself. This is
|
|
173
|
-
# specific to MULTI/EXEC, so we solve this here.
|
|
174
|
-
reply.is_a?(::RuntimeError) ? CommandError.new(reply.message) : reply
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def materialized_futures
|
|
179
|
-
if empty?
|
|
180
|
-
[]
|
|
181
|
-
else
|
|
182
|
-
[
|
|
183
|
-
Future.new([:multi], nil, 0),
|
|
184
|
-
*futures,
|
|
185
|
-
MultiFuture.new(futures)
|
|
186
|
-
]
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def timeouts
|
|
191
|
-
if empty?
|
|
192
|
-
[]
|
|
193
|
-
else
|
|
194
|
-
[nil, *super, nil]
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def commands
|
|
199
|
-
if empty?
|
|
200
|
-
[]
|
|
201
|
-
else
|
|
202
|
-
[[:multi]] + super + [[:exec]]
|
|
203
|
-
end
|
|
49
|
+
def send_blocking_command(command, timeout, &block)
|
|
50
|
+
future = Future.new(command, block, @exception)
|
|
51
|
+
@pipeline.blocking_call_v(timeout, command) do |result|
|
|
52
|
+
future._set(result)
|
|
204
53
|
end
|
|
54
|
+
@futures << future
|
|
55
|
+
future
|
|
205
56
|
end
|
|
206
57
|
end
|
|
207
58
|
|
|
208
|
-
class
|
|
209
|
-
def
|
|
210
|
-
|
|
211
|
-
@deprecation_displayed = false
|
|
59
|
+
class MultiConnection < PipelinedConnection
|
|
60
|
+
def multi
|
|
61
|
+
raise Redis::BaseError, "Can't nest multi transaction"
|
|
212
62
|
end
|
|
213
63
|
|
|
214
|
-
|
|
215
|
-
unless @deprecation_displayed
|
|
216
|
-
Pipeline.deprecation_warning("pipelined", Kernel.caller_locations(1, 10))
|
|
217
|
-
@deprecation_displayed = true
|
|
218
|
-
end
|
|
219
|
-
@delegate_dc_obj
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
class DeprecatedMulti < DelegateClass(Pipeline::Multi)
|
|
224
|
-
def initialize(pipeline)
|
|
225
|
-
super(pipeline)
|
|
226
|
-
@deprecation_displayed = false
|
|
227
|
-
end
|
|
64
|
+
private
|
|
228
65
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
@delegate_dc_obj
|
|
66
|
+
# Blocking commands inside transaction behave like non-blocking.
|
|
67
|
+
# It shouldn't be done though.
|
|
68
|
+
# https://redis.io/commands/blpop/#blpop-inside-a-multi--exec-transaction
|
|
69
|
+
def send_blocking_command(command, _timeout, &block)
|
|
70
|
+
send_command(command, &block)
|
|
235
71
|
end
|
|
236
72
|
end
|
|
237
73
|
|
|
@@ -244,23 +80,11 @@ class Redis
|
|
|
244
80
|
class Future < BasicObject
|
|
245
81
|
FutureNotReady = ::Redis::FutureNotReady.new
|
|
246
82
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
def initialize(command, transformation, timeout)
|
|
83
|
+
def initialize(command, coerce, exception)
|
|
250
84
|
@command = command
|
|
251
|
-
@transformation = transformation
|
|
252
|
-
@timeout = timeout
|
|
253
85
|
@object = FutureNotReady
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
def ==(_other)
|
|
257
|
-
message = +"The methods == and != are deprecated for Redis::Future and will be removed in 5.0.0"
|
|
258
|
-
message << " - You probably meant to call .value == or .value !="
|
|
259
|
-
message << " (#{::Kernel.caller(1, 1).first})\n"
|
|
260
|
-
|
|
261
|
-
::Redis.deprecate!(message)
|
|
262
|
-
|
|
263
|
-
super
|
|
86
|
+
@coerce = coerce
|
|
87
|
+
@exception = exception
|
|
264
88
|
end
|
|
265
89
|
|
|
266
90
|
def inspect
|
|
@@ -268,16 +92,12 @@ class Redis
|
|
|
268
92
|
end
|
|
269
93
|
|
|
270
94
|
def _set(object)
|
|
271
|
-
@object = @
|
|
95
|
+
@object = @coerce ? @coerce.call(object) : object
|
|
272
96
|
value
|
|
273
97
|
end
|
|
274
98
|
|
|
275
|
-
def _command
|
|
276
|
-
@command
|
|
277
|
-
end
|
|
278
|
-
|
|
279
99
|
def value
|
|
280
|
-
::Kernel.raise(@object) if @object.is_a?(::
|
|
100
|
+
::Kernel.raise(@object) if @exception && @object.is_a?(::StandardError)
|
|
281
101
|
@object
|
|
282
102
|
end
|
|
283
103
|
|
|
@@ -294,13 +114,18 @@ class Redis
|
|
|
294
114
|
def initialize(futures)
|
|
295
115
|
@futures = futures
|
|
296
116
|
@command = [:exec]
|
|
117
|
+
@object = FutureNotReady
|
|
297
118
|
end
|
|
298
119
|
|
|
299
120
|
def _set(replies)
|
|
300
|
-
@
|
|
301
|
-
future
|
|
121
|
+
@object = if replies
|
|
122
|
+
@futures.map.with_index do |future, index|
|
|
123
|
+
future._set(replies[index])
|
|
124
|
+
future.value
|
|
125
|
+
end
|
|
126
|
+
else
|
|
127
|
+
replies
|
|
302
128
|
end
|
|
303
|
-
replies
|
|
304
129
|
end
|
|
305
130
|
end
|
|
306
131
|
end
|