dalli 5.2.6 → 5.3.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 +48 -0
- data/README.md +30 -0
- data/lib/dalli/client.rb +189 -76
- data/lib/dalli/key_grouping.rb +50 -0
- data/lib/dalli/key_manager.rb +30 -0
- data/lib/dalli/pipelined_deleter.rb +7 -16
- data/lib/dalli/pipelined_getter.rb +64 -58
- data/lib/dalli/pipelined_setter.rb +6 -15
- data/lib/dalli/protocol/base.rb +117 -41
- data/lib/dalli/protocol/connection_manager.rb +106 -2
- data/lib/dalli/protocol/key_regularizer.rb +5 -0
- data/lib/dalli/protocol/meta.rb +37 -9
- data/lib/dalli/protocol/request_formatter.rb +62 -33
- data/lib/dalli/protocol/response_processor.rb +49 -6
- data/lib/dalli/protocol/ttl_sanitizer.rb +13 -4
- data/lib/dalli/ring.rb +4 -0
- data/lib/dalli/servers_arg_normalizer.rb +19 -1
- data/lib/dalli/socket.rb +17 -2
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +14 -1
- data/lib/rack/session/dalli.rb +12 -3
- metadata +2 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dalli
|
|
4
|
+
##
|
|
5
|
+
# Groups a multi-key operation's keys by server, for PipelinedGetter,
|
|
6
|
+
# PipelinedSetter and PipelinedDeleter. Each of them is created for one
|
|
7
|
+
# operation, and retries the whole operation after a RetryableNetworkError.
|
|
8
|
+
#
|
|
9
|
+
# A key with no server to go to is left out, as it always has been: a
|
|
10
|
+
# server that was down before the operation started is skipped, and with
|
|
11
|
+
# failover its keys go to another server. Without failover a retry is
|
|
12
|
+
# different. The server was there for the first attempt and failed while
|
|
13
|
+
# serving it, so if the retry finds it unreachable, the operation raises
|
|
14
|
+
# Dalli::NetworkError rather than returning that server's keys as misses or
|
|
15
|
+
# leaving them unwritten. With failover the keys go to another server, or
|
|
16
|
+
# are left out when none is left, as for a server down from the start.
|
|
17
|
+
##
|
|
18
|
+
module KeyGrouping
|
|
19
|
+
NO_KEYS = [].freeze
|
|
20
|
+
private_constant :NO_KEYS
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# action names the operation in the debug log ('get', 'set', 'delete')
|
|
25
|
+
def groups_for_keys(keys, action)
|
|
26
|
+
groups = @ring.keys_grouped_by_server(keys)
|
|
27
|
+
unrouted = groups.delete(nil)
|
|
28
|
+
check_unrouted!(unrouted) unless @ring.failover?
|
|
29
|
+
if unrouted
|
|
30
|
+
Dalli.logger.debug do
|
|
31
|
+
"unable to #{action} #{unrouted.length} keys because no matching server was found"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
groups
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The first attempt records the keys that already had no server; a retry
|
|
38
|
+
# raises for any other key that has none now
|
|
39
|
+
def check_unrouted!(unrouted)
|
|
40
|
+
return @unrouted_on_first_attempt = unrouted || NO_KEYS unless @unrouted_on_first_attempt
|
|
41
|
+
return unless unrouted
|
|
42
|
+
|
|
43
|
+
lost = unrouted - @unrouted_on_first_attempt
|
|
44
|
+
return if lost.empty?
|
|
45
|
+
|
|
46
|
+
raise Dalli::NetworkError, "#{lost.length} keys had a server that failed during this request " \
|
|
47
|
+
'and is still unreachable after retrying'
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/dalli/key_manager.rb
CHANGED
|
@@ -71,6 +71,31 @@ module Dalli
|
|
|
71
71
|
"#{evaluate_namespace}#{namespace_separator}#{key}"
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
+
##
|
|
75
|
+
# Validates the keys for a multi-get. Returns a Hash from each key as
|
|
76
|
+
# sent to the caller's key (as a String) it came from, for #caller_key.
|
|
77
|
+
# A reply names the key as sent: namespaced, digested when too long, and
|
|
78
|
+
# decoded from base64 as UTF-8 when it was encoded, so it can't be turned
|
|
79
|
+
# back into the caller's key on its own. Keys are compared as bytes, and
|
|
80
|
+
# a key given more than once is sent once.
|
|
81
|
+
##
|
|
82
|
+
def caller_keys_by_wire_key(keys)
|
|
83
|
+
keys.each_with_object({}) do |key, caller_keys|
|
|
84
|
+
key = key.to_s
|
|
85
|
+
caller_keys[bytes_form(validate_key(key))] ||= key
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
##
|
|
90
|
+
# The caller's key for a multi-get reply's key, using the Hash from
|
|
91
|
+
# #caller_keys_by_wire_key. Returns nil for a key that wasn't requested.
|
|
92
|
+
##
|
|
93
|
+
def caller_key(caller_keys, reply_key)
|
|
94
|
+
# A non-ASCII key in the Hash is binary, and a reply's key decoded from
|
|
95
|
+
# base64 is UTF-8; ASCII keys, the usual case, are found as is
|
|
96
|
+
caller_keys[reply_key] || caller_keys[reply_key.b]
|
|
97
|
+
end
|
|
98
|
+
|
|
74
99
|
def key_without_namespace(key)
|
|
75
100
|
return key if namespace.nil?
|
|
76
101
|
|
|
@@ -146,6 +171,11 @@ module Dalli
|
|
|
146
171
|
((key.bytesize + 2) / 3) * 4
|
|
147
172
|
end
|
|
148
173
|
|
|
174
|
+
# ASCII keys compare equal in any encoding as is
|
|
175
|
+
def bytes_form(key)
|
|
176
|
+
key.ascii_only? ? key : key.b
|
|
177
|
+
end
|
|
178
|
+
|
|
149
179
|
def prefix_length(digest)
|
|
150
180
|
return TRUNCATED_KEY_TARGET_SIZE - (TRUNCATED_KEY_SEPARATOR.length + digest.length) if namespace.nil?
|
|
151
181
|
|
|
@@ -7,6 +7,8 @@ module Dalli
|
|
|
7
7
|
# and using quiet mode to minimize round trips.
|
|
8
8
|
##
|
|
9
9
|
class PipelinedDeleter
|
|
10
|
+
include KeyGrouping
|
|
11
|
+
|
|
10
12
|
def initialize(ring, key_manager)
|
|
11
13
|
@ring = ring
|
|
12
14
|
@key_manager = key_manager
|
|
@@ -25,8 +27,9 @@ module Dalli
|
|
|
25
27
|
# tombstoned rather than removed. This is best-effort: a transient
|
|
26
28
|
# network error is retried automatically, and keys handled before the
|
|
27
29
|
# error are not recounted, so the result may under-report when a retry
|
|
28
|
-
# occurs. If a server
|
|
29
|
-
# Dalli::NetworkError
|
|
30
|
+
# occurs. If a server stops during the call and is still unreachable
|
|
31
|
+
# after retrying, raises Dalli::NetworkError without failover (see
|
|
32
|
+
# KeyGrouping).
|
|
30
33
|
##
|
|
31
34
|
def process(keys, req_options = nil)
|
|
32
35
|
return 0 if keys.empty?
|
|
@@ -44,7 +47,8 @@ module Dalli
|
|
|
44
47
|
private
|
|
45
48
|
|
|
46
49
|
def setup_requests(keys, req_options = nil)
|
|
47
|
-
|
|
50
|
+
validated_keys = keys.map { |k| @key_manager.validate_key(k.to_s) }
|
|
51
|
+
groups = groups_for_keys(validated_keys, 'delete')
|
|
48
52
|
make_delete_requests(groups, req_options)
|
|
49
53
|
groups
|
|
50
54
|
end
|
|
@@ -89,18 +93,5 @@ module Dalli
|
|
|
89
93
|
0
|
|
90
94
|
end
|
|
91
95
|
end
|
|
92
|
-
|
|
93
|
-
def groups_for_keys(keys)
|
|
94
|
-
validated_keys = keys.map { |k| @key_manager.validate_key(k.to_s) }
|
|
95
|
-
groups = @ring.keys_grouped_by_server(validated_keys)
|
|
96
|
-
|
|
97
|
-
if (unfound_keys = groups.delete(nil))
|
|
98
|
-
Dalli.logger.debug do
|
|
99
|
-
"unable to delete #{unfound_keys.length} keys because no matching server was found"
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
groups
|
|
104
|
-
end
|
|
105
96
|
end
|
|
106
97
|
end
|
|
@@ -5,6 +5,8 @@ module Dalli
|
|
|
5
5
|
# Contains logic for the pipelined gets implemented by the client.
|
|
6
6
|
##
|
|
7
7
|
class PipelinedGetter
|
|
8
|
+
include KeyGrouping
|
|
9
|
+
|
|
8
10
|
# For large batches, interleave sends with response draining to prevent
|
|
9
11
|
# socket buffer deadlock. Only kicks in above this threshold.
|
|
10
12
|
INTERLEAVE_THRESHOLD = 10_000
|
|
@@ -25,8 +27,14 @@ module Dalli
|
|
|
25
27
|
# With return_cas: false the CAS value is not requested, and each yielded
|
|
26
28
|
# CAS is 0.
|
|
27
29
|
#
|
|
28
|
-
# A transient network error is retried automatically
|
|
29
|
-
#
|
|
30
|
+
# A transient network error is retried automatically, starting the batch
|
|
31
|
+
# again, so a key can be yielded twice. If a server stops during the call
|
|
32
|
+
# and is still unreachable after retrying, raises Dalli::NetworkError
|
|
33
|
+
# without failover (see KeyGrouping).
|
|
34
|
+
#
|
|
35
|
+
# A value that can't be deserialized raises Dalli::UnmarshalError, as get
|
|
36
|
+
# does, once every server's replies are read, so the connections stay in
|
|
37
|
+
# step. The other values are yielded first.
|
|
30
38
|
#
|
|
31
39
|
def process(keys, req_options = nil, return_cas: true, &block)
|
|
32
40
|
return {} if keys.empty?
|
|
@@ -37,13 +45,15 @@ module Dalli
|
|
|
37
45
|
begin
|
|
38
46
|
# Stores partial results collected during interleaved send phase
|
|
39
47
|
@partial_results = {}
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
@unmarshal_error = nil
|
|
49
|
+
@caller_keys = @key_manager.caller_keys_by_wire_key(keys.flatten)
|
|
50
|
+
setup_requests(servers, @caller_keys.keys, req_options, return_cas: return_cas)
|
|
42
51
|
|
|
43
52
|
# First yield any partial results collected during interleaved send
|
|
44
53
|
yield_partial_results(&block)
|
|
45
54
|
|
|
46
|
-
fetch_responses(servers,
|
|
55
|
+
fetch_responses(servers, @ring.socket_timeout, &block) until servers.empty?
|
|
56
|
+
raise @unmarshal_error if @unmarshal_error
|
|
47
57
|
ensure
|
|
48
58
|
abort_unfinished(servers)
|
|
49
59
|
end
|
|
@@ -64,6 +74,9 @@ module Dalli
|
|
|
64
74
|
# metadata path has no interleaving support, and the stale-aware callers it
|
|
65
75
|
# serves fetch far smaller batches than get_multi does.
|
|
66
76
|
#
|
|
77
|
+
# A value that can't be deserialized raises Dalli::UnmarshalError, once its
|
|
78
|
+
# server's replies are read. The servers after it aren't asked.
|
|
79
|
+
#
|
|
67
80
|
# `req_options` accepts :p_token/:l_token, applied to every key in the batch.
|
|
68
81
|
#
|
|
69
82
|
# A transient network error is retried automatically. A terminal
|
|
@@ -74,15 +87,18 @@ module Dalli
|
|
|
74
87
|
|
|
75
88
|
@ring.lock do
|
|
76
89
|
results = {}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
90
|
+
caller_keys = @key_manager.caller_keys_by_wire_key(keys)
|
|
91
|
+
groups_for_keys(caller_keys.keys, 'get').each do |server, keys_for_server|
|
|
92
|
+
server.request(:read_multi_with_metadata_req, keys_for_server, req_options).each_pair do |key, metadata|
|
|
93
|
+
caller_key = @key_manager.caller_key(caller_keys, key)
|
|
94
|
+
results[caller_key] = metadata if caller_key
|
|
95
|
+
end
|
|
96
|
+
rescue Dalli::NetworkError, Dalli::UnmarshalError
|
|
80
97
|
raise
|
|
81
98
|
rescue DalliError => e
|
|
82
99
|
Dalli.logger.debug { e.inspect }
|
|
83
100
|
Dalli.logger.debug { "unable to get keys for server #{server.name}" }
|
|
84
101
|
end
|
|
85
|
-
results.transform_keys! { |key| @key_manager.key_without_namespace(key) }
|
|
86
102
|
results
|
|
87
103
|
end
|
|
88
104
|
rescue Dalli::RetryableNetworkError => e
|
|
@@ -95,7 +111,8 @@ module Dalli
|
|
|
95
111
|
|
|
96
112
|
def yield_partial_results
|
|
97
113
|
@partial_results.each_pair do |key, value_list|
|
|
98
|
-
|
|
114
|
+
caller_key = @key_manager.caller_key(@caller_keys, key)
|
|
115
|
+
yield caller_key, value_list if caller_key
|
|
99
116
|
end
|
|
100
117
|
@partial_results.clear
|
|
101
118
|
end
|
|
@@ -104,15 +121,15 @@ module Dalli
|
|
|
104
121
|
# the next server's, so memcached is already answering the first servers
|
|
105
122
|
# while the rest are prepared. Adds the servers with a pending response
|
|
106
123
|
# to started.
|
|
107
|
-
def setup_requests(started,
|
|
108
|
-
groups_for_keys(
|
|
124
|
+
def setup_requests(started, wire_keys, req_options = nil, return_cas: true)
|
|
125
|
+
groups_for_keys(wire_keys, 'get').each do |server, keys_for_server|
|
|
109
126
|
make_getkq_request(server, keys_for_server, req_options, return_cas: return_cas)
|
|
110
127
|
next unless server.connected?
|
|
111
128
|
|
|
112
129
|
started << server
|
|
113
130
|
finish_query_for_server(server)
|
|
114
131
|
rescue Dalli::NetworkError
|
|
115
|
-
|
|
132
|
+
abort_unfinished(started)
|
|
116
133
|
raise
|
|
117
134
|
rescue Dalli::DalliError
|
|
118
135
|
started.delete(server)
|
|
@@ -160,33 +177,26 @@ module Dalli
|
|
|
160
177
|
raise
|
|
161
178
|
end
|
|
162
179
|
|
|
163
|
-
#
|
|
164
|
-
#
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
# When #process stops before reading every server's replies (the caller's
|
|
171
|
-
# block raises or breaks, a Timeout.timeout fires, a value can't be
|
|
172
|
-
# deserialized), the rest are still on the socket and some may be in the
|
|
173
|
-
# response buffer. Closing those connections keeps them from being read
|
|
174
|
-
# as a later request's replies. Aborts that #process already handled
|
|
175
|
-
# emptied servers.
|
|
180
|
+
# When #process stops before reading every server's replies (a server
|
|
181
|
+
# fails, the caller's block raises or breaks, a Timeout.timeout fires),
|
|
182
|
+
# the rest are still on the socket and some may be in the response buffer.
|
|
183
|
+
# Closing those connections keeps them from being read as a later
|
|
184
|
+
# request's replies. Empties servers, so they aren't aborted again.
|
|
176
185
|
def abort_unfinished(servers)
|
|
177
|
-
servers.each
|
|
186
|
+
servers.each(&:pipeline_abort)
|
|
178
187
|
servers.clear
|
|
179
188
|
end
|
|
180
189
|
|
|
181
|
-
# Reads the replies available, removing finished servers from servers
|
|
182
|
-
|
|
190
|
+
# Reads the replies available, removing finished servers from servers.
|
|
191
|
+
# The wait is for any server to send something, so it starts again after
|
|
192
|
+
# each read: a large batch that keeps arriving isn't cut off.
|
|
193
|
+
def fetch_responses(servers, timeout, &block)
|
|
183
194
|
# Remove any servers which are not connected
|
|
184
195
|
servers.select!(&:connected?)
|
|
185
196
|
return if servers.empty?
|
|
186
197
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
return abort_with_timeout(servers) if readable_servers.empty?
|
|
198
|
+
readable_servers = servers_with_response(servers, timeout)
|
|
199
|
+
time_out(servers) if readable_servers.empty?
|
|
190
200
|
|
|
191
201
|
# Loop through the servers with responses, and
|
|
192
202
|
# delete any from our list that are finished
|
|
@@ -196,33 +206,42 @@ module Dalli
|
|
|
196
206
|
rescue NetworkError
|
|
197
207
|
# Abort and raise if we encountered a network error. This triggers
|
|
198
208
|
# a retry at the top level on RetryableNetworkError.
|
|
199
|
-
|
|
209
|
+
abort_unfinished(servers)
|
|
200
210
|
raise
|
|
201
211
|
end
|
|
202
212
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
# Swallows Dalli::NetworkError
|
|
211
|
-
def abort_with_timeout(servers)
|
|
213
|
+
# No server sent anything for socket_timeout. Each counts it as a failed
|
|
214
|
+
# read, as get does, so it's retried and, after socket_max_failures,
|
|
215
|
+
# marked down. Raises the first server's error.
|
|
216
|
+
def time_out(servers)
|
|
217
|
+
error = nil
|
|
212
218
|
servers.each do |server|
|
|
213
|
-
|
|
219
|
+
server.pipeline_timed_out!
|
|
220
|
+
rescue Dalli::NetworkError => e
|
|
221
|
+
error ||= e
|
|
214
222
|
end
|
|
215
|
-
|
|
223
|
+
raise error
|
|
216
224
|
end
|
|
217
225
|
|
|
218
226
|
# Processes responses from a server. Returns true if there are no
|
|
219
227
|
# additional responses from this server.
|
|
228
|
+
#
|
|
229
|
+
# A value that couldn't be deserialized is raised once the server's
|
|
230
|
+
# replies are all read; it's held until the other servers' are too.
|
|
220
231
|
def process_server(server)
|
|
232
|
+
# A reply names the key as sent; it's yielded under the caller's key
|
|
221
233
|
server.pipeline_next_responses do |key, value, cas|
|
|
222
|
-
|
|
234
|
+
caller_key = @key_manager.caller_key(@caller_keys, key)
|
|
235
|
+
yield caller_key, [value, cas] if caller_key
|
|
223
236
|
end
|
|
224
237
|
|
|
225
238
|
server.pipeline_complete?
|
|
239
|
+
rescue Dalli::UnmarshalError => e
|
|
240
|
+
# Raised by the caller's block, with replies still to read
|
|
241
|
+
raise unless server.pipeline_complete?
|
|
242
|
+
|
|
243
|
+
@unmarshal_error ||= e
|
|
244
|
+
true
|
|
226
245
|
end
|
|
227
246
|
|
|
228
247
|
def servers_with_response(servers, timeout)
|
|
@@ -237,18 +256,5 @@ module Dalli
|
|
|
237
256
|
# reuses the sockets already fetched above.
|
|
238
257
|
readable.filter_map { |sock| (idx = sockets.index(sock)) && servers[idx] }
|
|
239
258
|
end
|
|
240
|
-
|
|
241
|
-
def groups_for_keys(*keys)
|
|
242
|
-
keys.flatten!
|
|
243
|
-
keys.map! { |a| @key_manager.validate_key(a.to_s) }
|
|
244
|
-
groups = @ring.keys_grouped_by_server(keys)
|
|
245
|
-
if (unfound_keys = groups.delete(nil))
|
|
246
|
-
Dalli.logger.debug do
|
|
247
|
-
"unable to get keys for #{unfound_keys.length} keys " \
|
|
248
|
-
'because no matching server was found'
|
|
249
|
-
end
|
|
250
|
-
end
|
|
251
|
-
groups
|
|
252
|
-
end
|
|
253
259
|
end
|
|
254
260
|
end
|
|
@@ -7,6 +7,8 @@ module Dalli
|
|
|
7
7
|
# and using quiet mode to minimize round trips.
|
|
8
8
|
##
|
|
9
9
|
class PipelinedSetter
|
|
10
|
+
include KeyGrouping
|
|
11
|
+
|
|
10
12
|
def initialize(ring, key_manager)
|
|
11
13
|
@ring = ring
|
|
12
14
|
@key_manager = key_manager
|
|
@@ -15,8 +17,9 @@ module Dalli
|
|
|
15
17
|
##
|
|
16
18
|
# Writes multiple key-value pairs to memcached.
|
|
17
19
|
#
|
|
18
|
-
# A transient network error is retried automatically. If a server
|
|
19
|
-
# unreachable after retrying, raises
|
|
20
|
+
# A transient network error is retried automatically. If a server stops
|
|
21
|
+
# during the call and is still unreachable after retrying, raises
|
|
22
|
+
# Dalli::NetworkError without failover (see KeyGrouping).
|
|
20
23
|
#
|
|
21
24
|
# A value that can't be marshalled or is over value_max_bytes is skipped;
|
|
22
25
|
# the other keys are still stored, then the first such error is raised.
|
|
@@ -47,7 +50,7 @@ module Dalli
|
|
|
47
50
|
# rebuilt from it: a long key is digested, and a Symbol becomes a String.
|
|
48
51
|
def setup_requests(hash, ttl, req_options)
|
|
49
52
|
pairs = hash.transform_keys { |k| @key_manager.validate_key(k.to_s) }
|
|
50
|
-
groups = groups_for_keys(pairs.keys)
|
|
53
|
+
groups = groups_for_keys(pairs.keys, 'set')
|
|
51
54
|
make_set_requests(groups, pairs, ttl, req_options)
|
|
52
55
|
groups.keys
|
|
53
56
|
end
|
|
@@ -92,17 +95,5 @@ module Dalli
|
|
|
92
95
|
Dalli.logger.debug { "unable to complete pipelined set on server #{server.name}" }
|
|
93
96
|
end
|
|
94
97
|
end
|
|
95
|
-
|
|
96
|
-
def groups_for_keys(keys)
|
|
97
|
-
groups = @ring.keys_grouped_by_server(keys)
|
|
98
|
-
|
|
99
|
-
if (unfound_keys = groups.delete(nil))
|
|
100
|
-
Dalli.logger.debug do
|
|
101
|
-
"unable to set #{unfound_keys.length} keys because no matching server was found"
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
groups
|
|
106
|
-
end
|
|
107
98
|
end
|
|
108
99
|
end
|
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -39,8 +39,9 @@ module Dalli
|
|
|
39
39
|
|
|
40
40
|
def initialize(attribs, client_options = {})
|
|
41
41
|
hostname, port, socket_type, @weight, user_creds = ServerConfigParser.parse(attribs)
|
|
42
|
+
# Credentials are unused since Dalli 5: warned about, and not kept
|
|
42
43
|
warn_uri_credentials(user_creds)
|
|
43
|
-
@options = client_options
|
|
44
|
+
@options = client_options
|
|
44
45
|
@raw_mode = client_options[:raw]
|
|
45
46
|
@defer_drain = client_options[:defer_drain] ? true : false
|
|
46
47
|
@verify_reply_keys = client_options[:verify_reply_keys] ? true : false
|
|
@@ -68,24 +69,25 @@ module Dalli
|
|
|
68
69
|
|
|
69
70
|
begin
|
|
70
71
|
request_completed = false
|
|
71
|
-
|
|
72
|
+
# Drained before the request starts, so a failure while draining
|
|
73
|
+
# isn't taken for one after this request was sent
|
|
72
74
|
drain_pending_quiet_responses(opkey)
|
|
75
|
+
@connection_manager.start_request!
|
|
73
76
|
response = send(opkey, *args)
|
|
74
|
-
|
|
75
|
-
# end of the quiet block knows this server needs draining
|
|
76
|
-
@connection_manager.mark_quiet_responses_pending! if quiet? && opkey != :noop
|
|
77
|
-
|
|
78
|
-
# pipelined_get/pipelined_get_interleaved emit query but don't read the response(s)
|
|
79
|
-
@connection_manager.finish_request! unless %i[pipelined_get pipelined_get_interleaved].include?(opkey)
|
|
80
|
-
|
|
77
|
+
finish_request(opkey)
|
|
81
78
|
request_completed = true
|
|
82
79
|
response
|
|
83
|
-
rescue Dalli::
|
|
80
|
+
rescue Dalli::RetryableNetworkError => e
|
|
81
|
+
raise_unless_resend_safe!(e, opkey, args)
|
|
82
|
+
raise
|
|
83
|
+
rescue Dalli::MarshalError, Dalli::UnmarshalError, Dalli::ValueOverMaxSize => e
|
|
84
84
|
log_marshal_err(args.first, e) if e.is_a?(Dalli::MarshalError)
|
|
85
85
|
# Raised only while the connection is clean: a value is marshalled
|
|
86
86
|
# before any of its request is written, and write_multi_req raises
|
|
87
|
-
# only after reading every reply.
|
|
88
|
-
#
|
|
87
|
+
# only after reading every reply. Likewise a value is unmarshalled
|
|
88
|
+
# only after its reply is read, and the multi-gets raise only after
|
|
89
|
+
# reading every reply. Closing the connection would discard earlier
|
|
90
|
+
# quiet writes still waiting in the write buffer.
|
|
89
91
|
@connection_manager.abort_request!
|
|
90
92
|
request_completed = true
|
|
91
93
|
raise
|
|
@@ -191,8 +193,9 @@ module Dalli
|
|
|
191
193
|
end
|
|
192
194
|
|
|
193
195
|
# If the status is ok and the key is not nil, then this is a
|
|
194
|
-
# getkq response with a value that we want to set in the response hash
|
|
195
|
-
|
|
196
|
+
# getkq response with a value that we want to set in the response hash.
|
|
197
|
+
# A false status is a reply to skip (see getk_response_from_buffer).
|
|
198
|
+
if status && key
|
|
196
199
|
if block
|
|
197
200
|
yield key, value, cas
|
|
198
201
|
else
|
|
@@ -211,31 +214,38 @@ module Dalli
|
|
|
211
214
|
|
|
212
215
|
values || {}
|
|
213
216
|
end
|
|
214
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
215
217
|
|
|
216
|
-
# Abort current pipelined get
|
|
217
|
-
#
|
|
218
|
-
#
|
|
218
|
+
# Abort current pipelined get, when the get_multi stopped before reading
|
|
219
|
+
# all its replies: the caller's block raised or broke, an external
|
|
220
|
+
# timeout fired, or another server failed. The underlying socket is
|
|
219
221
|
# disconnected, and the exception is swallowed.
|
|
220
222
|
#
|
|
221
223
|
# Returns nothing.
|
|
222
|
-
def pipeline_abort
|
|
223
|
-
|
|
224
|
-
@pipeline_out_of_step = false
|
|
225
|
-
@expected_reply_keys = nil
|
|
224
|
+
def pipeline_abort
|
|
225
|
+
clear_pipeline
|
|
226
226
|
@connection_manager.abort_request!
|
|
227
227
|
return true unless connected?
|
|
228
228
|
|
|
229
229
|
# Closes the connection, which ensures that our connection is in a
|
|
230
|
-
# clean state for future requests.
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
# row would otherwise mark a healthy server down.
|
|
234
|
-
log_pipeline_abort(timed_out)
|
|
230
|
+
# clean state for future requests. This server didn't fail, so it
|
|
231
|
+
# doesn't count toward socket_max_failures.
|
|
232
|
+
Dalli.logger.debug { "#{name} get_multi stopped before reading all replies; closing the connection" }
|
|
235
233
|
@connection_manager.close
|
|
236
234
|
true
|
|
237
235
|
end
|
|
238
236
|
|
|
237
|
+
# A pipelined get waited socket_timeout for this server to send
|
|
238
|
+
# anything. Counted as a failed read, like a get's read timeout: raises
|
|
239
|
+
# Dalli::RetryableNetworkError, or Dalli::NetworkError once the server
|
|
240
|
+
# reaches socket_max_failures and is marked down.
|
|
241
|
+
def pipeline_timed_out!
|
|
242
|
+
clear_pipeline
|
|
243
|
+
# Raised and rescued so that down! records it as the reason
|
|
244
|
+
raise IO::TimeoutError, "No reply to get_multi within #{@connection_manager.socket_timeout}s"
|
|
245
|
+
rescue IO::TimeoutError => e
|
|
246
|
+
@connection_manager.error_on_request!(e)
|
|
247
|
+
end
|
|
248
|
+
|
|
239
249
|
# Did the last call to #pipeline_response_setup complete successfully?
|
|
240
250
|
def pipeline_complete?
|
|
241
251
|
!response_buffer.in_progress?
|
|
@@ -250,14 +260,74 @@ module Dalli
|
|
|
250
260
|
|
|
251
261
|
private
|
|
252
262
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
private_constant :URI_CREDENTIAL_WARNING
|
|
256
|
-
|
|
263
|
+
# Dalli::Client strips credentials before a server is built, so this is
|
|
264
|
+
# only for a server built directly from a URI with them
|
|
257
265
|
def warn_uri_credentials(user_creds)
|
|
258
266
|
return if user_creds[:username].nil? && user_creds[:password].nil?
|
|
259
267
|
|
|
260
|
-
Dalli.logger.warn(URI_CREDENTIAL_WARNING)
|
|
268
|
+
Dalli.logger.warn(ServersArgNormalizer::URI_CREDENTIAL_WARNING)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Requests that leave memcached in the same state, and get the same kind
|
|
272
|
+
# of reply, when it applies them twice. Only these are retried after a
|
|
273
|
+
# failure that came once some of the request was sent, since memcached
|
|
274
|
+
# may have applied it and only the reply was lost. A set, replace or
|
|
275
|
+
# delete is in this group only without a CAS (see #resend_safe?). :cas
|
|
276
|
+
# is the read behind get_cas.
|
|
277
|
+
RESEND_SAFE_OPS = %i[get gat touch cas version stats reset_stats noop flush
|
|
278
|
+
read_multi_req read_multi_with_metadata_req pipelined_get pipelined_get_interleaved
|
|
279
|
+
write_multi_req pipelined_set delete_multi_req pipelined_delete
|
|
280
|
+
finish_pipelined_delete].freeze
|
|
281
|
+
private_constant :RESEND_SAFE_OPS
|
|
282
|
+
|
|
283
|
+
# A request that may have been applied, and can't safely be sent again,
|
|
284
|
+
# fails with a NetworkError the callers don't retry. Sending it again
|
|
285
|
+
# would apply an incr, decr, append or prepend twice, or report an add
|
|
286
|
+
# or CAS write that succeeded as failed.
|
|
287
|
+
def raise_unless_resend_safe!(err, opkey, args)
|
|
288
|
+
return unless err.request_sent?
|
|
289
|
+
return if resend_safe?(opkey, args)
|
|
290
|
+
|
|
291
|
+
raise Dalli::NetworkError, "#{name}: #{opkey} failed after it was sent, so it may have been applied; " \
|
|
292
|
+
"not retrying it. #{err.message}"
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def resend_safe?(opkey, args)
|
|
296
|
+
case opkey
|
|
297
|
+
# A replace sent again stores the same value, as a set does
|
|
298
|
+
when :set, :replace then no_cas?(args[3])
|
|
299
|
+
when :delete then no_cas?(args[1])
|
|
300
|
+
# With N or R, only the first request wins the recache race; a resent
|
|
301
|
+
# one would report the race lost to the caller that won it
|
|
302
|
+
when :meta_get then !(args[1] && (args[1][:vivify_ttl] || args[1][:recache_ttl]))
|
|
303
|
+
else RESEND_SAFE_OPS.include?(opkey)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# A CAS of 0 or nil means an unconditional write
|
|
308
|
+
def no_cas?(cas)
|
|
309
|
+
cas.nil? || cas.to_i.zero?
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Only a request whose reply was read resets the failure count. Otherwise
|
|
313
|
+
# a server that takes set_multi's writes, then drops the connection before
|
|
314
|
+
# the noop's reply, would be back at zero on every retry, never reach
|
|
315
|
+
# socket_max_failures, and be retried forever.
|
|
316
|
+
def finish_request(opkey)
|
|
317
|
+
case opkey
|
|
318
|
+
# Finished by finish_pipeline, once the replies are read
|
|
319
|
+
when :pipelined_get, :pipelined_get_interleaved then nil
|
|
320
|
+
when :pipelined_set, :pipelined_delete then @connection_manager.finish_unanswered_request!
|
|
321
|
+
else
|
|
322
|
+
if quiet? && opkey != :noop
|
|
323
|
+
# Its reply is left on the socket; the end of the quiet block, or
|
|
324
|
+
# the next non-quiet request, reads it
|
|
325
|
+
@connection_manager.mark_quiet_responses_pending!
|
|
326
|
+
@connection_manager.finish_unanswered_request!
|
|
327
|
+
else
|
|
328
|
+
@connection_manager.finish_request!
|
|
329
|
+
end
|
|
330
|
+
end
|
|
261
331
|
end
|
|
262
332
|
|
|
263
333
|
ALLOWED_QUIET_OPS = %i[add replace set delete incr decr append prepend flush noop].freeze
|
|
@@ -354,7 +424,7 @@ module Dalli
|
|
|
354
424
|
|
|
355
425
|
kwargs = {}
|
|
356
426
|
kwargs[:stale] = true if opts[:invalidate]
|
|
357
|
-
kwargs[:ttl] = TtlSanitizer.sanitize(
|
|
427
|
+
kwargs[:ttl] = TtlSanitizer.sanitize(opts[:tombstone_ttl]) if opts[:tombstone_ttl]
|
|
358
428
|
kwargs[:drop_value] = true if opts[:drop_value]
|
|
359
429
|
kwargs
|
|
360
430
|
end
|
|
@@ -374,6 +444,8 @@ module Dalli
|
|
|
374
444
|
# the expected state before pipeline_response_setup is called.
|
|
375
445
|
response_buffer.clear
|
|
376
446
|
response_buffer.raw = raw_request?(options)
|
|
447
|
+
# Drops an error held from a get_multi that stopped early
|
|
448
|
+
response_processor.take_unmarshal_error
|
|
377
449
|
expect_reply_keys(keys)
|
|
378
450
|
|
|
379
451
|
# The terminating noop is sent by pipeline_response_setup
|
|
@@ -388,6 +460,8 @@ module Dalli
|
|
|
388
460
|
# left by an earlier get_multi that stopped early belong to its replies.
|
|
389
461
|
response_buffer.reset
|
|
390
462
|
response_buffer.raw = raw_request?(options)
|
|
463
|
+
# Drops an error held from a get_multi that stopped early
|
|
464
|
+
response_processor.take_unmarshal_error
|
|
391
465
|
expect_reply_keys(keys)
|
|
392
466
|
|
|
393
467
|
keys.each_slice(chunk_size) do |chunk|
|
|
@@ -427,7 +501,7 @@ module Dalli
|
|
|
427
501
|
break
|
|
428
502
|
end
|
|
429
503
|
|
|
430
|
-
results[key] = [value, cas]
|
|
504
|
+
results[key] = [value, cas] if status && key
|
|
431
505
|
end
|
|
432
506
|
rescue SystemCallError, Dalli::NetworkError
|
|
433
507
|
# Ignore errors during drain - they'll be handled in fetch_responses
|
|
@@ -479,10 +553,14 @@ module Dalli
|
|
|
479
553
|
end
|
|
480
554
|
|
|
481
555
|
# Called after the noop response is received at the end of a set
|
|
482
|
-
# of pipelined gets
|
|
556
|
+
# of pipelined gets. A value that couldn't be deserialized is raised
|
|
557
|
+
# here, once all of this server's replies are read, so the connection
|
|
558
|
+
# is left in step.
|
|
483
559
|
def finish_pipeline
|
|
484
560
|
response_buffer.clear
|
|
485
561
|
@connection_manager.finish_request!
|
|
562
|
+
unmarshal_error = response_processor.take_unmarshal_error
|
|
563
|
+
raise unmarshal_error if unmarshal_error
|
|
486
564
|
|
|
487
565
|
true # to simplify response
|
|
488
566
|
end
|
|
@@ -496,12 +574,10 @@ module Dalli
|
|
|
496
574
|
'(a request made from inside the get_multi block?)'
|
|
497
575
|
end
|
|
498
576
|
|
|
499
|
-
def
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
Dalli.logger.debug { "#{name} get_multi stopped before reading all replies; closing the connection" }
|
|
504
|
-
end
|
|
577
|
+
def clear_pipeline
|
|
578
|
+
response_buffer.clear
|
|
579
|
+
@pipeline_out_of_step = false
|
|
580
|
+
@expected_reply_keys = nil
|
|
505
581
|
end
|
|
506
582
|
|
|
507
583
|
# The single-server set_multi path passes its whole pairs hash as the
|