dalli 5.0.10 → 5.1.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 +47 -80
- data/Gemfile +1 -1
- data/README.md +7 -6
- data/lib/dalli/client.rb +272 -96
- data/lib/dalli/compressor.rb +5 -53
- data/lib/dalli/key_manager.rb +2 -23
- data/lib/dalli/pipelined_deleter.rb +29 -12
- data/lib/dalli/pipelined_getter.rb +57 -8
- data/lib/dalli/pipelined_setter.rb +15 -3
- data/lib/dalli/protocol/base.rb +55 -28
- data/lib/dalli/protocol/connection_manager.rb +9 -90
- data/lib/dalli/protocol/key_regularizer.rb +18 -1
- data/lib/dalli/protocol/meta.rb +89 -37
- data/lib/dalli/protocol/request_formatter.rb +110 -32
- data/lib/dalli/protocol/response_processor.rb +38 -55
- data/lib/dalli/protocol/value_compressor.rb +3 -30
- data/lib/dalli/protocol/value_serializer.rb +1 -1
- data/lib/dalli/socket.rb +4 -2
- data/lib/dalli/version.rb +2 -2
- data/lib/dalli.rb +0 -1
- data/lib/rack/session/dalli.rb +2 -1
- metadata +2 -3
- data/lib/dalli/json_serializer.rb +0 -28
data/lib/dalli/compressor.rb
CHANGED
|
@@ -13,54 +13,8 @@ module Dalli
|
|
|
13
13
|
Zlib::Deflate.deflate(data)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
# Compressed input is fed to zlib this many bytes at a time. MRI's zlib
|
|
19
|
-
# yields the output in small chunks; one that returns it instead still
|
|
20
|
-
# produces at most MAX_EXPANSION times this per step.
|
|
21
|
-
INFLATE_SLICE_BYTES = 64 * 1024
|
|
22
|
-
|
|
23
|
-
# max_bytes caps the decompressed size; inflating stops and raises as soon
|
|
24
|
-
# as the output passes it, so a small compressed value can't expand into
|
|
25
|
-
# gigabytes of memory.
|
|
26
|
-
def self.decompress(data, max_bytes: nil)
|
|
27
|
-
return Zlib::Inflate.inflate(data) unless may_exceed?(data, max_bytes)
|
|
28
|
-
|
|
29
|
-
inflate_within_limit(Zlib::Inflate.new, data, max_bytes)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# False when even the largest possible expansion stays within max_bytes,
|
|
33
|
-
# so the value can be inflated in one step
|
|
34
|
-
def self.may_exceed?(data, max_bytes)
|
|
35
|
-
max_bytes ? data.bytesize * MAX_EXPANSION > max_bytes : false
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def self.inflate_within_limit(inflater, data, max_bytes)
|
|
39
|
-
out = String.new(encoding: Encoding::BINARY)
|
|
40
|
-
0.step(data.bytesize - 1, INFLATE_SLICE_BYTES) do |pos|
|
|
41
|
-
inflate_slice(inflater, data.byteslice(pos, INFLATE_SLICE_BYTES), out, max_bytes)
|
|
42
|
-
# Anything after the end of the stream isn't part of the value, as
|
|
43
|
-
# with Zlib::Inflate.inflate; finish would return it as output
|
|
44
|
-
break if inflater.finished?
|
|
45
|
-
end
|
|
46
|
-
return out if inflater.finished?
|
|
47
|
-
|
|
48
|
-
append_within_limit(out, inflater.finish, max_bytes)
|
|
49
|
-
ensure
|
|
50
|
-
inflater.close
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Both yielded and returned output count toward the limit
|
|
54
|
-
def self.inflate_slice(inflater, slice, out, max_bytes)
|
|
55
|
-
rest = inflater.inflate(slice) { |chunk| append_within_limit(out, chunk, max_bytes) }
|
|
56
|
-
append_within_limit(out, rest, max_bytes)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def self.append_within_limit(out, chunk, max_bytes)
|
|
60
|
-
out << chunk if chunk
|
|
61
|
-
return out if out.bytesize <= max_bytes
|
|
62
|
-
|
|
63
|
-
raise Dalli::UnmarshalError, "Decompressed value exceeds #{max_bytes} bytes (decompressed_max_bytes)"
|
|
16
|
+
def self.decompress(data)
|
|
17
|
+
Zlib::Inflate.inflate(data)
|
|
64
18
|
end
|
|
65
19
|
end
|
|
66
20
|
|
|
@@ -78,11 +32,9 @@ module Dalli
|
|
|
78
32
|
io.string
|
|
79
33
|
end
|
|
80
34
|
|
|
81
|
-
def self.decompress(data
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
# MAX_WBITS + 16 makes zlib read the gzip format (and check its CRC)
|
|
85
|
-
Compressor.inflate_within_limit(Zlib::Inflate.new(Zlib::MAX_WBITS + 16), data, max_bytes)
|
|
35
|
+
def self.decompress(data)
|
|
36
|
+
io = StringIO.new(data, 'rb')
|
|
37
|
+
Zlib::GzipReader.new(io).read
|
|
86
38
|
end
|
|
87
39
|
end
|
|
88
40
|
end
|
data/lib/dalli/key_manager.rb
CHANGED
|
@@ -58,7 +58,7 @@ module Dalli
|
|
|
58
58
|
raise ArgumentError, 'key cannot be blank' unless key&.length&.positive?
|
|
59
59
|
|
|
60
60
|
key = key_with_namespace(key)
|
|
61
|
-
|
|
61
|
+
key.length > MAX_KEY_LENGTH ? truncated_key(key) : key
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
##
|
|
@@ -122,28 +122,7 @@ module Dalli
|
|
|
122
122
|
##
|
|
123
123
|
def truncated_key(key)
|
|
124
124
|
digest = digest_class.hexdigest(key)
|
|
125
|
-
|
|
126
|
-
truncated = "#{prefix}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
|
|
127
|
-
# A prefix with whitespace or non-ASCII characters sends the whole key
|
|
128
|
-
# base64-encoded, which can still be too long on the wire; shorten it.
|
|
129
|
-
while wire_length(truncated) > MAX_KEY_LENGTH && !prefix.empty?
|
|
130
|
-
prefix = prefix[0...-1]
|
|
131
|
-
truncated = "#{prefix}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
|
|
132
|
-
end
|
|
133
|
-
return truncated if wire_length(truncated) <= MAX_KEY_LENGTH
|
|
134
|
-
|
|
135
|
-
# Only possible with a digest_class whose digests aren't short hex strings
|
|
136
|
-
raise ArgumentError, "#{digest_class} digests are too long to fit in a truncated key"
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# memcached's key limit applies to the key as sent: in bytes, and after
|
|
140
|
-
# base64 encoding when the key needs it. A key can be within the limit in
|
|
141
|
-
# characters and still be rejected, and memcached's CLIENT_ERROR for it
|
|
142
|
-
# must never reach a pipelined get.
|
|
143
|
-
def wire_length(key)
|
|
144
|
-
return key.bytesize unless Dalli::Protocol::Meta::KeyRegularizer.required?(key)
|
|
145
|
-
|
|
146
|
-
((key.bytesize + 2) / 3) * 4
|
|
125
|
+
"#{key[0, prefix_length(digest)]}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
|
|
147
126
|
end
|
|
148
127
|
|
|
149
128
|
def prefix_length(digest)
|
|
@@ -15,17 +15,24 @@ module Dalli
|
|
|
15
15
|
##
|
|
16
16
|
# Deletes multiple keys from memcached.
|
|
17
17
|
#
|
|
18
|
+
# `req_options` is applied to every delete in the batch (see
|
|
19
|
+
# Dalli::Client#delete for the supported meta-delete keys).
|
|
20
|
+
#
|
|
18
21
|
# @param keys [Array<String>] keys to delete
|
|
19
|
-
# @return [Integer] the number of keys
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
22
|
+
# @return [Integer] the number of keys the server found and acted on. Only
|
|
23
|
+
# a key that did not exist decrements this count, so with
|
|
24
|
+
# `req_options: {invalidate: true}` it reports how many keys were
|
|
25
|
+
# tombstoned rather than removed. This is best-effort: a transient
|
|
26
|
+
# network error is retried automatically, and keys handled before the
|
|
27
|
+
# error are not recounted, so the result may under-report when a retry
|
|
28
|
+
# occurs. If a server remains unreachable after retrying, raises
|
|
29
|
+
# Dalli::NetworkError.
|
|
23
30
|
##
|
|
24
|
-
def process(keys)
|
|
31
|
+
def process(keys, req_options = nil)
|
|
25
32
|
return 0 if keys.empty?
|
|
26
33
|
|
|
27
34
|
@ring.lock do
|
|
28
|
-
groups = setup_requests(keys)
|
|
35
|
+
groups = setup_requests(keys, req_options)
|
|
29
36
|
finish_requests(groups)
|
|
30
37
|
end
|
|
31
38
|
rescue Dalli::RetryableNetworkError => e
|
|
@@ -36,9 +43,9 @@ module Dalli
|
|
|
36
43
|
|
|
37
44
|
private
|
|
38
45
|
|
|
39
|
-
def setup_requests(keys)
|
|
46
|
+
def setup_requests(keys, req_options = nil)
|
|
40
47
|
groups = groups_for_keys(keys)
|
|
41
|
-
make_delete_requests(groups)
|
|
48
|
+
make_delete_requests(groups, req_options)
|
|
42
49
|
groups
|
|
43
50
|
end
|
|
44
51
|
|
|
@@ -46,12 +53,20 @@ module Dalli
|
|
|
46
53
|
# Loop through the server-grouped sets of keys, writing
|
|
47
54
|
# the corresponding quiet delete requests to the appropriate servers
|
|
48
55
|
##
|
|
49
|
-
|
|
56
|
+
# NetworkError (which RetryableNetworkError subclasses) must propagate: the
|
|
57
|
+
# top-level rescue in #process retries the whole pipelined delete on it. A
|
|
58
|
+
# combined `rescue DalliError, NetworkError` would silently swallow
|
|
59
|
+
# RetryableNetworkError too -- since NetworkError < DalliError -- dropping
|
|
60
|
+
# this server's keys on a transient hiccup instead of retrying. Only a
|
|
61
|
+
# non-network DalliError should be swallowed here.
|
|
62
|
+
def make_delete_requests(groups, req_options = nil)
|
|
50
63
|
groups.each do |server, keys_for_server|
|
|
51
64
|
keys_for_server.select! do |key|
|
|
52
|
-
server.request(:pipelined_delete, key)
|
|
65
|
+
server.request(:pipelined_delete, key, req_options)
|
|
53
66
|
true
|
|
54
|
-
rescue
|
|
67
|
+
rescue Dalli::NetworkError
|
|
68
|
+
raise
|
|
69
|
+
rescue DalliError => e
|
|
55
70
|
Dalli.logger.debug { e.inspect }
|
|
56
71
|
Dalli.logger.debug { "unable to delete key #{key} for server #{server.name}" }
|
|
57
72
|
false
|
|
@@ -66,7 +81,9 @@ module Dalli
|
|
|
66
81
|
def finish_requests(groups)
|
|
67
82
|
groups.sum do |server, keys_for_server|
|
|
68
83
|
server.request(:finish_pipelined_delete, keys_for_server.size)
|
|
69
|
-
rescue
|
|
84
|
+
rescue Dalli::NetworkError
|
|
85
|
+
raise
|
|
86
|
+
rescue DalliError => e
|
|
70
87
|
Dalli.logger.debug { e.inspect }
|
|
71
88
|
Dalli.logger.debug { "unable to complete pipelined delete on server #{server.name}" }
|
|
72
89
|
0
|
|
@@ -20,13 +20,18 @@ module Dalli
|
|
|
20
20
|
##
|
|
21
21
|
# Yields, one at a time, keys and their values+attributes.
|
|
22
22
|
#
|
|
23
|
-
|
|
23
|
+
# `req_options` accepts :p_token/:l_token, applied to every key in the batch.
|
|
24
|
+
#
|
|
25
|
+
# A transient network error is retried automatically. If a server remains
|
|
26
|
+
# unreachable after retrying, raises Dalli::NetworkError.
|
|
27
|
+
#
|
|
28
|
+
def process(keys, req_options = nil, &block)
|
|
24
29
|
return {} if keys.empty?
|
|
25
30
|
|
|
26
31
|
@ring.lock do
|
|
27
32
|
# Stores partial results collected during interleaved send phase
|
|
28
33
|
@partial_results = {}
|
|
29
|
-
servers = setup_requests(keys)
|
|
34
|
+
servers = setup_requests(keys, req_options)
|
|
30
35
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
31
36
|
|
|
32
37
|
# First yield any partial results collected during interleaved send
|
|
@@ -40,6 +45,40 @@ module Dalli
|
|
|
40
45
|
retry
|
|
41
46
|
end
|
|
42
47
|
|
|
48
|
+
##
|
|
49
|
+
# Stale-aware bulk get across servers. Returns { key => metadata Hash } for
|
|
50
|
+
# the keys that were found; see Protocol::Meta#read_multi_with_metadata_req
|
|
51
|
+
# for why misses are absent rather than present with miss: true.
|
|
52
|
+
#
|
|
53
|
+
# Unlike #process this issues one request per server and reads its full
|
|
54
|
+
# response before moving on, rather than pipelining across servers: the
|
|
55
|
+
# metadata path has no interleaving support, and the stale-aware callers it
|
|
56
|
+
# serves fetch far smaller batches than get_multi does.
|
|
57
|
+
#
|
|
58
|
+
# `req_options` accepts :p_token/:l_token, applied to every key in the batch.
|
|
59
|
+
#
|
|
60
|
+
def process_with_metadata(keys, req_options = nil)
|
|
61
|
+
return {} if keys.empty?
|
|
62
|
+
|
|
63
|
+
@ring.lock do
|
|
64
|
+
results = {}
|
|
65
|
+
groups_for_keys(keys).each do |server, keys_for_server|
|
|
66
|
+
results.merge!(server.request(:read_multi_with_metadata_req, keys_for_server, req_options))
|
|
67
|
+
rescue Dalli::RetryableNetworkError
|
|
68
|
+
raise
|
|
69
|
+
rescue DalliError, NetworkError => e
|
|
70
|
+
Dalli.logger.debug { e.inspect }
|
|
71
|
+
Dalli.logger.debug { "unable to get keys for server #{server.name}" }
|
|
72
|
+
end
|
|
73
|
+
results.transform_keys! { |key| @key_manager.key_without_namespace(key) }
|
|
74
|
+
results
|
|
75
|
+
end
|
|
76
|
+
rescue Dalli::RetryableNetworkError => e
|
|
77
|
+
Dalli.logger.debug { e.inspect }
|
|
78
|
+
Dalli.logger.debug { 'retrying pipelined get with metadata because of network error' }
|
|
79
|
+
retry
|
|
80
|
+
end
|
|
81
|
+
|
|
43
82
|
private
|
|
44
83
|
|
|
45
84
|
def yield_partial_results
|
|
@@ -49,9 +88,9 @@ module Dalli
|
|
|
49
88
|
@partial_results.clear
|
|
50
89
|
end
|
|
51
90
|
|
|
52
|
-
def setup_requests(keys)
|
|
91
|
+
def setup_requests(keys, req_options = nil)
|
|
53
92
|
groups = groups_for_keys(keys)
|
|
54
|
-
make_getkq_requests(groups)
|
|
93
|
+
make_getkq_requests(groups, req_options)
|
|
55
94
|
|
|
56
95
|
# TODO: How does this exit on a NetworkError
|
|
57
96
|
finish_queries(groups.keys)
|
|
@@ -65,17 +104,27 @@ module Dalli
|
|
|
65
104
|
# on the wire by switching from getkq to getq, and using
|
|
66
105
|
# the opaque value to match requests to responses.
|
|
67
106
|
##
|
|
68
|
-
def make_getkq_requests(groups)
|
|
107
|
+
def make_getkq_requests(groups, req_options = nil)
|
|
69
108
|
groups.each do |server, keys_for_server|
|
|
70
109
|
if keys_for_server.size <= INTERLEAVE_THRESHOLD
|
|
71
110
|
# Small batch - send all at once (existing behavior)
|
|
72
|
-
server.request(:pipelined_get, keys_for_server)
|
|
111
|
+
server.request(:pipelined_get, keys_for_server, req_options)
|
|
73
112
|
else
|
|
74
113
|
# Large batch - interleave sends with response draining
|
|
75
114
|
# Pass @partial_results directly to avoid hash allocation/merge overhead
|
|
76
|
-
server.request(:pipelined_get_interleaved, keys_for_server, CHUNK_SIZE, @partial_results)
|
|
115
|
+
server.request(:pipelined_get_interleaved, keys_for_server, CHUNK_SIZE, @partial_results, req_options)
|
|
77
116
|
end
|
|
78
|
-
|
|
117
|
+
# NetworkError (which RetryableNetworkError subclasses) must propagate:
|
|
118
|
+
# #process's top-level rescue retries the whole pipelined get on it. This
|
|
119
|
+
# rescue used to catch DalliError and NetworkError together -- since
|
|
120
|
+
# NetworkError < DalliError, that silently swallowed RetryableNetworkError
|
|
121
|
+
# too, dropping this server's keys from the result on a transient hiccup
|
|
122
|
+
# instead of retrying, with nothing surfaced above debug-level logging.
|
|
123
|
+
# Only a non-network DalliError (this server genuinely can't serve these
|
|
124
|
+
# keys) should be swallowed here.
|
|
125
|
+
rescue Dalli::NetworkError
|
|
126
|
+
raise
|
|
127
|
+
rescue DalliError => e
|
|
79
128
|
Dalli.logger.debug { e.inspect }
|
|
80
129
|
Dalli.logger.debug { "unable to get keys for server #{server.name}" }
|
|
81
130
|
end
|
|
@@ -14,7 +14,9 @@ module Dalli
|
|
|
14
14
|
|
|
15
15
|
##
|
|
16
16
|
# Writes multiple key-value pairs to memcached.
|
|
17
|
-
#
|
|
17
|
+
#
|
|
18
|
+
# A transient network error is retried automatically. If a server remains
|
|
19
|
+
# unreachable after retrying, raises Dalli::NetworkError.
|
|
18
20
|
#
|
|
19
21
|
# @param hash [Hash] key-value pairs to set
|
|
20
22
|
# @param ttl [Integer] time-to-live in seconds
|
|
@@ -46,13 +48,21 @@ module Dalli
|
|
|
46
48
|
# Loop through the server-grouped sets of keys, writing
|
|
47
49
|
# the corresponding quiet set requests to the appropriate servers
|
|
48
50
|
##
|
|
51
|
+
# NetworkError (which RetryableNetworkError subclasses) must propagate: the
|
|
52
|
+
# top-level rescue in #process retries the whole pipelined set on it. A
|
|
53
|
+
# combined `rescue DalliError, NetworkError` would silently swallow
|
|
54
|
+
# RetryableNetworkError too -- since NetworkError < DalliError -- dropping
|
|
55
|
+
# this server's keys on a transient hiccup instead of retrying. Only a
|
|
56
|
+
# non-network DalliError should be swallowed here.
|
|
49
57
|
def make_set_requests(groups, hash, ttl, req_options)
|
|
50
58
|
groups.each do |server, keys_for_server|
|
|
51
59
|
keys_for_server.each do |key|
|
|
52
60
|
original_key = @key_manager.key_without_namespace(key)
|
|
53
61
|
value = hash[original_key]
|
|
54
62
|
server.request(:pipelined_set, key, value, ttl, req_options)
|
|
55
|
-
rescue
|
|
63
|
+
rescue Dalli::NetworkError
|
|
64
|
+
raise
|
|
65
|
+
rescue DalliError => e
|
|
56
66
|
Dalli.logger.debug { e.inspect }
|
|
57
67
|
Dalli.logger.debug { "unable to set key #{key} for server #{server.name}" }
|
|
58
68
|
end
|
|
@@ -65,7 +75,9 @@ module Dalli
|
|
|
65
75
|
def finish_requests(servers)
|
|
66
76
|
servers.each do |server|
|
|
67
77
|
server.request(:noop)
|
|
68
|
-
rescue
|
|
78
|
+
rescue Dalli::NetworkError
|
|
79
|
+
raise
|
|
80
|
+
rescue DalliError => e
|
|
69
81
|
Dalli.logger.debug { e.inspect }
|
|
70
82
|
Dalli.logger.debug { "unable to complete pipelined set on server #{server.name}" }
|
|
71
83
|
end
|
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -35,14 +35,6 @@ module Dalli
|
|
|
35
35
|
@raw_mode
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
# True when values should come back as stored: the client is in raw mode,
|
|
39
|
-
# or this request passed raw: true. Retrieval paths use it to skip
|
|
40
|
-
# requesting flags, so a stored value is never deserialized or
|
|
41
|
-
# decompressed for a caller who asked for raw bytes.
|
|
42
|
-
def raw_request?(options)
|
|
43
|
-
raw_mode? || (options.is_a?(Hash) && options[:raw]) ? true : false
|
|
44
|
-
end
|
|
45
|
-
|
|
46
38
|
# Chokepoint method for error handling and ensuring liveness
|
|
47
39
|
def request(opkey, *args)
|
|
48
40
|
verify_state(opkey)
|
|
@@ -86,6 +78,19 @@ module Dalli
|
|
|
86
78
|
# particular memcached instance is available for use.
|
|
87
79
|
def alive?
|
|
88
80
|
ensure_connected!
|
|
81
|
+
rescue Dalli::RetryableNetworkError => e
|
|
82
|
+
# A single connection attempt failure is retryable -- the same
|
|
83
|
+
# contract #request enforces on the send/receive path. Retrying here
|
|
84
|
+
# lets error_on_request!'s own fail-count threshold decide when to
|
|
85
|
+
# give up: it keeps raising RetryableNetworkError until
|
|
86
|
+
# socket_max_failures is reached, then raises a terminal
|
|
87
|
+
# NetworkError via down!, which the next rescue converts to false.
|
|
88
|
+
# Without this, a single transient hiccup during the liveness check
|
|
89
|
+
# itself -- as opposed to an actual request -- would report this
|
|
90
|
+
# server as not alive even though it would have reconnected fine.
|
|
91
|
+
Dalli.logger.debug { e.inspect }
|
|
92
|
+
Dalli.logger.debug { "retrying connection attempt to #{name} because of network error" }
|
|
93
|
+
retry
|
|
89
94
|
rescue Dalli::NetworkError
|
|
90
95
|
# ensure_connected! raises a NetworkError if connection fails. We
|
|
91
96
|
# want to capture that error and convert it to a boolean value here.
|
|
@@ -121,15 +126,7 @@ module Dalli
|
|
|
121
126
|
reconnect_on_pipeline_complete!
|
|
122
127
|
values = nil
|
|
123
128
|
|
|
124
|
-
|
|
125
|
-
# this rescue, so an Errno or Timeout::Error it raises reaches the
|
|
126
|
-
# caller instead of being mistaken for a network failure (which closed
|
|
127
|
-
# the connection and retried the whole get_multi, yielding keys twice).
|
|
128
|
-
begin
|
|
129
|
-
response_buffer.read
|
|
130
|
-
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
|
|
131
|
-
@connection_manager.error_on_request!(e)
|
|
132
|
-
end
|
|
129
|
+
response_buffer.read
|
|
133
130
|
|
|
134
131
|
status, cas, key, value = response_buffer.process_single_getk_response
|
|
135
132
|
# status is not nil only if we have a full response to parse
|
|
@@ -155,6 +152,8 @@ module Dalli
|
|
|
155
152
|
end
|
|
156
153
|
|
|
157
154
|
values || {}
|
|
155
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
|
|
156
|
+
@connection_manager.error_on_request!(e)
|
|
158
157
|
end
|
|
159
158
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
160
159
|
|
|
@@ -168,13 +167,10 @@ module Dalli
|
|
|
168
167
|
@connection_manager.abort_request!
|
|
169
168
|
return true unless connected?
|
|
170
169
|
|
|
171
|
-
# Closes the connection, which ensures that our connection
|
|
172
|
-
# clean state for future requests
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
# otherwise mark a healthy server down.
|
|
176
|
-
Dalli.logger.warn { "#{name} didn't finish a get_multi within socket_timeout; closing the connection" }
|
|
177
|
-
@connection_manager.close
|
|
170
|
+
# Closes the connection, which ensures that our connection
|
|
171
|
+
# is in a clean state for future requests
|
|
172
|
+
@connection_manager.error_on_request!('External timeout')
|
|
173
|
+
rescue NetworkError
|
|
178
174
|
true
|
|
179
175
|
end
|
|
180
176
|
|
|
@@ -254,13 +250,44 @@ module Dalli
|
|
|
254
250
|
opts[:cache_nils] ? true : false
|
|
255
251
|
end
|
|
256
252
|
|
|
253
|
+
# Extracts opaque routing-token kwargs (:p_token, :l_token) from a
|
|
254
|
+
# request-options Hash so they can be splatted into a RequestFormatter
|
|
255
|
+
# call. Returns {} when neither is set, so the splat is a no-op on the
|
|
256
|
+
# common path. Validation (type, forbidden bytes) happens at the
|
|
257
|
+
# wire-formatter level, where it can raise uniformly regardless of how
|
|
258
|
+
# the token reached the formatter.
|
|
259
|
+
def routing_token_kwargs(opts)
|
|
260
|
+
return {} unless opts.is_a?(Hash)
|
|
261
|
+
return {} unless opts[:p_token] || opts[:l_token]
|
|
262
|
+
|
|
263
|
+
{ p_token: opts[:p_token], l_token: opts[:l_token] }
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Maps the client-facing meta-delete options onto RequestFormatter's flag
|
|
267
|
+
# names, so they can be splatted into a meta_delete call. Returns {} when
|
|
268
|
+
# none are set, keeping the splat a no-op on the common path.
|
|
269
|
+
#
|
|
270
|
+
# :tombstone_ttl becomes the T flag, which is the same TTL the formatter
|
|
271
|
+
# already accepted -- deliberately not a second TTL parameter, since two
|
|
272
|
+
# would allow emitting two T tokens in one request. It is sanitized like
|
|
273
|
+
# every other TTL Dalli sends.
|
|
274
|
+
def tombstone_kwargs(opts)
|
|
275
|
+
return {} unless opts.is_a?(Hash)
|
|
276
|
+
|
|
277
|
+
kwargs = {}
|
|
278
|
+
kwargs[:stale] = true if opts[:invalidate]
|
|
279
|
+
kwargs[:ttl] = TtlSanitizer.sanitize(Integer(opts[:tombstone_ttl])) if opts[:tombstone_ttl]
|
|
280
|
+
kwargs[:drop_value] = true if opts[:drop_value]
|
|
281
|
+
kwargs
|
|
282
|
+
end
|
|
283
|
+
|
|
257
284
|
def connect
|
|
258
285
|
@connection_manager.establish_connection
|
|
259
286
|
@version = version
|
|
260
287
|
up!
|
|
261
288
|
end
|
|
262
289
|
|
|
263
|
-
def pipelined_get(keys)
|
|
290
|
+
def pipelined_get(keys, options = nil)
|
|
264
291
|
# Clear buffer to remove any stale data from interrupted operations.
|
|
265
292
|
# Use clear (not reset) to keep pipeline_complete? = true, which is
|
|
266
293
|
# the expected state before pipeline_response_setup is called.
|
|
@@ -268,7 +295,7 @@ module Dalli
|
|
|
268
295
|
|
|
269
296
|
req = +''
|
|
270
297
|
keys.each do |key|
|
|
271
|
-
req << quiet_get_request(key)
|
|
298
|
+
req << quiet_get_request(key, options)
|
|
272
299
|
end
|
|
273
300
|
# Could send noop here instead of in pipeline_response_setup
|
|
274
301
|
write(req)
|
|
@@ -277,7 +304,7 @@ module Dalli
|
|
|
277
304
|
# For large batches, interleave writing requests with draining responses.
|
|
278
305
|
# This prevents socket buffer deadlock when sending many keys.
|
|
279
306
|
# Populates the provided results hash with any responses drained during send.
|
|
280
|
-
def pipelined_get_interleaved(keys, chunk_size, results)
|
|
307
|
+
def pipelined_get_interleaved(keys, chunk_size, results, options = nil)
|
|
281
308
|
# Initialize the response buffer for draining during send phase
|
|
282
309
|
response_buffer.ensure_ready
|
|
283
310
|
|
|
@@ -285,7 +312,7 @@ module Dalli
|
|
|
285
312
|
# Build and write this chunk of requests
|
|
286
313
|
req = +''
|
|
287
314
|
chunk.each do |key|
|
|
288
|
-
req << quiet_get_request(key)
|
|
315
|
+
req << quiet_get_request(key, options)
|
|
289
316
|
end
|
|
290
317
|
write(req)
|
|
291
318
|
@connection_manager.flush
|